行程與行程控制

fork 接 exec 模式(fork-then-exec)

你的 shell 怎麼執行像 ls 這樣的指令?它不會把自己變成 ls(那會毀掉 shell),而經典 Unix 上也沒有單一的「把這個別的程式當子行程跑」呼叫。它改用一支兩步舞:先 fork() 做出一個自己的子副本,再在那個子行程裡呼叫 exec() 把副本換成真正的程式。父行程(shell)毫髮無傷地存活下來並等待;子行程變成 ls、執行、然後結束。這個 fork 接 exec 模式,正是類 Unix 系統上幾乎每個程式啟動另一個程式的方式。

用淺白步驟走一遍:(1)shell 呼叫 fork()。現在有兩個 shell,父與子。(2)子行程檢查回傳值,看到 0,知道自己是子行程。它可以在這裡做一點只該影響新程式的設定——重導向它的標準輸入/輸出、切換目錄、調整環境。(3)子行程對目標程式呼叫 exec(),比方 execvp("ls", argv);子行程的映像被換掉,它現在正以剛才安排好的設定跑 ls。(4)同時父行程從 fork() 看到一個正數回傳(子行程的 PID),呼叫 wait() 或 waitpid() 暫停,直到子行程結束、並收走它的結束狀態。優雅之處在於這種分工:fork 給你一個可犧牲的全新行程,fork 與 exec 之間那短短一扇窗讓你能安全地客製它,而 exec 把它變成你真正想要的程式。

為何重要:這是 Unix 上啟動程式的標準作法,它也解釋了否則看起來很怪的設計抉擇。為什麼 fork 與 exec 是分開的呼叫,而不是合併成一個「spawn」?正是為了讓你能在造出新行程與載入新程式之間,在子行程裡插入那個設定步驟——重導向某個檔案描述符、放下權限、設環境變數。像 C 函式庫的 system() 與 posix_spawn() 這類較高階的幫手把這個模式包了起來,但底下它們做的就是 fork(或等價物)、接 exec、再 wait。一旦你看懂這個模式,shell、建構工具與伺服器衍生工作者的整套編排就都說得通了。

pid_t pid = fork(); if (pid == 0) { execvp("ls", (char*[]){"ls","-l",NULL}); _exit(127); } else { int status; waitpid(pid, &status, 0); } —— 父行程 fork,子行程 exec 進 ls,父行程等待並回收。這就是執行一個指令的完整一生。

fork 造出子行程,exec 把它變成目標程式,wait 回收它——啟動三部曲。

請把 fork 與 exec 之間的設定保持最少且非同步信號安全;在 exec 換掉映像之前,子行程與父行程共享脆弱的狀態。這也是為什麼有 posix_spawn()——為常見情況提供更安全的合併原語。

又称
fork/execspawning a program啟動新程式的標準作法