Linux-Kernel进程管理

进程

进程是出于执行期的程序。但进程并不仅仅局限于一段可执行程序代码,还包含其他如打开的文件、挂起的信号、内核内部数据、处理器状态、线程等其他资源。进程就是正在执行的程序代码的实时结果。

线程是在进程汇中活动的对象,每个线程都有独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程而不是进程。linux系统的线程实现非常特别:她对线程和进程不加特殊区分,线程只不过是一种特殊的进程罢了。

调用 fork 函数创建子进程,接着调用 exec 函数创建新的地址空间,并把新的程序载入其中。在现代 linux 内核中,fork 实际上由 clone() 系统调用实现。最后,调用 exit 退出,释放占用的资源,进入僵尸状态,父进程通过 wait 或 waitpid 调用来释放。

进程描述符及任务结构

内核把进程的列表存放在叫做任务队列的双向循环链表中,链表中每一项都是类型为 task_struct 的称为进程描述符(PCB)的结构,进程描述符中包含一个具体进程的所有信息。

分配进程描述符

linux 通过 slab 分配器分配 task_struct 结构,这样能达到对象复用和缓和着色的目的。以前,进程的 task_struct 存放在它内核栈的末尾,现在使用 slab 分配器动态分配(通过预分配和重复使用降低资源消耗),只需要在栈底创建一个新的结构 struct thread_info ,其中有一个指向进程描述符的指针。

进程描述符的存放

内核通过一个唯一的进程标识值或 PID 来标识每个进程。默认最大值是 32768 ,这个值越大,转一圈就越慢(双向循环链表)。

为了快速获得当前进程的进程描述符,可以拿出一个专门寄存器来存放指向进程描述符的指针,也可以使用 thread_info 来查找。

进程状态

运行、可中断、不可中断、被其他进程跟踪、停止。

设置当前进程状态

set_task_state(task, state);
// task->state = state;

进程上下文

一般程序在用户空间执行,当执行了系统调用,就陷入内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中,在此上下文中 current 宏是有效的。

进程家族树

进程之间存在父子进程这样的继承关系,所有进程都是 PID 为 1 的 init 进程的后代。task_struct 中有很多元素揭示了这种关系。

进程创建

  1. fork() 通过拷贝当前进程创建一个子进程
  2. exec() 负责读取可执行文件并将其载入地址空间开始运行

写时拷贝

在 fork 时并不复制进程地址空间,因为后续不一定会用到(子进程一般直接调用 exec() 函数)。读取时使用共享的,只有当需要写入时才复制一份。

这样 fork() 的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

fork()

通过 clone() 系统调用来实现,通过一系列参数来指明父子进程需要共享的资源。具体步骤不表,在创建完成之后,内核有意选择子进程先执行(并不一定),因为一般子进程都是马上调用 exec() 函数,这样可以避免写时拷贝的额外开销。

vfork()

除了不拷贝父进程的页表项之外,和 fork() 的功能相同,子进程会比父进程先执行。在没有写时拷贝时,是很有意义的,但是现在 fork() 写时拷贝并且明确了子进程先执行,那么好处就剩下不拷贝父进程页表了,但是带来的漏洞会更多。所以,forget it。

线程在linux中的实现

linux 把所有的线程都当作进程,仅仅被视为一个与其他进程共享某些资源的进程,也有隶属于自己的 task_struct 。

创建线程

同样调用 clone() 系统调用,通过一些参数来指明需要共享的资源。

内核线程

内核经常需要在后台执行一些操作,这种任务通常可以通过内核线程完成。内核线程是独立运行在内核空间的标准线程,和普通线程的区别在于没有独立的地址空间,mm 指针设置为 NULL 。

进程终结

进程结束时,大部分都要靠 do_exit() 来完成。释放与进程相关联的所有资源,进程处于 EXIT_ZOMBIE 退出状态,只占用内核栈、thread_info 结构和 task_struct 结构。存在的目的在于向父进程提供信息。

删除进程描述符

由父进程通过 wait() 系统调用来实现的。

孤儿进程

如果父进程先于子进程退出,子进程就会成为一个孤儿进程,会永远处于僵死状态。因此,我们需要一个机制为其找到一个父进程(先从线程组找,否则就 init)