exit()、_exit() 與正常/非正常終止
每個行程終究會結束。它如何結束、以及它身後留下的那個小數字,是它對世界說的最後一句話。一個行程可以正常結束——它做完了工作、選擇停下,交回一個說「成功」或「這是出了什麼錯」的狀態碼。或者它可以非正常結束——它被殺掉、它當掉了、作業系統把它擊落。想像離開一棟建築:正常終止是走出正門、在登記簿上簽名;非正常終止是因為火警(一個致命信號)響起而被抬出去。
正常終止發生在程式呼叫 exit()、或從 main() 回傳、或呼叫較低階的 _exit() 時。這三者都會結束行程,並向父行程回報一個整數結束狀態(慣例上 0 代表成功,非零代表某種失敗)。exit() 與 _exit() 的差別在於離場路上會跑哪些清理:exit()(以及從 main 回傳)是有禮貌的版本——它沖刷緩衝的輸出、執行用 atexit() 註冊的函式、關閉 stdio 串流,然後結束行程。_exit()(與它的生料表親 _Exit())則立即結束行程,完全不做那些清理。這個區別在一個剛 fork 出來、即將 exec 或已失敗的子行程裡至關重要:那裡你呼叫 _exit(),才不會把父行程緩衝的資料沖刷兩次。非正常終止則是行程並非自己選擇停下:一個致命信號結束了它——一次壞記憶體存取造成的記憶體區段錯誤(SIGSEGV)、管理員下的 SIGKILL、失敗的斷言(SIGABRT)。父行程能分辨這兩者:正常結束的子行程回報一個結束狀態,而被信號殺掉的子行程回報是哪個信號結束了它。
為何重要:結束狀態是一個程式被允許說出、關於它有沒有成功的那一個機器可讀的字,也是 shell、指令稿與父行程拿來做分支判斷的東西(shell 的 $? 存著上一個結束狀態)。在 fork 出的子行程裡把 exec() 與 _exit() 搞混是一個真實又常見的錯誤,會產生重複或亂掉的輸出。而分辨正常結束與信號致死,正是 wait() 的狀態巨集所要做的事——當掉不是一個整齊的結束碼,而是結束碼的缺席。
在 fork 出的子行程裡,exec 失敗後你呼叫 _exit(127),而非 exit(127),這樣子行程才不會去沖刷它從父行程繼承來的 stdio 緩衝區。在 main 裡,return 0;(或 exit(0);)回報成功並執行完整清理。反之,一次失控的指標寫入會以 SIGSEGV 結束行程——一次根本沒有結束碼的非正常終止。
exit() 先清理再結束;_exit() 立即結束;致命信號則非正常終止、沒有結束碼。
結束狀態是一個位元組(0-255)。從 main() 回傳負數或超過 255 的值會環繞進那個範圍,所以請把狀態碼設計在 0-255 之內,並把 0 保留給成功。