Share Notes

chundev

View the Project on GitHub latteouka/share-notes

Linux Zombie Process 完整指南 — 從診斷到 Docker PID 1 問題

日期:2026-03-30 環境:Ubuntu 24.04 / Docker Compose


TL;DR

伺服器登入時看到 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 是否有害、來源是什麼、要不要處理。


診斷過程

Step 1:找出 zombie

# 精確篩 STAT 欄(第 8 欄)為 Z 開頭的程序
ps aux | awk '$8 ~ /^Z/'

ps auxSTAT 欄位會顯示程序狀態,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 不消耗任何資源。

Step 2:找到 parent process

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 啟動後沒被正確回收。

Step 3:判斷是否需要處理

不需要立即處理。 4 個 zombie 不佔資源,容器重啟時自動清除。


核心知識:Linux 程序狀態

程序狀態一覽

狀態碼 名稱 說明
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 中看不到)

Zombie 的生命週期

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 vs Orphan

面向 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 容器),就會出問題。


Docker 容器的 PID 1 問題

為什麼容器特別容易產生 zombie?

傳統 Linux 系統中,PID 1 是 init(systemd/SysVinit),它有兩個重要職責:

  1. 收養 orphan process — 當 parent 死亡,child 被 re-parent 到 PID 1
  2. Reap zombie — 定期呼叫 wait() 清除已終止的 children

Docker 容器的 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"]

tini 做了什麼?

  1. Zombie reaping — 註冊 SIGCHLD handler,定期 waitpid(-1, ...) 清除所有 zombie
  2. Signal forwarding — 監聽 SIGTERM/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 接手。


學到的事


參考資料