如何正确地使用vfork():简析vfork()与fork()的不同
今天看到知乎上有人问了一个 由于不恰当的使用vfork()而导致的一个奇怪现象,底下的回答非常精彩。趁此机会我也仔细了解了一下vfork()的特性。
其实对vfork()最完备、权威的表述莫过于man手册里面的解释了。
简单的说,vfork()跟fork()类似,都是创建一个子进程,这两个函数的的返回值也具有相同的含义。但是vfork()创建的子进程基本上只能做一件事,那就是立即调用_exit()函数或者exec函数族成员, 调用任何其它函数(包括exit())、修改任何数据(除了保存vfork()返回值的那个变量)、执行任何其它语句(包括return)都是不应该的。此外,调用vfork()之后, 父进程会一直阻塞,直到子进程调用_exit()终止,或者调用exec函数族成员。
关于如何正确使用vfork(),上面这一段就是全部了。但是为什么vfork()会这样呢? 其实vfork()和fork()之间只有两点不同:
- fork()会复制父进程的页表,而vfork()不会复制,直接让子进程共用父进程的页表;
- fork()使用了写时复制技术,而vfork()没有,它任何时候都不会复制父进程地址空间。
即使算上vfork()会阻塞父进程而fork()不会,也只有三点不同,没有更多不同了。所以vfork()产生的子进程跟父进程完全共同使用同一个地址空间,甚至共享同一个函数堆栈!也就是子进程中对任何数据变量的修改,不管是局部的还是全局的,都会影响到父进程。而任何一个函数调用都会修改栈空间,这就是为什么vfork()的子进程不能随便调用别的函数。
但需要注意的是,由于vfork()毕竟还是产生一个新的进程,所以子进程拥有自己的进程描述符,拥有自己的寄存器,最重要的是,拥有自己的打开文件列表!
注意拥有自己的打开文件列表非常重要,因为如果子进程只是简单地共用父进程的打开文件列表,那么当子进程调用_exit()退出时,_exit()内部会自动关闭当前进程打开的所有文件描述符,也就是打开文件列表里面的文件,这将导致父进程恢复执行时,无法访问到自己之前已经打开过的文件,包括标准输入、标准输出和标准错误输出。所幸的是这永远不会发生,子进程会复制父进程的打开文件列表,并增加文件引用计数。
那为什么vfork()子进程中可以调用_exit(),却不可以调用exit(),也不可以直接return呢?
exit()是对_exit()的封装,它自己在调用_exit()前会做很多清理工作,其中包括刷新并关闭当前进程使用的流缓冲(比如stdio.h里面的printf等),由于vfork()的子进程完全共享了父进程地址空间,子进程里面的流也是共享的父进程的流,所以子进程里面是不能做这些事的。
直接return就更不行了,子进程return以后,会从当前函数的外部调用点后面继续执行,这后面子进程可能将会执行很多语句,结果就没法预料了。
最后看一段程序,如果理解了这段程序,那么对vfork()的理解基本上就没什么大问题了:
#include <stdio.h> #include <unistd.h> void stack1() { vfork(); } void stack2() { _exit(0); } int main() { stack1(); printf("%d goes 1\n", getpid()); stack2(); printf("%d goes 2\n", getpid()); return 0; }
如果父进程pid为1000,子进程pid为1001,那么输出将会是:
1001 goes 1
1000 goes 2