1. 进程
进程就是处于执行期的程序。但进程不仅仅包括可执行程序代码,还包括打开的文件、内存、信号量、处理器状态等资源。
线程是进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。
linux对线程和进程并不特别区分,线程是一种特殊的进程,它会与其他进程共享某些资源。
1.1 进程描述符
内核把进程的列表存放在叫做任务队列的双向循环链表中,链表中的每一项都是一个结构体task_struct,它被称为进程描述符。该结构体定义在 linux/sched.h 中,它几乎包含了一个进程的所有信息,所以非常复杂,其大致结构如下图所示:
进程描述符与任务队列的关系如下图所示:
linux通过slab分配器分配task_struct结构。slab分配器在《linux内核设计与实现》的第12章“内存管理”有详细讲解。
1.2 进程状态
进程描述符中的state字段描述了进程的当前状态(参见图3-3)。系统中的每个进程都必然处于五种进程状态中的一种。state的值也必为下列五种状态标志之一:
TASK_RUNNING(运行)—进程是可执行的;它或者正在执行,或者在运行队列中等待执行(运行队列将会在第4章中讨论)。这是进程在用户空间中执行惟一可能的状态;也可以应用到内核空间中正在执行的进程。
TASK_INTERRUPTIBLE(可中断)—进程正在睡眠(也就是说它被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并投入运行。
TASK_UNINTERRUPTIBLE(不可中断)—除了不会因为接收到信号而被唤醒从而投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不作响应,所以较之可中断状态,使用得较少。
TASK_ZOMBIE(僵死)—该进程已经结束了,但是其父进程还没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4(),进程描述符就会被释放。
TASK_STOPPED(停止)—进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。
1.3 进程家族树
在linux系统中,所有的进程都是PID为1的init进程的后代。
系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有0个或多个子进程。
进程间的关系存放在进程描述符task_struct结构体中,每个task_struct都包含一个指向其父进程的指针parent,和一个指向其子进程链表的指针children。
2. 进程管理
linux进程通过调用fork()函数复制当前进程来创建一个子进程,通过exec()函数读取可执行文件并将其载入地址空间开始运行,最后通过exit()函数终结自己并将占用的资源释放掉。
进程退出执行后被设置为僵死状态,直到它的父进程调用wait()或waitpid为止。父进程可以通过wait4()查询子进程是否终结。
2.1 进程创建
linux的fork()使用写时拷贝页实现。在创建子进程后,内核并不会复制整个地址空间,而是让父子进程共享地址空间,只有在进程地址空间的内容要发生变化时,才会将父进程变化的地址空间复制一份给子进程。在一般情况下,进程创建后都会立即调用exec()函数来运行一个可执行文件,写时拷贝技术可以避免复制大量根本就不会被使用的数据。
这样,fork()的实际开销就是复制父进程的页表以及给子进程创建一个唯一的进程描述符。
fork()实际上是由clone()系统调用实现的,它将需要的参数标志传递给clone(),然后由clone()去调用do_fork(),do_fork()调用copy_process()函数,让进程开始运行。
copy_process()函数为新进程创建一个内核栈、thread_info结构和task_struct结构。根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
fork()之后,内核会通过将子进程放在队列的前面,让子进程先执行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销。
2.2 进程终结
当一个进程终结时,内核必须释放它所占用的资源,并把这一消息告知其父进程。
进程调用exit()函数执行终结任务,exit()调用do_exit()函数来完成大部分的资源释放工作。
在do_exit()之后,进程已经僵死不能再运行了,但是系统还保留了它的进程描述符,这样做可以让系统有办法在子进程终结后仍能获得它的信息。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。
如果父进程在子进程之前退出,必须有一个机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态,白白地耗费掉内存。对于这个问题,解决方法是给子进程在当前进程组内找一个进程作为父亲,如果不行,就让init做它们的父进程。
2.3 线程在linux中的实现
linux实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念,而是把所有的线程都当作进程来实现。线程仅仅被视为一个与其他进程共享某些资源的进程。
线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源:
/* 指明父子进程共享地址空间、文件系统资源、文件描述符和信号处理程序 */
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
内核经常需要在后台执行一些操作,这可以通过内核线程完成。内核线程和普通进程的区别在于内核线程没有独立的地址空间(它的task_struct结构的mm指针被设置为NULL),它只在内核空间运行,从来不切换到用户空间去。内核线程和普通进程一样,可以被调度,也可以被抢占。