简介
在多任务的操作系统中,运行中的进程需要一个方法去创建新的进程。在类unix系统中,fork函数和它的变种是具有代表性的并且唯一的途径。为了让一个进程开始执行一个不同的程序,它首先分裂自己的一个副本。副本叫做子进程,它调用exec系统调用覆盖自己的代码段、数据段等。
fork
fork操作为子进程创建了自己独立的地址空间,子进程完全复制了父进程的内存。现在的类unix操作系统中,借鉴了SunOS-4.0系统的虚拟内存模型,实现了copy-on-write(写时拷贝)技术,不需要对实际的物理内存进行拷贝。因此,父子进程中虚拟内存页会引用相同的物理内存页帧,直到它们中的一个写入内容到这些物理页中,然后才进行实际的拷贝。通常情况下,在执行fork之后紧接着用exec去加载新程序的时候这个优化非常重要。往往子进程在运行自己的程序之前,只执行一些小的行为集合。
在Linux下,fork实现了写时拷贝技术,为了创建子进程独有的任务结构,写时拷贝只带来了复制父进程的页表所造成的时间和内存上的消耗。
当一个进程调用fork函数的时候,它被视为父进程,新创建的进程被视为子进程。fork之后,这两个进程恢复执行好像都调用过fork一样。
如果创建成功,子进程的PID返回到父进程,0返回到子进程。失败的话,-1返回到父进程,代表创建子进程失败,errno中保存了错误码。
vfork
vfork是fork的一个变种,它们使用相同的调用约定和语法,但是只能用在一些限制条件下。它的原型来自Unix 3BSD版本,这个系统是第一个支持虚拟内存的unix系统。它遵循POSIX标准,允许vfork和fork有完全同样的功能,但是在POSIX.1-2004的版本中标记为遗弃的函数,POSIX.1-2008移除了vfork的规格。
调用vfork之后,父进程被挂起,直到子进程完全退出或者调用exec族函数用新程序取代了老程序。子进程与父进程的内存页共享,未进行任何的copy,更没有写时拷贝技术。因此如果子进程修改任何的共享内存页,不会创建任何的新页,这些修改在父进程中是可见的,因为完全不涉及内存页拷贝(增加内存消耗)。使用vfork函数创建子进程时,子进程借用数据结构而不是复制它们,所以仍然比使用写时复制技术的fork函数速度更快。
linux中,vfork是clone(2)一种特别的形式。它创建新进程的时候不拷贝父进程的页表,它可以用于对效率敏感的应用程序,这类程序在子进程被创建后立即调用exec(3)族函数。
fork和vfork对比
- vfork不同于fork的是调用vfork的线程会被挂起直到子进程终止(正常情况下调用_exit(2), 不正常情况下,产生一个错误信号),或者调用execve(2)族函数。直到这两种情况之前,子进程共享所有父进程的内存,包括堆栈。在vfork调用过程中子进程不允许返回(阻塞父进程)或者是调用exit()函数(直接调用exit()函数会影响父进程所中的退出处理程序并且清空父进程的stdio缓冲区),但是可以调用_exit()函数。
- 和fork一样,vfork创建的子进程继承了调用进程的属性拷贝(例如,文件描述符、信号处理和当前的工作路径),唯一不同的就是对虚拟地址空间不拷贝。
- 一些嵌入式操作系统,像ucLinux移除了fork只保留了vfork,因为它们需要在缺少MMU而不能实现写时复制的设备上运行,所以fork没有MMU是不能工作,而vfork可以。
- 调用vfork等同于调用clone(2)时配置如下三个标志:
CLONE_VM | CLONE_VFORK | SIGCHLD。
调用fork等同于调用clone(2)时只指定一个标志:
SIGCHLD
vfork的由来
在以前没有写时拷贝技术,fork要求完全拷贝父进程的数据空间,经常是没有必要的,因为通常会立即调用exec(3)。因此为了更高效,BSD引入了vfork系统调用,它不会拷贝父进程的地址空间,而是借用父进程的内存和线程控制,直到调用execve(3)或退出。在子进程使用父进程的资源时,父进程被挂起。使用vfork时要特别注意:例如,不要修改父进程中已经进入寄存器的变量。
关于vfork的争议:
vfork和fork有同样的功能,但是调用vfork创建子进程时,下面的情况都会导致无法预期的情况出现:
- 修改除了用于存储返回值的pid_t类型的变量以外的任何数据;
- 直接从vfork的调用处返回;
- 在成功调用_exit()或者任何exec族函数中的任何函数之前调用其他的函数。
vfork另一问题是在与动态链接库的交互过程中可能会在多线程的程序中引起死锁。
子进程不应该没有目的的去修改内存,因为这些修改在子进程退出或者执行新的程序之后父进程都能看到。信号处理可能会有问题:如果子进程进行信号处理时修改了内存,这些改变对于父进程来说,可能会导致不一致的进程状态(例如,内存的修改对父进程来说可见,但是打开的文件描述符的状态却不可见)。
vfork为什么依然存在?
从某种意义上说vfork的存在是架构上的瑕疵,4.2BSD的手册如此描述:在适当的系统共享机制实现的时候,这个系统调用会被移除。应用不应该依赖vfork的内存共用,因为它的功能基本等同于fork()。然而,即使现代的内存管理硬件已经减少了fork与vfork的性能差距,但因为一些不同的原因在linux和其他类似的系统中仍然保留vfork():
- 一些对性能挑剔的应用程序需要vfork所带来的微弱的性能优势。
- vfork可以在没有MMU的系统上使用,fork的实现必须有MMU的支持。
- 在很多系统上内存是有限的,vfork()避免了执行新程序时临时占用内存(查阅proc(5)中/proc/sys/vm/overcommit_memory的描述) 。(这个功能在大的父进程想要通过子进程运行一个小的帮助者程序时特别有利)。相比之下,在这种情况下,使用fork需要占用与父进程同样大小的内存(如果绝对的超量占用内存有效)或者超量使用内存,这些情况可能带来out-of-memory (OOM) killer终止某个进程的风险。
vfork被posix_spawn取代
由于vfork存在很多问题,所以它的替代品posix_spawn(3)](http://www.man7.org/linux/man-pages/man3/posix_spawn.3.html) 出现了,POSIX.1-2004的版本中标记为遗弃的函数,在POSIX.1-2008 版本中移除了vfork的标准,POSIX标准标注了posix_spawn(3) 函数实现了同样的功能,这个函数的功能等同于fork(2)+exec(3),它的设计被用于缺少MMU的系统上。
示例
应用中的fork:
下面的程序示范了fork函数在C应用程序中是如何使用的。程序运行时分裂为两个进程,父子进程通过fork的返回值可以判断:
int main(void)
{
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);
}
else if (pid == 0) {
printf("Hello from the child process!\n");
_exit(EXIT_SUCCESS);
}
else {
int status;
(void)waitpid(pid, &status, 0);
}
return EXIT_SUCCESS;
}
该程序中fork的返回值保存在pid_t类型的变量中,pid_t是POSIX类型中专门用来表示进程ID的类型。
pid为-1表示创建进程失败。如果创建成功的话,会变成两个进程,它们都在fork调用的地方继续执行,如果PID为0,则是子进程,但是0是一个无效的进程ID,它只是用来标识子进程。子进程打印了自己的消息,然后退出。(由于技术原因,子进程必须调用_exit()函数而不是C标准的exit()函数)。
父进程中会返回子进程的PID,它总是一个正数。父进程把这个PID值作为waitpid的参数,执行waitpid系统调用,然后挂起直到子进程退出。子进程退出后,父继承恢复执行,退出意味着子进程返回了自己的退出状态
参会资料:
https://en.wikipedia.org/wiki/Fork_(system_call)
http://www.man7.org/linux/man-pages/man2/fork.2.html
http://www.man7.org/linux/man-pages/man2/vfork.2.html
http://pubs.opengroup.org/onlinepubs/009695399/functions/vfork.html