引言:
其实linux内核并没有想象中的那么难,只要自己静下心,以源码为主线,再加上几本经典的书就可以慢慢的去学习kernel了.当自己有了一定的基础之后,学习kernel源码也变得比较容易了,没有了大二刚开始那会看kernel时的一脸茫然.这一节把关于进程管理的相关知识点总结下,这样不仅可以加深自己学习的印象,还便于日后查阅.
正文:
- 进程通常包含可执行程序代码,打开文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的地址空间及一个或多个执行线程。
- 每个线程都拥有一个独立的程序计数器,进程栈和一组进程寄存器。内核调度的是线程而不是进程。
- 现代操作系统中进程提供两种虚拟机制:虚拟处理器,虚拟内存。
- 线程之间可以共享虚拟内存,但每个都拥有各自的虚拟处理器。
- 程序本身并不是进程,而是处于执行期的程序以及相关的资源的总称。完全可能多个进程执行的是同一个程序。并且进程间还可以共享许多诸如打开的文件,地址空间之类的资源。
- 调用fork()结束时,在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次,一次回到父进程,另一次回到新的子进程。
- 通常,创建新的进程都是为了立即执行新的,不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。在现代linux内核中,fork()实际上是由clone()系统调用实现的。最终,程序通过exit()系统调用退出执行。这个函数会终结进程并释放其占用的资源。
- 父进程可以通过wait4()系统调用查询子进程是否终结。进程退出执行后被设置成僵死状态,直到它的父进程调用wait()或waitpid()为止。
- 内核把进程的列表存放在任务队列中(双向循环链表),链表的每一项都是task_struct,称为进程描述符。
- linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的。因此只需在栈底或栈顶创建一个新的struct tread_info.
- 访问进程描述符是一个重要的频繁操作。
- 进程描述符中的state域描述了当前状态(运行态,可中断,不可中断,被其他进程跟踪,停止态)。
- 在执行ps(1)命令时,那些标为D状态而又不能被杀死的进程的原因是此进程状态为不可中断态,此状态不对信号做出相应。所以你不可能给它发送Sigkill信号,即使有,终结这样一个任务也是不明智的选择,因为该任务有可能正在执行重要的操作,甚至还可能持有一个信号量。
- 设置当前进程状态,set_task_state(task,state)
- 进程上下文:可执行代码是进程的重要组成部分,这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。
系统调用和异常处理程序是内核明确定义的接口。进程只有通过这些接口才能陷入内核执行。
- 每个进程可以拥有0个或多个子进程。
- 在中断上下文,系统不代表进程执行,而是执行一个中断处理程序。不会有进程去干扰这些中断处理程序,所以此时不存在进程上下文。
- init进程的进程描述符是作为init_task静态分配的。
- for_each_process()宏提供了依次访问整个任务队列的能力。
- fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在PID,PPID,和某些资源和统计量。exec()函数负责读取可执行文件并将其载入地址空间开始运行。
- linux 的fork()使用写时拷贝页实现(一种可以推迟甚至免除拷贝数据的技术)。此时内核并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。
- fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
- fork()—>clone(){系统调用}—>do_fork()—>copy_process()
- 内核有意选择子进程首先执行,因为一般子进程都会马上调用exec()函数,可避免写时拷贝的额外开销。
- 除了不拷贝父进程的页表项以外,vfork()和fork()功能相同。子进程作为父进程的一个单独线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec().子进程不能向地址空间写入。
- 线程机制提供了在同一程序内共享内存地址空间的一组线程,这些线程还可以共享打开的文件和其他资源。支持并发程序设计技术,在多处理器系统上,也能保证正真的并行处理。
- linux中线程和进程没有区分,把线程当做一个与其他进程共享某些资源的进程。
- 传递给clone()的参数标志决定了新创建进程的行为方式和父子进程间共享的资源种类。
- 内核线程没有独立的地址空间,它们只在内核空间运行。也可被调度和抢占。
内核线程只能由其他内核线程创建。内核是通过从kthreadd内核进程中衍生出所有新的内核线程来自动处理这一点的。
- 当一个进程终结时,内核必须释放它所占有的资源,并告知其父进程。
- 一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时。
- c编译器会在main函数返回点后面放置调用exit()的代码。
- 当进程接受到它既不能处理也不能忽略的信号或异常时,它还可能被动终结。终结大部分工作由do_exit()来完成。
- do_exit()会调用schedule()切换到新的进程。因为处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程所执行的最后一段代码。do_exit()永不返回。
- 孤儿进程会在退出时永远处于僵死状态,白白耗费内存。解决方法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init()做为它的父进程。
- init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。