一個常見的疑問#

寫一個簡單 container 跑 sleep:

FROM alpine
CMD ["sleep", "3600"]

進入 container 後試著對自己發 SIGTERM:

kill -TERM 1

很多人會發現:什麼都沒發生。但同樣的 sleep 在 host 上 kill -TERM <pid> 立刻會死。為什麼 container 內的 PID 1 殺不掉?

答案是 kernel 對 PID 1 有特別保護機制。

Kernel 的 PID 1 保護規則#

Linux kernel 對 PID 1(含 PID namespace 內的 PID 1)有特殊處理:

  • 來自同一個 PID namespace 的 signal:只有 PID 1 自己安裝過 handler 的 signal 才會被傳遞;裝預設行為(SIG_DFL)的 signal 會被 kernel 直接丟掉
  • 來自父 PID namespace(host)的 signal:規則略有不同,SIGKILL、SIGSTOP 永遠送達

這個保護是為了避免 init process 被自己的子嗣意外殺死,導致整個系統(或 container)崩潰。

重點是「PID 1 自己沒裝 handler」的 signal 才會被丟掉。如果 PID 1 用 sigaction(2) 裝了 SIGTERM handler,那 SIGTERM 就能正常送達並執行 handler。

三種情境的行為#

把 signal 來源、目標 PID 1 是否有 handler 兩個維度組合,行為清楚很多。

情境 A:container 內對 PID 1 發 signal,PID 1 沒裝 handler#

# 假設 container 內 PID 1 是 sleep
kill -TERM 1
  • Kernel 看到 PID 1 對 SIGTERM 仍是 default disposition
  • 直接丟掉
  • sleep 繼續跑,沒事發生

情境 B:container 內對 PID 1 發 signal,PID 1 有裝 handler#

# 假設 container 內 PID 1 是某個有 SIGTERM handler 的 server
kill -TERM 1
  • Kernel 看到 PID 1 對 SIGTERM 有 custom handler
  • Signal 正常送達
  • Handler 執行

情境 C:host 對 container PID 1 發 SIGKILL#

# host 上
kill -KILL <host-pid-of-container-pid1>
  • SIGKILL 永遠不被攔截
  • Container PID 1 立即終止
  • Container 隨之結束

為什麼 docker stop 對 sleep 有效#

承上面的例子:

FROM alpine
CMD ["sleep", "3600"]

docker stop 會:

  • 先送 SIGTERM 給 container PID 1
  • sleep 沒裝 SIGTERM handler → 被 kernel 丟掉
  • 等待 10 秒沒結束
  • 升級成 SIGKILL → sleep 立即被殺
  • Container 結束

整個 stop 花了 10 秒。如果 sleep 是真實的應用,這 10 秒就是浪費掉的「graceful 視窗」。這就是為什麼一定要把 PID 1 換成會處理 SIGTERM 的程式。

為什麼 tini 能解決問題#

docker run --init 或在 image 裡裝 tini 後:

  • PID 1 = tini
  • Tini 在啟動時用 sigaction 裝了所有重要 signal 的 handler
  • SIGTERM 送來,tini 的 handler 被觸發
  • Tini 把 SIGTERM 轉發給 child(應用)
  • 應用是 PID 2,沒有保護,正常處理 signal

於是整個 stop 流程在應用層感覺不到 PID 1 保護的存在。

實際驗證的做法#

可以寫個小實驗自己跑(不在這裡假造輸出):

  • Container A:CMD ["sleep", "3600"],docker exec 進去 kill -TERM 1,觀察 sleep 是否被殺
  • Container B:寫一個 C 程式,sigaction 裝 SIGTERM handler,包成 image,重做上一步,觀察行為差異
  • Container C:用 docker run --init 跑同樣的 sleep,再進去 kill -TERM 1(注意此時 sleep 是 PID 2)

第三個實驗會發現 kill -TERM 1 還是不會殺 tini(tini 收到後忽略),但 kill -TERM 2 立刻能讓 sleep 結束,因為 PID 2 沒有保護。

跨 PID namespace 的 signal 規則#

從 host(父 namespace)對 container PID 1 發 signal 時,規則不太一樣:

  • SIGKILL 與 SIGSTOP 永遠送達,PID 1 沒得選
  • 其他 signal 仍受 PID 1 保護規則影響:必須裝 handler 才會送達
  • 這也是為什麼 docker stop 內部還是要靠應用配合:daemon 從 host 發 SIGTERM,應用沒裝 handler 一樣會被丟掉

總結幾條規則#

  • PID 1 的 default-disposition signal 會被 kernel 丟掉(同 namespace 內),只有 SIGKILL、SIGSTOP 例外
  • 想讓 PID 1 對 SIGTERM 有反應,必須裝 handler
  • 把應用直接當 PID 1 + 沒裝 handler = docker stop 一定走到 SIGKILL
  • 解法:用 tini / dumb-init / docker run --init,或讓應用自己處理 signal

延伸閱讀#

  • man 7 pid_namespaces(搜尋 “PID 1”)
  • Linux kernel source:kernel/signal.csig_task_ignoredsig_kernel_only
  • Tini README 的 FAQ:Why is kill -TERM 1 ignored?