一個常被忽略的問題#
在 container 內跑 ps -ef,會看到 PID 1 是應用本身(或某個 init),但它的 PPID 通常是 0。這個 0 看起來很神祕:誰是它的爸爸?
答案是:在 container 的 PID namespace 裡,PID 1 確實沒有 parent(PPID = 0)。但從 host 的角度,它有一個非常具體的 parent,那就是負責拉起這個 container 的 runtime 元件。
從 Host 看 Container PID 1#
Container 內的 PID 1,在 host 上是另一個 PID(通常是個比較大的數字)。它的 host 端 parent 取決於使用的 runtime stack。
- Docker(傳統架構):containerd-shim → container 內 PID 1
- Docker(現代架構):dockerd → containerd → containerd-shim-runc-v2 → container 內 PID 1
- Kubernetes + containerd:kubelet → containerd → containerd-shim → container 內 PID 1
- Kubernetes + CRI-O:kubelet → crio → conmon → container 內 PID 1
- Podman:conmon → container 內 PID 1
真正當 parent 的,幾乎都是「shim」這一類常駐小程式(containerd-shim、conmon)。它們存在的目的之一就是:當 container runtime 本身(containerd、crio)重啟時,shim 仍能繼續持有 container PID 1,不讓它變孤兒。
為什麼需要 Shim#
直覺上會以為 dockerd 直接 fork 出 container process 最簡單,但實務上會出問題。
- 若 dockerd 是 parent,dockerd 升級重啟時,所有 container 都會被 reparent 到 init
- Reparent 之後 dockerd 拿不到 container 的 exit status
- 無法 attach、stream stdout/stderr 到原本的用戶端
Shim 解決這些問題:
- Shim 是 container PID 1 的真正 parent,holds the wait(2)
- Shim 持有 stdio pipe,container runtime 可以隨時連回來
- Container runtime 重啟,shim 不受影響,container 繼續活著
觀察方式#
從 host 觀察 container PID 1 的 parent,是判斷 runtime 行為的好方法。
# 找出某個 container 的 PID(host 視角)
docker inspect --format '{{.State.Pid}}' my-container
# 看它的 parent
ps -o pid,ppid,cmd -p <host-pid>
ps -o pid,ppid,cmd -p $(ps -o ppid= -p <host-pid>)
# 從 PID 出發畫到 root
pstree -s -p <host-pid>典型輸出會是這樣的鏈:
- systemd(1) → containerd(xxx) → containerd-shim-runc-v2(yyy) → my-app(zzz)
而 my-app 在 container 內就是 PID 1。
PID Namespace 的視角差異#
同一個 process,在不同 namespace 看到的 PID 不同。這由 PID namespace 機制提供。
- Host namespace 看:PID = 12345,PPID = 12300(containerd-shim)
- Container namespace 看:PID = 1,PPID = 0
cat /proc/<host-pid>/status會看到NSpid欄位列出該 process 在每一層 namespace 中的 PID。
跟 Init Process 的關係#
傳統觀念裡 PID 1 是 init,PID 1 的 parent 是 kernel。在 container 中:
- Container 內的 PID 1 角色相當於該 namespace 的 init
- 但它從 host 看不是真正的 init,只是某個一般 process
- 它的 host 端 parent 是 shim
- 這影響了它對 zombie reap、signal handling 的責任(後續章節展開)
為什麼這件事值得單獨拉一章#
理解 container PID 1 的 parent 是誰,可以解釋很多現象:
- 為什麼重啟 dockerd 不會殺掉 container(因為 parent 是 shim)
- 為什麼 docker stop 能精準把 SIGTERM 送到 container PID 1(shim 直接 kill)
- 為什麼 container 內的孤兒會被 reparent 到 container PID 1,而不是 host 的 init
- 為什麼選錯 PID 1 程式會讓整個 container 行為怪異
延伸閱讀#
- containerd 文件:Runtime v2 / shim 設計
- conmon 專案 README
- man 7 pid_namespaces