一個常見的疑問#
寫一個簡單 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.c的sig_task_ignored、sig_kernel_only - Tini README 的 FAQ:Why is
kill -TERM 1ignored?