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語句的含義退出。