三.进程管理
3.2 进程描述符及任务结构
内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符(process descriptor)的结构,它包含一个具体进程的所有信息。
内核通过一个唯一的进程标识符(process identification value)或PID来标识每个进程。PID实际是个int类型,最大值默认设置为32768(short int的最大值)。内核把每个进程的PID存在它们各自的进程描述符中。最大值即是系统中允许同时存在的进程的最大数目。
3.3 进程创建
在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix采用两个单独的函数去执行:fork()和exec()去执行。首先,fork()函数通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(例如:挂起的信号)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。fork()执行时使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术,内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。
资源的复制只有在需要写入的时候才进行。在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下(如fork()后立即调用exec())它们就无需复制了。
fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
3.4 线程在Linux中的实现
Linux把所有的线程都当作进程来实现。线程被视为一个与其他进程共享某些资源的进程。每个线程都有唯一隶属于自己的task_struct。对于linux来说,它只是一种进程间共享资源的手段。假如有一个包含四个线程的进程,在提供专门线程支持的系统中,通常会有一个包含指向四个不同线程的指针的进程描述符。该描述符负责描述像地址空间、打开的文件这样的共享资源。线程本身再去描述它独占的资源。相反,Linux仅仅创建四个进程并分配四个普通的task_struct结构。建立这四个进程时指定他们共享某些资源。
3.4.1 线程的创建
线程的创建和普通进程类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源。
clone(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND,0);
上述代码产生的结果和调用fork()差不多,只是父子俩共享地址空间、文件系统资源、文件描述符和信号处理程序。
3.4.2 进程终结
do_exit()调用schedule()切换到新的进程,此时进程的状态为EXIT_ZOMBIE,不会再被调度。此时与进程有关的资源都被释放掉了。它占用的所有内存就是内核栈、thread_info结构和task_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还给系统使用。
如果父进程在子进程之前推出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程会在退出时永远处于僵死状态,白白地耗费内存。解决方法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。