Fork_(系统调用)

在電腦領域中，尤其是 Unix 及類 Unix 系統作業系統中，**fork**（行程複製）是一種建立自身行程副本的操作。它通常是核心實現的一種系統呼叫。Fork 是類 Unix 作業系統上建立行程的一種主要方法，甚至歷史上是唯一方法。

## 概述

在多工作業系統中，行程（執行的程式）需要一種方法來建立新行程，例如執行其他程式。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?
> 
> 

同樣，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 系統呼叫的返回值決定它們執行什麼功能。樣板代碼中的標頭檔等已被省略。

下面是該程式的解析：

呼叫中的第一句是呼叫 fork 系統呼叫來分割執行為兩個行程。fork 的返回值被記錄在類型為 pid\_t 的變數中，其中是 POSIX 類型的行程識別碼（PID）。

在電腦領域，尤其是 Unix 及類 Unix 系統作業系統中，**fork** 是一種建立自身行程副本的操作。它通常是核心實現的一種系統呼叫。Fork 是在類 Unix 作業系統上建立行程的一種主要方法，甚至歷史上曾是唯一方法。

-1 錯誤表示 fork 出錯：沒有新行程被建立。因此要印出一條錯誤訊息。

如果 fork 成功，那麼現在有兩個行程。兩者都從 fork 返回時開始執行 main 函式。為了使行程執行不同的任務，程式必須基於 fork 的返回值決定其作為子行程或父行程執行某個分支。

Fork 操作會為子行程建立一個單獨的定址空間。子行程擁有父行程所有記憶體段的精確副本。在現代的 UNIX 變種中，這遵循出自 SunOS-4.0 的虛擬記憶體模型，根據寫入時複製語意，實體記憶體不需要被實際複製。取而代之的是，兩個行程的虛擬記憶體頁面可能指向實體記憶體中的同一個頁，直至它們寫入該頁時，寫入才會發生。在用 fork 配合 exec 來執行新程式的情況下，此最佳化很重要。通常，子行程在停止程式執行前會執行一小組有利於其他程式的操作，它可能用到少量的其父行程的資料結構。

其他行程 —— 即父行程，會收到 fork 傳來的子行程的行程識別碼，其始終為一個正數。父行程將此識別碼傳遞給 waitpid 系統呼叫來暫停執行，直至子行程登出。當此情況發生後，父行程繼續執行並按 return 語句的含義登出。

## 參見

* fork 炸彈
* Fork–exec
* Exit
* Wait

## 參考資料