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设置和内存页面，在父进程与子进程之间共享，不进行复制，尤其是没有写入时复制语义；因此，如果子进程在任何共享页面中进行修改，不会创建新的页面，并且修改的页面对父进程同样可见。因为没有页面复制（消耗额外的内存），此技术在纯复制环境中使用exec时较普通fork更优化。在POSIX中，除非是将立即调用exec家族（及其他几个操作）的函数，其他任何目的会导致未定义行为。使用vfork时，子进程借用而非复制数据结构，所以vfork仍比使用写时复制语义的fork更快。

System V在System VR4被引入前不支持此系统函数，因为它的内存共享容易出错：Vfork 不复制页表，因此它比 System V 的 fork 实现更快。但子进程在与父进程相同的物理地址空间中执行（直到执行 exec 或 exit），因此可能会覆盖父进程的数据和栈。如果程序员错误地使用 vfork，可能会出现危险情况，因此调用 vfork 的责任在于程序员。System V 方法与 BSD 方法之间的区别是哲学上的：内核应该隐藏其实现的特殊性，不让用户知道，还是应该允许精通技术的用户利用这些实现来更高效地完成逻辑功能？同样，Linux对vfork的手册页面强烈不鼓励它的使用：使用vfork的其他问题包括死锁，它可能发生在多线程程序中，由于与动态链接交互。作为vfork接口的替代品，POSIX引入了posix_spawn函数家族，它结合了fork和exec的动作。这些函数可以实现为fork的程序库例程，就像Linux那样，或者就像Solaris那样为了更好的性能实现为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）之隐喻：新进程的每个组件的内存布局都是重新创建的。spawn后来被微软的操作系统采用（1993年）。

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

## 应用程序范例

下列Hello World程序的变种以C语言展示了fork系统调用的机理。该程序fork为两个进程，每个都基于fork系统调用的返回值决定它们执行什么功能。样板代码中的头文件等已被省略。

```c
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;
}
```

下面是该程序的解析：

```c
   pid_t pid = fork();
```

调用中的第一句是调用fork系统调用来分割执行为两个进程。fork的返回值被记录在类型为pid_t的变量中，其中是POSIX类型的进程标识符（PID）。

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

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

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

```c
   else if (pid == 0) {
      printf("Hello from the child process!\n");
      _exit(EXIT_SUCCESS);
   }
```

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

```c
   else {
      int status;
      (void)waitpid(pid, &status, 0);
   }
```

其他进程——即父进程，会收到fork传来的子进程的进程标识符，其始终为一个正数。父进程将此标识符传递给 waitpid 系统调用来暂停执行，直至子进程退出。当此情况发生后，父进程继续执行并按return语句的含义退出。