Fork (系统调用)

概述

在多工作業系統中，行程（執行的程式）需要一種方法來建立新行程，例如執行其他程式。Fork及其變種在類Unix系統中通常是這樣做的唯一方式。如果行程需要啟動另一個程式的可執行檔，它需要先Fork來建立一個自身的副本。然後由該副本即「子行程」呼叫exec系統呼叫，用其他程式覆蓋自身：停止執行自己之前的程式並執行其他程式。
Fork操作會為子行程建立一個單獨的定址空間。子行程擁有父行程所有記憶體段的精確副本。在現代的UNIX變種中，這遵循出自SunOS-4.0的虛擬記憶體模型，根據寫入時複製語意，實體記憶體不需要被實際複製。取而代之的是，兩個行程的虛擬記憶體頁面可能指向實體記憶體中的同一個頁，直到它們寫入該頁時，寫入才會發生。在用fork配合exec來執行新程式的情況下，此最佳化很重要。通常來說，子行程在停止程式執行前會執行一小組有利於其他程式的操作，它可能用到少量的其父行程的資料結構。
當一個行程呼叫fork時，它被認為是父行程，新建立的行程是它的孩子（子行程）。在fork之後，兩個行程還執行著相同的程式，都像是呼叫了該系統呼叫一般恢復執行。然後它們可以檢查呼叫的返回值確定其狀態：是父行程還是子行程，以及據此行事。
fork系統呼叫在第一個版本的Unix就已存在，它借用於更早的GENIE 分時系統。Fork是標準化的POSIX的一部分。

通訊

子行程從父行程的檔案描述符副本開始。對於行程間通訊，父行程通常會建立一個或多個管道，在fork行程之後，行程關閉它們不需要的管道端。

Vfork

Vfork是與fork具有相同呼叫約定和很多相同語意的一個變種，但只能在有限的情況下使用它。它起源於Unix的3BSD版本，這是首個支援虛擬記憶體的Unix版本。它已按POSIX標準化，這使得vfork能具有與fork完全相同的行為。但這已在2004年的版本中被標為過時，並在後續版本中被posix_spawn()取代（其通常通過vfork實現）。
在發出一個vfork系統呼叫時，父行程被暫停，直至子行程完成執行或被新的可執行映像取代（通過系統呼叫之「exec」家族中的一項）。子行程借用父行程的MMU設定和記憶體頁面，在父行程與子行程之間共享，不進行複製，尤其是沒有寫入時複製語意；因此，如果子行程在任何共享頁面中進行修改，不會建立新的頁面，並且修改的頁面對父行程同樣可見。因為沒有頁面複製（消耗額外的記憶體），此技術在純複製環境中使用exec時較普通fork更最佳化。在POSIX中，除非是將立即呼叫exec家族（及其他幾個操作）的函式，其他任何目的會導致未定義行為。使用vfork時，子行程借用而非複製資料結構，所以vfork仍比使用寫時複製語意的fork更快。
System V在System VR4被引入前不支援此系統函式，因為它的記憶體共享容易出錯：
Vfork does not copy page tables so it is faster than the System V fork implementation. But the child process executes in the same physical address space as the parent process (until an exec or exit) and can thus overwrite the parent's data and stack. A dangerous situation could arise if a programmer uses vfork incorrectly, so the onus for calling vfork lies with the programmer. The difference between the System V approach and the BSD approach is philosophical: Should the kernel hide idiosyncrasies of its implementation from users, or should it allow sophisticated users the opportunity to take advantage of the implementation to do a logical function more efficiently?
——Maurice J. Bach
同樣，Linux對vfork的手冊頁面強烈不鼓勵它的使用：
It is rather unfortunate that Linux revived this specter from the past. The BSD man page states: "This system call will be eliminated when proper system sharing mechanisms are  implemented. Users should not depend on the memory sharing semantics of vfork() as it will, in that case, be made synonymous to fork(2)."
使用vfork的其他問題包括死結 ，它可能發生在多執行緒程式中，由於與動態連結互動。 作為vfork介面的替代品，POSIX引入了posix_spawn函式家族，它結合了fork和exec的動作。這些函式可以實現為fork的程式庫常式，就像Linux那樣，或者為了更好的效能實現為vfork ，就像Solaris那樣。不過，POSIX規範中註明它是「為內核操作設計」，尤其是用於執行在受限硬體和實時系統上的作業系統。
雖然4.4BSD的實現中擺脫了vfork的實現，使vfork做到與fork相同的行為，它在NetBSD作業系統中因效能原因而恢復。
一些嵌入式作業系統（例如uClinux）省略fork並只實現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系統呼叫的返回值決定它們執行什麼功能。樣板代碼中的表頭檔等已被省略。
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();
呼叫中的第一句是呼叫fork系統呼叫來分割執行為兩個行程。fork的返回值被記錄在類型為pid_t的變數中，其中是POSIX類型的行程識別元（PID）。
在電腦領域，尤其是Unix及類Unix系統作業系統中，fork是一種建立自身行程副本的操作。它通常是內核實現的一種系統呼叫。Fork是在類Unix作業系統上建立行程的一種主要方法，甚至歷史上曾是唯一方法。
-1錯誤表示fork出錯：沒有新行程被建立。因此要印出一條錯誤訊息。
如果fork成功，那麼現在有兩個行程。兩者都從fork返回時開始執行main函式。為了使行程執行不同的任務，程式必須基於fork的返回值決定其作為子行程或父行程執行某個分支。
   else if (pid == 0) {
      printf("Hello from the child process!\n");
      _exit(EXIT_SUCCESS);
   }
Fork操作會為子行程建立一個單獨的定址空間。子行程擁有父行程所有記憶體段的精確副本。在現代的UNIX變種中，這遵循出自SunOS-4.0的虛擬記憶體模型，根據寫入時複製語意，實體記憶體不需要被實際複製。取而代之的是，兩個行程的虛擬記憶體頁面可能指向實體記憶體中的同一個頁，直至它們寫入該頁時，寫入才會發生。在用fork配合exec來執行新程式的情況下，此最佳化很重要。通常，子行程在停止程式執行前會執行一小組有利於其他程式的操作，它可能用到少量的其父行程的資料結構。
   else {
      int status;
      (void)waitpid(pid, &status, 0);
   }
其他行程——即父行程，會收到fork傳來的子行程的行程識別元，其始終為一個正數。父行程將此識別元傳遞給 waitpid 系統呼叫來暫停執行，直至子行程退出。當此情況發生後，父行程繼續執行並按return語句的含義退出。