向下之旅(三):进程管理(一)
进程是Unix操作系统最基本的抽象之一(另一个是文件)。进程不仅是一段可执行的代码,还包括其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,地址空间以及一个或多个执行线程,还包括用来存放全局变量的数据段。进程是处于执行器的程序和所包含资源的总称。(Linux对线程和进程并不特别区分,线程只不过是一种特殊的进程罢了)。
进程提供两种虚拟机制:虚拟处理器和虚拟内存。虽然实际上可能是多个进程在分享一个处理器,但是虚拟处理器给进程一个假象,让这些进程觉得自己在独享处理器。虚拟内存同样如此,同一进程之间的线程可以共享虚拟内存,但拥有各自的虚拟处理器。
进程通过调用fork()系统调用被创建。该系统调用通过复制一个现有的进程来创建一个全新的进程,调用fork()的进程被称为父进程,新产生的进程被称为子进程。fork()系统调用在内核返回两次:一次回到父进程,另一次回到新诞生的子进程。接着调用exec*()这族函数创建新的地址空间,并将新的程序载入。其中fork()实际上是由clone()系统调用实现的。最终,程序通过exit()系统调用退出执行,进入僵死状态,知道它的父进程调用wait()或waitpid()为止。父进程可以通过wait4()系统调用查询子进程是否终结,这使得进程拥有了等待特定程序执行完毕的能力。
内核把进程存放在叫做任务队列的双向循环链表中。链表中的每一项都是类型为task_struct,称为进程描述符的结构。该结构定义在<linux/sched.h>文件中。进程描述符中包含一个具体进程的完整信息。在32位机器上,它大约有1.7K字节。Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的(通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗)。这样只需在内核栈栈底(对于向下增长的栈来说)或栈顶(对于向上增长的栈来说)创建一个新的结构struct thread_info。这个新的结构能使得在汇编代码中计算其偏移变得相当容易。(struct thread_info来寻找task_struct)。

每个任务(进程)的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struck的指针。
内核通过一个唯一的进程标识值(process identification value)或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型。为了与老版本的Unix和Linux兼容,PID最大值默认设置为32768(short int 短整型的最大值)。内核把每个进程的PID存在它们各自的进程描述符中。这个最大值实际上就是系统中允许同时存在的进程的最大数目。对于大型服务器可能需要更多进程。这个值越小,转一圈就越快,本来数值大的进程比数值小的进程迟运行,但这样一来就破坏了这一原则,若确实需要的话,可以手动更改/proc/sys/kernel/pid_max的值来提高上限。
在内核中,访问进程通常需要获得指向其task_strcut指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏查找到当前正在运行进程的进程描述符的速度就非常重要。硬件不同,宏的实现也不同,X86上的体系结构因寄存器不富余,只能在内核栈的尾端创建thread_info结构,通过计算偏移间接的查找task_struct结构。而IBM基于RISC的现代微处理器,将task_struct的地址保存在一个寄存器中,因此性能更高。
进程描述符中的state域描述了进程的当前状态。共为5中:
1.TASK_RUNNING(运行)——进程是可执行的,它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行的唯一可能的状态,也可以应用到内核空间中正在执行的进程。
2.TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(即被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并投入执行。
3.TASK_UNINTERRUPTIBLE(不可中断)——除了不会因为接收到信号而被唤醒从而投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。
4.TASK_ZOMBLE(僵死)——该进程已经结束了,但是其父进程还没有调用wait4()系统调用。为了父进程能够获得它的信息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4(),进程描述符就会被释放。
5.TASK_STOPPED(停止)——进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接受到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。

内核经常需要调整某个进程的状态。这时最好使用set_task_state(task,state)函数,将task任务设置为state状态。必要的时候,它会设置内存屏障来强制其他处理器作重新排序(一般只有在SMP系统中有此必要)。
可执行程序代码是进程中重要的组成部分。这些代码从可执行文件中载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序调用了系统调用或者是触发了某个异常,它就陷入了内核控件。此时,内核“代表进程执行”并处于进程上下文中。在此上下文中current宏是有效的。系统调用和异常处理程序是对内核明确定义的接口。进程只有通过内核这些公布的接口,才能陷入内核执行——对内核的访问都必须经过这些接口。
Linux和Unix系统一样,所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。系统中的每一个进程都必有一个父进程,零个或多个子进程。进程间的关系存放在进程描述符中。每个task_struct都包含一个指向其父进程tast_struct、叫做parent指针,还包含一个称为children的子进程链表。所以,对于当前进程可以通过下面的代码在获得其父进程的进程描述符:
struct task_struct *my_parent = current->parent;
同样,也可以按以下方式访问子进程
struct task_struct *task; struct list_head *list; list_for_each(list, ¤t->children){ task = list_entry(list, struct task_struct, sibling); /* task 现在指向当前的某个子进程 */ }
实际上,可以通过这种继承体系结构从系统的任何一个进程出发查找到任意指定的其他进程。但大多数的时候,只需要通过简单的重复方式就可以遍历系统中的所有进程。当一个拥有大量进程的系统中通过重复来遍历所有的进程是非常耗时的。因此没有充足的理由,别这样做。
参考自:《Linux Kernel Development》.
浙公网安备 33010602011771号