之前写过一点总结,发现之前理解的好浅,好naive,今天再重新写一次。
前言:进程相关
什么是进程?
程序是一个可行的文件,而进程是一个执行中的程序实例。利用分时技术,在linux操作系统上可以同时运行多个进程。
进程由什么组成?
进程由可执行的指令代码、数据和堆栈区组成。进程中的代码和数据部分分别对应一个执行文件中的代码段和数据段。每个进程只能执行自己的代码和访问自己的数据及堆栈区(发现之前没有弄明白这么做)。进程之间的通信需要通过系统调用来执行。
内核如何管理进程?
内核程序通过进程表对进程进行管理,每个进程在进程表中占有一项。在linux系统中,进程表项是一个task_struct任务结构指针。有些书中成为进程控制块PCB或进程描述符PD。其中,保存着用于控制和管理进程的所有信息,主要包括进程当前运行的状态信息、信号、进程号、父进程号、运行时间累计值、正在使用的文件和本任务的局部描述符以及任务状态段信息。
什么是进程上下文?
当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为进程的上下文。
进程创建
unix的进程创建很特别。许多其他的操作系统提供了产生spawn进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。unix是将上述步骤分为两个单独的函数执行:fork()和exec()。
首先,fork()通过拷贝当前进程创建一个子进程;
其次,exec()函数负责读取可执行文件并将其载入地址空间开始运行。
这两个函数组合起来使用和其他系统使用单一函数的效果相似。
1. fork()
1.1 fork()的使用
fork系统调用
包含在如下头文件:
#include <sys/types.h>
#include <unistd.h>
函数功能:创建一个子进程
函数原型:pid_t fork(void);
fork()可能的返回结果有3种:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果创建子进程失败,返回一个负值给父进程,比如ENOMEM,内存不足;
fork()后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
另外,fork只是把进程当前的情况拷贝一份,不一定会从头到尾都拷贝。
1.2 copy-on-write
传统的fork()系统调用直接把所有的资源复制给新创建的进程,这种实现简单但效率地下,因为它拷贝的数据也许并不共享,更糟的是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。
所以,linux的fork()采用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。
在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
分配单独的物理空间:发生缺页中断,分配全新的内存空间。
《linux内核设计与实现》,fork之后内核会通过将子进程放在任务队列链表的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。(任务链表中的每一项为task_struct,称为进程描述符)
对比图:
(1)传统的fork():父进程p1创建子进程p2:
(2)linux的fork():
COW技术,内核只为新生成的子进程创建虚拟空间,它们来复制于父进程的数据结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
总结:
Linux的fork()使用写时拷贝(copy-on-write)实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。
1.3 fork()源码实现
fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。这句话,开始不能理解,看了源码就是这个意思。
(1)复制父进程的页表:调用copy_page_tables();
(2)给子进程创建唯一的进程描述符task_struct:
p = (struct task_struct *) get_free_page(),参考[4]中作者写的copy-on-write是基于page的,而不是基于进程的。
2. vfork()
在fork还没实现copy on write之前。Unix设计者很关心fork之后立刻执行exec所造成的地址空间浪费,所以引入了vfork系统调用。
vfork有个限制,子进程必须立刻执行_exit或者exec函数。
参考:
[1] 《linux内核设计与实现》
[2] 《linux内核完全注释》 好后悔没有好好读完全注释
[3] http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part3/
[4] http://blog.youkuaiyun.com/freas_1990/article/details/9052239