chundev
日期:2026-03-30 環境:Ubuntu 24.04 / Docker Compose
伺服器登入時看到 4 zombie processes,用 ps aux | grep -w Z 找出全是容器內 Chromium 殘留。Zombie 的本質是「child 死了但 parent 沒收屍」,在 Docker 容器中特別常見,因為容器的 PID 1 通常不是 init system,不會自動 reap children。解法是加 --init flag 或用 tini。
SSH 登入伺服器時,系統資訊顯示:
=> There are 4 zombie processes.
需要判斷這些 zombie 是否有害、來源是什麼、要不要處理。
# 精確篩 STAT 欄(第 8 欄)為 Z 開頭的程序
ps aux | awk '$8 ~ /^Z/'
ps aux 的 STAT 欄位會顯示程序狀態,Z 就是 zombie。用 awk 指定第 8 欄比 grep Z 更精確,避免誤中其他欄位中碰巧包含 Z 的程序。結果:
root 3279 0.0 0.0 0 0 ? Z 08:05 0:00 [chrome_crashpad] <defunct>
root 3281 0.0 0.0 0 0 ? Z 08:05 0:00 [chrome_crashpad] <defunct>
root 3284 0.0 0.0 0 0 ? Z 08:05 0:00 [chromium] <defunct>
root 3285 0.0 0.0 0 0 ? Z 08:05 0:00 [chromium] <defunct>
注意 CPU 和記憶體都是 0 — zombie 不消耗任何資源。
ps -o pid,ppid,stat,comm -p 3279,3281,3284,3285
PID PPID STAT COMMAND
3279 2077 Z chrome_crashpad
3281 2077 Z chrome_crashpad
3284 2077 Z chromium
3285 2077 Z chromium
4 個 zombie 的 parent 都是 PID 2077。查看 parent:
ps aux | grep 2077
root 2077 0.4 0.8 1126196 69584 ? Ssl 08:05 0:01 node /usr/local/bin/pm2-runtime ecosystem.config.cjs
是 Docker 容器裡的 PM2。Chromium 是應用內的 Puppeteer/Playwright 啟動後沒被正確回收。
✅ 不需要立即處理。 4 個 zombie 不佔資源,容器重啟時自動清除。
| 狀態碼 | 名稱 | 說明 |
|---|---|---|
R |
Running | 正在執行或在 run queue 中等待 |
S |
Sleeping | 可中斷的等待(等 I/O、timer 等) |
D |
Disk Sleep | 不可中斷的等待(通常是 I/O),kill -9 也殺不掉 |
T |
Stopped | 被 SIGSTOP 或 debugger 暫停 |
Z |
Zombie | 已終止,但 parent 未呼叫 wait() 回收 |
X |
Dead | 最終狀態,已被回收(ps 中看不到) |
Child 程序結束
↓
Kernel 呼叫 do_exit()
↓
設定狀態為 EXIT_ZOMBIE
保留 PID、exit status、resource usage 在 process table
↓
等待 Parent 呼叫 wait() / waitpid() 讀取 exit status
↓
Kernel 釋放 process table entry → 狀態變為 EXIT_DEAD → 消失
關鍵: Kernel 保留 zombie 是有目的的 — 讓 parent 有機會讀取 child 的結束狀態。這是 POSIX 規範的行為,不是 bug。
| 面向 | Zombie | Orphan |
|---|---|---|
| 定義 | Child 已死,Parent 未 wait() |
Parent 已死,Child 還在跑 |
| 程序狀態 | Z(已終止) |
R/S/D(仍在執行) |
| 資源消耗 | 幾乎為零(僅佔 process table entry) | 正常消耗 CPU/記憶體 |
| 處理方式 | Parent wait() 或 kill parent |
Kernel 自動 re-parent 到 init(1) |
| 危害 | 大量累積可耗盡 PID 空間 | 通常無害 |
Orphan 被 init 收養後,終止時 init 會自動 wait() — 所以 orphan 不會變成 zombie。但如果 PID 1 不是正統 init(例如 Docker 容器),就會出問題。
傳統 Linux 系統中,PID 1 是 init(systemd/SysVinit),它有兩個重要職責:
wait() 清除已終止的 childrenDocker 容器的 PID 1 是你的應用程式(Node.js、Python 等),它不會做這兩件事。
容器 PID 1 (Node.js / pm2-runtime)
├── Chromium (Puppeteer 啟動)
│ ├── chrome_crashpad (crash reporter)
│ └── chromium (renderer process)
│
[Puppeteer 完成工作,kill 主 Chromium process]
[chrome_crashpad 和 renderer 變成 orphan]
[Kernel re-parent 到 PID 1 (pm2-runtime)]
[它們結束後變成 zombie]
[pm2-runtime 不會呼叫 wait() → zombie 永久留存]
這正是本次診斷看到的情況 — PM2 作為 PID 1,不會 reap Chromium 的子程序。
--init 或 tini方法 1:Docker --init flag(最簡單)
docker run --init my-app
Docker 1.13+ 內建 tini,--init 會自動注入一個輕量 init process 作為 PID 1。
方法 2:Docker Compose
services:
app:
image: my-app
init: true # 等同於 --init
方法 3:Dockerfile 中安裝 tini
FROM node:20-alpine
RUN apk add --no-cache tini
ENTRYPOINT ["tini", "--"]
CMD ["node", "app.js"]
SIGCHLD handler,定期 waitpid(-1, ...) 清除所有 zombieSIGTERM/SIGINT 並轉發給 child(解決 docker stop 要等 10 秒 timeout 的問題)整個 binary 只有幾十 KB,幾乎零開銷。
# 列出所有 zombie
ps aux | awk '$8 ~ /^Z/'
# 顯示 zombie 的 PID、PPID、指令
ps -A -ostat,pid,ppid,comm | grep -e '^[zZ]'
# 計算 zombie 數量
ps aux | awk '$8 ~ /^Z/' | wc -l
# 找到 zombie 的 parent
ps -o ppid= -p <zombie_pid>
# 對 parent 發 SIGCHLD 要求 reap(有時有效)
kill -SIGCHLD <parent_pid>
# 如果 SIGCHLD 無效,kill parent(zombie 被 init 收養後自動清除)
kill <parent_pid>
⚠️ 你無法直接 kill 一個 zombie。 它已經死了,沒有程序可以接收信號。唯一的方式是讓 parent reap 它,或 kill parent 讓 init 接手。
docker run --init 或 Compose init: true 是最簡單的解法 — 一行設定,零成本防範wait()/waitpid() 完整文件,包含 zombie 機制說明