殭屍行程(zombie process)
這名字聽起來很戲劇化,但殭屍是個安靜、看似無害的東西——直到它們堆積起來。殭屍行程是一個已經跑完並終止、但父行程尚未用 wait() 收走其結束狀態的行程。這個行程在任何有意義的層面上都已經死了:它的記憶體沒了、不跑任何程式碼、不用任何 CPU。剩下的只是核心行程表裡一塊小小的墓碑——PID 與結束狀態——留著好讓父行程仍能查出子行程如何結束。它死了,但還沒被埋。
這裡是精確的機制。當子行程終止時,核心不能把它完全刪掉,因為父行程也許仍想呼叫 wait() 讀「我的子行程成功了嗎?」。所以核心保留那筆最小紀錄,並把子行程標記為殭屍(在 ps 裡顯示為狀態 Z,或「defunct」)。父行程一呼叫 wait() 或 waitpid(),核心就交出狀態並抹去紀錄——殭屍被收割、消失。因此殭屍只存在於「子行程終止」與「父行程收割它」之間那扇窗裡。若父行程及時收割,殭屍只是一瞬間的事,你甚至很少會看到一個。它們只在父行程不斷建立子行程卻從不等待時才成為問題:每個結束的子行程都賴著當殭屍,而因為每個都仍佔著一個 PID 表位置,一個有缺陷、長時間執行的父行程可以慢慢把系統的 PID 耗盡。
為何重要:殭屍是 wait() 缺失的經典症狀,本身並非危險——你無法用信號殺死殭屍(它已經死了;kill -9 毫無作用)。真正的修法是讓父行程收割它的子行程,或安排讓它們被自動收割。有一條俐落的出路:若父行程自己死了,它的殭屍子行程會被重新指派給 init(PID 1),由 init 收割——所以殭屍的另一條出路,是它那個失職的父行程結束。在 ps 裡認出一排 <defunct> 項目是「一個忘了 wait 的父行程」,是一項核心的除錯技能。
一個父行程在迴圈裡呼叫 fork() 衍生工作者,卻從不呼叫 wait()。在 ps 裡你看到像「1888 pts/0 Z+ 0:00 [worker] <defunct>」這樣的行。每個結束的工作者都是一個佔著 PID 位置的殭屍。kill -9 1888 毫無作用;解法是讓父行程 wait()(或讓父行程結束,把它們交給 init)。
一個 <defunct> 項目是等著被收割的已終止子行程——你殺不了已經死的東西。
殭屍不用 CPU、幾乎不用記憶體;它的代價是一個 PID 表位置,而非資源。解法絕不是 kill -9(它已經死了)——而是讓父行程收割,或讓父行程結束好讓 init 收割。