兩種「不正常」的 Process 狀態#
Process 結束時的正常流程是:child 呼叫 exit、parent 呼叫 wait(2) 把它收乾淨。但只要這個流程在某一步出錯,就會留下兩種典型異常。
- 殭屍程序(Zombie Process):child 已 exit,但 parent 還沒 wait
- 孤兒程序(Orphan Process):parent 先死了,child 還在跑
兩者都是 container 場景的高頻問題,因為 container 裡的 PID 1 經常不是合格的 init process。
殭屍程序(Zombie Process)#
Zombie 是一個已經終止、但 task_struct 還沒被釋放的 process。它的記憶體、fd、資料段都已經回收,唯一還在的是「exit status」和那條 task_struct entry,等 parent 來 reap。
- 在 ps 中狀態為 Z,CMD 通常顯示
<defunct> - 不消耗 CPU 與記憶體
- 但會佔用 PID,PID 用完整個系統就無法再 fork
ps -ef | grep defunct
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /^Z/ { print }'Zombie 無法被 SIGKILL 殺掉。它已經死了,要殺的是它的 parent,或讓 parent 去 wait 它。
為什麼會產生 Zombie#
最常見原因:parent 沒寫好 wait(2) 邏輯。
- 不關心 child 結束,從不呼叫 wait
- 收到 SIGCHLD 但沒處理
- Parent 自己被卡住(deadlock、infinite loop)來不及 wait
正確做法之一是顯式忽略 SIGCHLD,讓 kernel 自動 reap:
signal(SIGCHLD, SIG_IGN);或者裝一個 SIGCHLD handler 在裡面 waitpid(-1, &st, WNOHANG) 直到沒有 child 為止。
孤兒程序(Orphan Process)#
Orphan 是 parent 先死、child 還活著的情況。Child 此時 PPID 指向的 process 已經不存在,kernel 會把它 reparent(重新指定父親)。
- 傳統行為:reparent 給 PID 1(init process)
- 孤兒本身仍然正常執行,不是錯誤狀態
- Init process 負責在它結束後 reap,所以不會變 zombie
Reparent 之後,PPID 欄位會變成 1(或當前的 subreaper PID)。透過
ps -o pid,ppid可以看到。
Reparent 行為與 Subreaper#
Linux 3.4 起新增了 PR_SET_CHILD_SUBREAPER。一個 process 可以宣告自己為 subreaper,那麼它子樹下的孤兒就會被 reparent 到它,而不是一路飛到 PID 1。
- systemd user session 用這個機制
- containerd-shim 也用這個機制管理 container 內的孤兒
- 一般應用不會用,但理解它有助於 debug 為什麼孤兒不見得 reparent 到 PID 1
#include <sys/prctl.h>
prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0);Container 場景的特殊性#
Container 內 PID 1 不是傳統 init,常常就是應用本身(python、node、java)。這帶來兩個風險:
- Zombie 累積:應用沒寫 SIGCHLD handler,fork 出的 helper 一個個變 zombie
- Orphan 失去歸宿:應用 PID 1 不會主動 reap,繼承過來的孤兒結束後也變 zombie
長時間跑下來,container 內 PID 表會被 zombie 塞滿,新 fork 失敗,整個服務開始異常。
解法:用 tini、dumb-init 等迷你 init,或
docker run --init,把 PID 1 換成會 reap 的程式。
一張對照表#
| Zombie | Orphan | |
|---|---|---|
| 狀態 | 已 exit,未被 reap | 還在跑,parent 已死 |
| 是否異常 | 是(要 reap) | 否(kernel 會 reparent) |
| 解法 | parent 呼叫 wait | kernel 自動處理 |
| 在 container 的問題 | PID 1 不 reap → 累積 | 重新 reparent 到 PID 1,仍要 reap |
延伸閱讀#
- man 2 wait
- man 2 prctl
- man 7 signal
- Container 場景見後續章節
06-irresponsible-pid1