PID 1 不只是「第一個 process」#
在 Linux 上,PID 1 有特殊地位。它是 kernel 啟動後第一個 user space process,也是整棵 process tree 的根。kernel 對它有特別保護、特別預期,使用者也預設它做某些事情。
- 它是所有孤兒程序(Orphan Process)的最終 parent
- 它要負責 reap 系統內所有 reparent 過來的子孫
- 它要處理或轉發系統 signal
- 它一旦死掉,kernel 會 panic
這些責任統稱為「init responsibility」。傳統 init system(sysvinit、OpenRC、systemd)都是為了履行這些責任而生。
責任一:Reap Zombie#
Init process 的核心工作之一是 reap。任何時刻,只要它的子樹有 process 結束,最終都會落到它身上要求 wait。
- 直接子嗣:Init 自己 fork 出來的服務
- 間接後代:原本的 parent 死了被 reparent 過來的孤兒
如果 init 不 reap,這些 process 會卡在 zombie 狀態。一個合格的 init 必須在收到 SIGCHLD 時迴圈呼叫 waitpid(2):
while (waitpid(-1, NULL, WNOHANG) > 0) {
/* keep reaping until no more dead children */
}注意要用 WNOHANG 在 loop 裡反覆呼叫,因為一次 SIGCHLD 可能對應多個 child 同時結束,signal 會被合併。
責任二:處理 Signal#
Init process 需要正確處理 SIGTERM、SIGINT、SIGHUP 等訊號。對 container 而言,這直接影響 graceful shutdown。
- SIGTERM:請求結束,init 應通知子行程逐一收尾
- SIGINT:使用者中斷,行為類似 SIGTERM
- SIGHUP:傳統上是 reload 設定檔
- SIGCHLD:child 狀態改變,觸發 reap
Container runtime(docker、kubelet)發 graceful shutdown 是直接把 SIGTERM 送給 container 內的 PID 1。如果 PID 1 不處理 SIGTERM,整個 stop 流程就會走到逾時、被 SIGKILL 強制砍。
責任三:轉發 Signal 給子行程#
更嚴格的要求:init 不只要自己處理 SIGTERM,還應該把它轉發給所有子行程。否則使用者按 Ctrl-C,只有 PID 1 收到,子嗣繼續跑。
- 真實 init(systemd)會走 cgroup 級別的 signal 廣播
- 簡易 init(tini、dumb-init)通常用 process group 機制:把 PID 1 也當作 process group leader,kill 整個 group
責任四:成為 Subreaper(PR_SET_CHILD_SUBREAPER)#
PR_SET_CHILD_SUBREAPER 是 Linux 3.4 引入的 prctl 旗標,讓非 PID 1 的 process 也能扮演「子樹的 init」。
- 設定後,自己子樹下的孤兒會 reparent 到自己,而不是一路飛到 PID 1
- 對 container 來說:container 內 PID 1 本身就是 subreaper(因為 PID namespace 邊界自然形成)
- 對使用 systemd user session 或 supervisor pattern 的 process,這個旗標讓它能扮演局部 init
#include <sys/prctl.h>
if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) < 0) {
perror("prctl");
}Container PID 1 不需要特別呼叫 PR_SET_CHILD_SUBREAPER。PID namespace 的設計讓它自動成為該 namespace 內所有孤兒的最終歸宿。
對照:傳統 Init vs Container Init#
| 傳統 init(systemd 等) | Container init(tini 等) | |
|---|---|---|
| 範圍 | 整台機器 | 單一 container |
| Reap | 是 | 是 |
| Signal forwarding | 是(cgroup 廣播) | 是(process group) |
| 服務管理 | 是(unit、dependency) | 否(只有單一應用) |
| Reload | 是 | 通常無 |
Container init 是「精簡版 init」,只保留最關鍵的兩件事:reap 與 signal forwarding。這兩件事剛好是把應用塞進 PID 1 後最容易出事的地方。
為什麼這件事重要#
下一章會討論:當一般應用程式(不具備 init 責任)被當成 PID 1 跑時,會出現什麼問題。理解本章的責任清單,是理解那些問題的前提。
延伸閱讀#
- man 2 prctl(搜尋 PR_SET_CHILD_SUBREAPER)
- systemd 設計文件
- tini 專案 README:https://github.com/krallin/tini ↗