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 跑時,會出現什麼問題。理解本章的責任清單,是理解那些問題的前提。

延伸閱讀#