进程简介

进程(Process)是Unix操作系统最基本的抽象概念之一。进程是正在执行的程序,同时也是操作系统进行资源管理的最小单位,进程需要管理打开的文件、挂起的信号、内核内部数据、处理器状态等。

线程(Thread)是进程中正在执行的程序片段,是操作系统进行调度的最小单位,一个线程指的是进程中一个单一顺序的控制流。Linux中线程只是一个特殊的进程,并没有对进程和线程进行专门的区分。

进程提供了两种虚拟抽象:虚拟处理器和虚拟内存。虚拟处理器让进程觉得自己独占处理器,虚拟内存让程序觉得自己独占系统所有内存。

Linux系统中通常通过fork()创建一个新的进程,通过exec函数族来载入新的程序执行(一般在fork了一个新进程之后调用),通过exit()退出执行。

进程描述符task_struct

进程描述符包含一个进程的所有信息,使用task_struct结构存储。内核使用双向循环列表来存储所有进程的文件描述符,该链表被称为任务队列(task list)。task_struct是一个非常大的结构,存储的信息包括进程号、打开的文件、进程的地址空间、挂起的信号、进程状态等。 task_struct

task_struct结构使用slab allocator进行分配(slab通过对象重用的方式提高分内存使用效率)。和进程描述符相关的一个数据结构是tread_info,这个结构存储在内核栈的底部(此时栈向下扩展)或者顶部(此时栈向上扩展)。每个进程都有两个栈,用户栈和内核栈,在用户态运行时CPU堆栈指针寄存器指向用户栈地址。进程在内核态运行时,CPU堆栈指针寄存器指向内核栈地址。内核栈使用的空间很小,通常为8KB。 内核栈

内核通过唯一的进程标识值PID来标识每个进程,PID默认的最大值为32768,可以修改最大值,上限约为4百万。内核中一般通过task_struct指针来处理进程,通过current宏可以获得当前正在CPU上执行的进程的task_struct指针。

进程状态

task_struct的state域存储了进程当前的状态,总共有7种状态,一个进程在一个时刻只可能处于一种状态。下面列出了这7种状态。

  • TASK_RUNNING 进程正在CPU上执行,或者等待被执行(ready)
  • TASK_INTERRUPTIBLE 进程暂停中,等待一些条件为真,比如进程获得了需要的资源、接收到硬件终端、或者收到某些signal等。
  • TASK_UNINTERRUPTIBLE 和TASK_INTERRUPTIBLE类似,区别在于进程不会被signal唤醒,只能等待某些事件(资源、硬件终端等)
  • TASK_STOPPED 进程暂停执行,进程没有运行也不能进入运行状态(但是也没死),进程在接收到SIGSTOP, SIGTSTP, SIGTTIN或者SIGTTOU 信号时会进入这个状态。
  • TASK_TRACED 进程被debugger暂停了执行,例如通过ptrace()对程序进行调试。
  • EXIT_ZOMBIE 进程执行已经结束了,但是父进程还没有调用wait4()或者waitpid()来回收该进程。
  • EXIT_DEAD 进程执行已经结束了,父进程刚刚调用wait4()或者waitpid()来回收该进程,进程正在被从系统移除,这个状态主要用于避免多个进程同时对该进程调用类似wait()的函数或者系统调用

进程创建

Linux系统中通常通过fork()创建一个新的进程,通过exec函数族来改变进程执行的的程序。

Copy-on-Write

传统上,fork时会将父进程的所有资源都复制一份给子进程,但是这种方法的效率太差,特别是fork之后马上调用exec载入其他程序时,前面复制的资源都会被抛弃。Linux使用copy-on-write策略来提高效率。fork时,只会复制父进程的page table 并为子进程创建一个新的进程描述符,父子进程使用相同的physical page,当其中一个进程对某个physical page进行写操作时,内核会将该页内容复制到一个新的physical page,并将该page分配给进行写操作的进程。

fork() 与 clone()

linux使用clone()系统调用来实现fork()。调用clone()可以指定一系列参数,来规定父进程和子进程分享哪些资源(比如共享地址空间等),clone()会调用do_fork(),do_fork()会调用copy_process()。

Linux线程的实现

线程是现代操作系统的常见一个抽象概念,线程之间共享内存地址空间、打开的文件等资源,并且可以并发。

Linux的线程实现机制非常特殊,对Linux内核来说,不存在线程的概念,Linux使用标准进程来实现线程,每个线程都一个自己独占的task_struct,线程只是和其他进程共享地址空间等资源的进程而已。

创建线程

线程创建的方式和内存是一样的,区别在于给clone()系统调用传递的参数不一样,创建线程时,clone的参数规定了哪些资源是共享的。

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

上述代码产生的作用和普通的fork是类似的,只是父子进程共享地址空间、文件系统资源、文件描述符、信号处理程序。此时的子进程和父进程就是我们说的线程了。

fork()的实现则是clone(SIGCHLD,0),仅共享信号处理程序。

内核线程

内核通过内核线程在后台执行各种操作,内核线程指的是只存在于内核空间的标准进程(线程),内核线程(进程)和普通进程之间的最大区别是内核线程没有独立的地址空间(task_struct的mm域,即地址空间域为NULL),他们只在内核空间运行,不会切换到用户空间。实际上内核线程会使用上一个运行的进程的地址空间,因为用户空间和内核空间是分开的,所以不会产生冲突。内核线程和普通进程一样可以被调度和抢占。只有内核线程可以创建新的内核线程,内核线程通常是在系统启动时被创建的。

进程终结

通常进程的终结发生在exit()系统调用时,进程可能显示地调用这个系统调用,也可能隐式地调用(C编译器会在main函数return点后面插入exit语句)。当进程接收到一个终结信号或者发生进程无法处理也不能忽略的异常时,进程会被动地终结。终结进程的大部分工作是在do_exit()中完成的,do_exit()会释放进程占用的资源。包括

  • 1)释放地址空间结构mm_struct(如果没有共享,这就会彻底释放该地址空间对象)。
  • 2)将使用的文件描述符、文件系统的引用计数减1。
  • 3)向父进程发送信号,并给当前进程的子进程寻找新的父亲。-
  • 4)将进程描述符task_struct的exit_state设为EXIT_ZOMBIE(僵尸进程),成为僵尸进程。

至此进程在内存只有内核栈、thread_info结构、task_struct结构。作为僵尸进程存在的唯一目的就是为父进程提供信息。

父进程调用wait函数族(最终使用wait4()实现)并被阻塞,当有一个子进程退出时,函数会返回,并提供子进程的相关信息。release_task()会被调用,彻底释放和该进程所有的数据结构(包括进程描述符、tread_info结构、内核栈)和剩下的资源。

僵尸进程与孤儿进程

如果父进程先于子进程退出,子进程会成为孤儿进程。此时必须给子进程找到新的进程作为父进程,否则当没有父进程的子进程退出时,因为没有父进程收尸,子进程会永远作为僵尸进程存在于系统中,浪费资源。Linux内核对此的处理方法是在当前线程组中为子进程寻找一个线程作为父亲,如果不行(比如当前线程组没有其他线程),就让init做父进程。init进程会例行地调用wait()来清除僵尸子进程。

参考资料

《Linux Kernel Development 3rd Edition》

《Understanding The Linux Kernel 3rd Edition》