Fork (系统调用)

在计算机领域中,尤其是Unix及类Unix系统操作系统中,fork(进程复制)是一种创建自身行程副本的操作。它通常是内核实现的一种系统调用。Fork是类Unix操作系统上创建进程的一种主要方法,甚至历史上是唯一方法。

概述
在多任务操作系统中,行程(运行的程序)需要一种方法来创建新进程,例如运行其他程序。Fork及其变种在类Unix系统中通常是这样做的唯一方式。如果进程需要启动另一个程序的可执行文件,它需要先Fork来创建一个自身的副本。然后由该副本即“子进程”调用系统调用,用其他程序覆盖自身:停止执行自己之前的程序并执行其他程序。

Fork操作会为子进程创建一个单独的定址空間。子进程拥有父进程所有内存段的精确副本。在现代的UNIX变种中,这遵循出自SunOS-4.0的虚拟内存模型,根据寫入時複製语义,物理内存不需要被实际复制。取而代之的是,两个进程的可能指向物理内存中的同一个页,直到它们写入该页时,写入才会发生。在用fork配合exec来执行新程序的情况下,此优化很重要。通常来说,子进程在停止程序运行前会执行一小组有利于其他程序的操作,它可能用到少量的其父进程的数据结构。

当一个进程调用fork时,它被认为是父进程,新创建的进程是它的孩子(子进程)。在fork之后,两个进程还运行着相同的程序,都像是调用了该系统调用一般恢复执行。然后它们可以检查调用的返回值确定其状态:是父进程还是子进程,以及据此行事。

fork系统调用在第一个版本的Unix就已存在,它借用于更早的分時系統。Fork是标准化的POSIX的一部分。

通信
子进程从父进程的文件描述符副本开始。对于进程间通信,父进程通常会创建一个或多个管道,在fork进程之后,进程关闭它们不需要的管道端。

变种
Vfork
Vfork是与fork具有相同调用约定和很多相同语义的一个变种,但只能在有限的情况下使用它。它起源于Unix的3BSD版本,这是首个支持虚拟内存的Unix版本。它已按POSIX标准化,这使得vfork能具有与fork完全相同的行为。但这已在2004年的版本中被标为过时,并在后续版本中被posix_spawn()取代(其通常通过vfork实现)。

在发出一个vfork系统调用时,父进程被暂停,直至子进程完成执行或被新的可执行映像取代(通过系统调用之“”家族中的一项)。子进程借用父进程的MMU设置和内存页面,在父进程与子进程之间共享,不进行复制,尤其是没有寫入時複製语义;}}同样,Linux对vfork的手册页面强烈不鼓励它的使用:。不过,POSIX规范中注明它是“为内核操作设计”,尤其是用于运行在受限硬件和实时系统上的操作系统。

虽然4.4BSD的实现中摆脱了vfork的实现,使vfork做到与fork相同的行为,但它在NetBSD操作系统中因性能原因而恢复。

一些嵌入式操作系统(例如uClinux)只实现vfork,因为它们需要在由于缺乏内存管理单元(MMU)而不可能实现写时复制的设备上操作。

Rfork
Plan 9操作系统由Unix的设计者创造,包括fork,但也有一个名为“rfork”的变种,它允许父进程与子进程之间资源的细粒度共享,包括地址空间(除了调用栈段,那是每个进程独有的)、环境变量和文件系统命名空间;这使它成为了创建进程和其中的线程的一个统一接口。 在FreeBSD和IRIX中采用了来自Plan 9的rfork,后者将其更名为“sproc”。

Clone
“clone”(克隆)是Linux内核中的一个系统调用,它创建一个可以与其父共享“执行上下文”的子进程。类似FreeBSD的rfork和IRIX的sproc,Linux的clone受到了Plan 9的rfork启发,并可用于实现线程(尽管应用程序的程序员通常使用更高级的接口,例如pthreads,实现在clone的顶层)。因为导致太多开销,出自Plan 9和IRIX的“separate stacks”(单独堆栈)特性已被省略(据Linus Torvalds)。

其他操作系统中的Fork
在VMS操作系统(1977年)的原始设计中,新进程根据当前一些特定地址进行复制来创建被认为是有风险的。当前进程中的错误状态可能被复制给子进程。因此在这里使用了进程“产卵”(spawning)之隐喻:新进程的每个组件的内存布局都是重新创建的。后来被微软的操作系统采用(1993年)。

VM/CMS(OpenExtensions)的POSIX兼容组件提供了一个非常有限的fork实现,其中的父进程在子进程执行时被暂停,并且子与父共享同一地址空间。这本质上是一个名为fork的vfork。(注意,这只适用于CMS客户机操作系统,其他VM客户机操作系统如Linux提供标准的fork功能。)

应用程序范例
下列Hello World程序的变种以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;
}
下面是该程序的解析:
pid_t pid = fork();
调用中的第一句是调用系统调用来分割执行为两个进程。的返回值被记录在类型为pid_t的变量中,其中是POSIX类型的进程标识符(PID)。

在计算机领域,尤其是Unix及类Unix系统操作系统中,fork是一种创建自身行程副本的操作。它通常是内核实现的一种系统调用。Fork是在类Unix操作系统上创建进程的一种主要方法,甚至历史上曾是唯一方法。

-1错误表示出错:没有新进程被创建。因此要印出一条错误消息。

如果成功,那么现在有两个进程。两者都从返回时开始执行函数。为了使进程执行不同的任务,程序必须基于的返回值决定其作为子进程或父进程执行某个分支。

else if (pid == 0) {
printf("Hello from the child process!\n");
_exit(EXIT_SUCCESS);
}
Fork操作会为子进程创建一个单独的定址空間。子进程拥有父进程所有内存段的精确副本。在现代的UNIX变种中,这遵循出自SunOS-4.0的虚拟内存模型,根据寫入時複製语义,物理内存不需要被实际复制。取而代之的是,两个进程的可能指向物理内存中的同一个页,直至它们写入该页时,写入才会发生。在用配合来执行新程序的情况下,此优化很重要。通常,子进程在停止程序运行前会执行一小组有利于其他程序的操作,它可能用到少量的其父进程的数据结构。
else {
int status;
(void)waitpid(pid, &status, 0);
}
其他进程——即父进程,会收到传来的子进程的进程标识符,其始终为一个正数。父进程将此标识符传递给 waitpid 系统调用来暂停执行,直至子进程退出。当此情况发生后,父进程继续执行并按return语句的含义退出。

参见

  • fork炸弹

*
*

  • Wait

参考资料

评论 (0)

  • 还没有评论,来抢沙发吧。