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被引入前不支持此系统函数，因为它的内存共享容易出错：同样，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）之隐喻：新进程的每个组件的内存布局都是重新创建的。后来被微软的操作系统采用（1993年）。

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

## 应用程序范例
下列Hello World程序的变种以C语言展示了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();
```
调用中的第一句是调用系统调用来分割执行为两个进程。的返回值被记录在类型为pid_t的变量中，其中是POSIX类型的进程标识符（PID）。

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

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

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

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

## 参见
* fork炸弹
* 
* 
* Wait

## 参考资料