僵屍進程是指子進程已終止,但父進程未調用 wait()/waitpid() 回收其退出狀態,導致進程描述符(PID、退出碼、資源統計等)長期滯留在內核進程表中。大量僵屍進程會耗盡 PID 資源,導致系統無法創建新進程。
graph TD
A[父進程 fork 子進程] --> B{子進程先結束?}
B -->|是| C[父進程未調用 wait]
C --> D[子進程變為僵屍態 Z]
D --> E[父進程調用 wait 回收]
E --> F[僵屍消失]
B -->|否| G[父進程先結束]
G --> H[子進程被 init 進程收養]
H --> I[init 定期 wait 回收]
I --> F
#include#include #include #include int main() { pid_t pid = fork(); if (pid == 0) { // 子進程 printf("Child PID=%d running\n", getpid()); sleep(2); printf("Child exiting\n"); exit(0); } else if (pid > 0) { // 父進程 printf("Parent PID=%d, child=%d\n", getpid(), pid); sleep(10); // 模擬忙碌,未調用 wait printf("Parent exiting\n"); } return 0; }
編譯運行:gcc zombie.c -o zombie && ./zombie &
查看:ps -eo pid,ppid,stat,cmd | grep -E 'Z|defunct' 可見 Z 狀態進程。
// 子進程睡眠 10s,父進程睡眠 2s 後退出 // 父進程結束後,子進程 PPID 變為 1 (init),init 會自動回收
pid_t pid = fork();
if (pid > 0) {
int status;
waitpid(pid, &status, 0); // 阻塞等待子進程結束
// WIFEXITED(status) 判斷正常退出,WEXITSTATUS(status) 獲取退出碼
}
缺點:父進程被掛起,無法並發處理其他任務。
#include#include void sigchld_handler(int sig) { int saved_errno = errno; while (waitpid(-1, NULL, WNOHANG) > 0) {} // 循環回收所有已終止子進程 errno = saved_errno; } int main() { struct sigaction sa = {0}; sa.sa_handler = sigchld_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // 不因子進程停止產生信號 sigaction(SIGCHLD, &sa, NULL); // 繼續主業務邏輯... for (int i = 0; i < 5; i++) { if (fork() == 0) { sleep(i+1); exit(0); } } sleep(20); // 觀察無僵屍 return 0; }
優點:父進程不阻塞,異步回收,健壯處理多子進程同時退出。
if (fork() == 0) { // 一級子進程
if (fork() == 0) { // 二級子進程(真正的工作進程)
// 執行實際任務
exit(0);
}
exit(0); // 一級子進程立即退出
}
waitpid(pid, NULL, 0); // 父進程只需等待一級子進程
原理:二級子進程成為「孤兒進程」,被 init 收養,退出時由 init 回收,父進程無需關心。
| 方案 | 適用場景 | 複雜度 | 父進程阻塞 |
|---|---|---|---|
| 同步 wait | 簡單任務、啟動型腳本 | 低 | 是 |
| SIGCHLD 信號 | 高併發服務器、長期運行進程 | 中 | 否 |
| 雙重 Fork | 守護進程啟動、後台任務分離 | 中 | 極短 |
工程實踐中,**方案二(信號處理)最通用**,建議封裝為通用庫函數,所有需要創建子進程的模組統一調用。