fork() 與父/子行程關係
一個行程怎麼建立另一個?在類 Unix 系統上,基本答案出奇地古怪:一個行程把自己複製一份。fork() 系統呼叫把執行中的行程拿來,做出一個幾乎一模一樣的副本——同樣的程式碼、同樣的變數值、同樣開著的檔案、同樣執行到的位置。你現在有兩個行程:原本的(父行程)與新的副本(子行程)。這比較不像打造一個新程式,更像細胞分裂:一個變成兩個,兩者都從 fork() 之後的下一行繼續跑。
著名的轉折在於 fork() 會回傳兩次——每個行程各一次——而每個副本正是靠這唯一的回傳值得知自己是誰。在父行程裡,fork() 回傳子行程的 PID(一個正數)。在子行程裡,fork() 回傳 0。(失敗時在父行程回傳 -1,且不會造出子行程。)所以同一行程式碼在兩者都跑,卻走不同分支:「如果回傳值是 0,我是子行程;否則我是父行程,而這個數字是我子行程的 PID。」fork 之後兩個行程彼此獨立——它們有各自的定址空間,所以子行程改的變數不會改到父行程,反之亦然。為了避免瞬間複製可能高達數 GiB 的記憶體,現代核心採用寫入時複製:父與子起初共用同一批標記為唯讀的實體分頁,只有當其中一方真的去寫某一頁時,那一頁才被複製。分裂在邏輯上立即發生,在實體上則按需才發生。
為何重要:fork() 是每棵行程樹生長的根源——除了 init 之外的每個行程都源自一次 fork。它與 exec()(下一個詞)搭配來啟動新程式,與 wait() 搭配來清理子行程。雙重回傳是對新手而言最令人驚訝的一件事,而搞錯它——以為 fork() 之後的程式碼只跑一次——是經典的錯誤。另要注意:fork() 複製整個定址空間,但與執行緒搭配時有尖銳的陷阱(在子行程裡只有呼叫它的那條執行緒存活),這也是 fork 接 exec 之所以是安全日常模式的原因之一。
pid_t pid = fork(); if (pid == 0) { /* 子行程在這裡跑 */ } else if (pid > 0) { /* 父行程在這裡跑,pid 是子行程的 PID */ } else { /* fork 失敗,errno 被設定 */ }。兩個分支的程式碼實體上只存在一份,但在執行期,子行程走第一個分支,父行程走第二個。
fork() 回傳兩次:在子行程裡是 0,在父行程裡是子行程的 PID——這就是各自如何認出自己的方式。
寫入時複製並不表示父與子共用變數。它們只在寫入發生前共用實體分頁,之後各拿到私有副本;邏輯上,它們的記憶體從 fork 那一刻起就是獨立的。