关于准备知识:
每个进程都有以下属性:
1 地址空间
每个进程都有自己的进程地址空间,格式大概是这个样子:
栈(Stack)以帧为单位,当程序调用函数(假如该函数名为fun01)时,stack会向下增长一帧,这个帧会存储该函数的参数、局部变量以及返回地址,计算机将控制权交给fun01,fun01处于激活状态,这时 Global Data 和 该帧中的局部变量共同构成了context也就是环境上下文。当函数又进一步调用另一个函数的时候,一个新的帧会继续增加到栈的下方,控制权转移到新的函数中。当激活函数返回的时候,会从栈中弹出(pop,读取并从栈中删除)该帧,并根据帧中记录的返回地址,将控制权交给返回地址所指向的指令。
2 进程元数据
进程元数据可以用来区分进程,了解进程状态信息
每一个进程都有PID(进程id),PPID(进程的父进程id),PGID(进程组id)等,这些信息描述了进程的各种信息,但他们并不保存在进程的内存空间中。内核会为每个进程在内核自己的空间中分配一个变量(task_struct结构体)以保存上述信息。内核可以通过查看自己空间中的各个进程的附加信息就能知道进程的概况,而不用进入到进程自身的空间 (就好像我们可以通过门牌就可以知道房间的主人是谁一样,而不用打开房门)。每个进程的附加信息中有位置专门用于保存接收到的信号。
正题:如何创建进程?
一般情况下:
1 当一个程序调用fork的时候,实际上就是将本进程的内存空间,包括text, global data, heap和stack,又复制出来一个,构成一个新的进程。
子进程的栈、数据以及栈段开始时是父进程内存相应各部分的完全拷贝,因此它们互不影响。从性能方面考虑,父 进程到子进程的数据拷贝并不是创建时就拷贝了的,而是采用了写时拷贝(copy-on -write)技术来处理。
2 同时在内核中为改进程创建新的附加信息 (比如新的PID,而PPID为原进程的PID)。
3 然后,程序调用exec的时候,进程清空自身内存空间的text, global data, heap和stack,并根据新的程序文件重建text, global data, heap和stack (此时heap和stack大小都为0),并开始运行。
扩展:
子进程在fork出来的时候,使用了写时复制(COW,Copy-On-Write)方式获得父进程的数据空间、 堆和栈副本,这其中也包括文件描述符。刚刚fork成功时,父子进程中相同的文件描述符指向系统文件表中的同一项(这也意味着他们共享同一文件偏移量)。这其中当然也包含父进程创建的socket。
接着,一般我们会调用exec执行另一个程序,此时会用全新的程序替换子进程的正文,数据,堆和栈等。此时保存文件描述符的变量当然也不存在了,我们就无法关闭无用的文件描述符了。所以通常我们会fork子进程后在子进程中直接执行close关掉无用的文件描述符,然后再执行exec。
但是在复杂系统中,有时我们fork子进程时已经不知道打开了多少个文件描述符(包括socket句柄等),这此时进行逐一清理确实有很大难度。我们期望的是能在fork子进程前打开某个文件句柄时就指定好:“这个句柄我在fork子进程后执行exec时就关闭”。其实时有这样的方法的:即所谓 的 close-on-exec 文件描述标志(File Descriptors Flag)
我们要对文件描述符,文件描述符标志,文件状态标识做一下区分:
文件描述符 File Descriptors
文件描述符是一个标示,非负整数,类似于windows里的句柄,
文件描述标志 File Descriptors Flag(目前就只有一个close-on-exec):
它仅仅是一个标志,当进程fork一个子进程的时候,在子进程中调用了exec函数时就用到了这个标志。意义是执行exec前是否要关闭这个文件描述符。要把文件描述符标志和文件状态标志区分开来。
文件状态标志 File Status Flag:
在系统内核维护的系统打开文件表中,每一个系统文件表项都有一个关于write、read等的标志
参考:
本文参考:
http://www.cnblogs.com/stemon/p/5242547.html
http://blog.csdn.net/ljxfblog/article/details/41680115
http://www.cnblogs.com/vamei/archive/2012/10/09/2715388.html