进程的创建
fork(),exit(),wait()以及execve()简介
- 系统调用
fork()
允许一进程(父进程)创建一新进程(子进程)。具体做法是,新的子进程几乎是对父进程的翻版:子进程获得父进程的栈,数据段,堆和执行文本段的拷贝。 - 库函数
exit(status)
终止一进程,将进程占用的所有资源(内存,文件描述符等)归还内核,交其再次分配。参数status
为一整型变量,表示进程的退出状态。父进程可使用系统调用wait()
来获取该状态。 - 系统调用
wait(&status)
的目的有二:其一,如果子进程尚未调用exit()
终止,那么wait()
会挂起父进程直至子进程终止;其二,子进程的终止状态通过wait()
的status
参数返回。 - 系统调用
execve(pathname, argv, envp)
加载一个新程序(路径名为pathname
,参数列表为argv
,环境变量列表为envp
)到当前进程的内存。这将丢弃现存的程序文本段,并为新程序重新创建栈,数据段以及堆。
创建新进程:fork()
系统调用fork()
创建一个新进程。
#include <unistd.h>
pid_t fork(void);
完成对fork()
的调用后将存在两个进程,且每个进程都会从fork()
的返回处继续执行。
这两个进程将执行相同的程序文本段,但却各自拥有不同的栈段,数据段以及堆段拷贝。子进程的栈,数据以及栈段开始时是对父进程内存相应各部分的完全复制。执行fork()
之后,每个进程均可修改各自的栈数据,以及堆段中的变量,而不影响另一个进程。
调用fork()
之后,系统将率先执行哪个进程,是无法确定的。
父,子进程间的文件共享
执行fork()
时,子进程会获得父进程所有文件描述符的副本。这些副本的创建方式类似于dup()
。这也意味着父,子进程中对应的描述符均指向相同的打开文件句柄。打开文件句柄包含有当前文件偏移量以及文件状态标志。一个打开文件的这些属性因而在父子进程间实现了共享。如果子进程更新文件偏移量,那么这种改变也会影响到父进程中相应的描述符。
父子进程间共享文件属性的妙用屡见不鲜。例如,假设父子进程同时写入一文件,共享文件偏移量会确保二者不会覆盖彼此的输出内容。不过,这不能阻止父子进程的输出随意混杂在一起。要想规避这一想想,需要进行进程间同步。比如,父进程可以使用系统调用wait()
来暂停运行并等待子进程退出。shell
就是这么做的,只有当执行命令的子进程退出后,shell
才会打印出提示符。
如果不需要这种对文件描述符的共享方式,那么在设计应用程序时,应该在fork()
调用后注意两点:其一,令父,子进程使用不同的文件描述符;七二,各自立即关闭不再使用的描述符。
fork()的内存语义
可以将fork()
认作对父进程程序段,数据段,堆段以及栈段创建拷贝。
- 内核将每一进程的代码段标记为只读,从而使进程无法修改自身代码。这样,父,子进程可共享同一代码段。系统调用
fork()
在为子进程创建代码段时,其所构建的一系列进程级页表项均指向与父进程相同的物理内存页帧。 - 对于父进程数据段,堆段和栈段中的各页,内核采用写时复制的技术来处理。最初,内核做了一些设置,令这些段的页表项指向与父进程相同的物理内存页,并将这些业面自身标记为只读。调用
fork()
之后,内核会捕获所有父进程或子进程针对这些页面的修改企图,并为将要修改的页面创建拷贝。系统将新的页面拷贝分配给遭内核捕获的进程,还会对子进程的相应页表项做适当调整。从这一刻起,父子进程可以分别修改各自的页拷贝,不再相互影响。
控制进程的内存需求
通过将fork()
与wait()
组合使用,可以控制一个进程的内存需求。进程的内存需求量,即进程所使用的虚拟内存页范围,受到多种因素的影响,例如,调用函数,或从函数返回时栈的变化情况,对exec()
的调用,以及因调用malloc()
和free()
而对堆所作的修改。
对于以下程序
pid_t childPid;
int status;
childPid = fork();
if(childPid == -1)
errExit("fork");
if(child == 0)
exit(func(arg));
if(wait(&status) == -1)
errExit("wait");
假设上面程序所示方式调用fork()
和wait()
,且将对函数func()
的调用至于括号之中。由执行程序可知,由于所有可能的变化都发生于子进程,对func()
的调用之前开始,父进程的内存使用量将保持不变。
这一用法的实用性归于如下理由。
- 若已知
func()
导致内存泄漏,或是引发堆内存的过度碎片化,该技术则可以避免这些问题。
系统调用vfork()
vfork()
可以为调用进程创建一个新的子进程。然而,vfork()
是为子进程立即执行exec()
的程序而专门设计的。
#include <unistd.h>
pid_t vfork(void);
//父进程中,创建成功返回子进程的进程id;子进程中,返回0
vfork()
因为如下两个特性而更具有效率,这也是与fork()
的区别所在。
- 无需为子进程复制虚拟内存页或页表。相反,子进程共享父进程的内存,直至其成功执行了
exec()
或者调用_exit()
退出。 - 在子进程调用
exec()
或_exit()
之前,将暂停执行父进程。
这两点另有深意,如果子进程使用父进程的内存,因此子进程对数据段,堆或栈的任何改变将在父进程恢复执行时为其所见。此外,如果子进程在vfork()
与后续的exec()
或_exit()
之间执行了函数返回,这同样会影响到父进程。
在不影响父进程的前提下,子进程能在vfork()
与exec()
之间所做的操作屈指可数。其中包括对打开文件描述符进行操作。因为系统是在内核空间为每个进程维护文件描述符表,且在vfork()
调用期间将复制该表,所以子进程对文件描述符的操作不会影响到父进程。
vfork()
的语义在于执行该调用后,系统将保证子进程先于父进程获得调度以使用CPU。