進程呼叫 exit() 後並不會立即消失,而是留下一個「殭屍進程」資料結構,佔據進程表項目,儲存退出碼、CPU 時間、頁錯誤次數等資訊,等待父進程呼叫 wait() 或 waitpid() 收集。若父進程疏忽未收集,殭屍將長期佔用 PID,嚴重時導致無法建立新進程。本文深入解析殭屍生命週期、示範 C 程式製造殭屍,並說明正確清理姿勢。
SIGCHLD;核心將子進程標記為 Z(Zombie),保留最小資料結構等待父進程。kill -9 對殭屍無效,因為它已非執行實體;唯有父進程 wait 或父進程結束(由 init 繼承收集)才能徹底釋放。/* zombie.c */
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
} else if (pid == 0) { /* 子進程:立即結束 */
printf("Child %d exiting\n", getpid());
exit(0);
} else { /* 父進程:睡眠 60 秒不收集 */
printf("Parent %d sleeping, child %d becomes zombie\n", getpid(), pid);
sleep(60);
wait(NULL); /* 最後收集 */
printf("Parent reaped child\n");
}
return 0;
}
$ gcc zombie.c -o zombie
$ ./zombie &
[1] 1217
$ ps -ax | grep -E 'zombie|Z'
1217 pts/0 S 0:00 ./zombie
1218 pts/0 Z 0:00 [zombie] <-- 狀態 Z 即殭屍
1578 pts/0 R 0:00 ps -ax
/* 在父進程適當位置呼叫 */
while (waitpid(-1, NULL, WNOHANG) > 0) { /* 清理所有已結束子進程 */ }
wait():阻塞直到任一子進程結束。waitpid(pid, &status, WNOHANG):非阻塞檢查特定 PID;WNOHANG 讓呼叫立即返回,適合在事件迴圈中定期清理。signal(SIGCHLD, SIG_IGN); /* 讓核心自動回收,適用於不需要知道退出碼的簡單服務 */
Linux 核心見 SIG_IGN 會自動釋放子進程資源,無殭屍產生。
if (fork() == 0) { /* 第一層子進程 */
if (fork() == 0) { /* 第二層孫進程:真正工作 */
do_work();
exit(0);
}
exit(0); /* 第一層子進程立即結束 */
}
waitpid(first_child, NULL, 0); /* 父進程等待第一層,孤兒孫進程由 init 收養 */
| 現象 | 原因 | 解決 |
|---|---|---|
大量 <defunct> 進程 | 父進程未呼叫 wait | 修正父進程代碼加入 waitpid 迴圈 |
| 無法建立新進程(PID 用盡) | 殭屍佔滿 PID 空間 | 重啟父進程或機器;長期修正代碼 |
| kill -9 無效 | 殭屍非執行實體 | 只能等待父進程 wait 或父進程結束 |
ps -eo pid,ppid,stat,cmd | grep Z:列出所有殭屍及其父進程 PID。top / htop:Tasks 列會顯示 zombie 計數。cat /proc/PID/status | grep State:確認單一進程狀態。最佳實踐:所有建立子進程的程式(特別是長期運行的服務),務必在適當位置設置SIGCHLD處理器並呼叫waitpid(-1, &status, WNOHANG),或直接signal(SIGCHLD, SIG_IGN)。這是 Unix/Linux 系統程式設計的基本衛生習慣。