一個常被忽略的問題#

在 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