bigywater.tech
Docker · 89

信号转发:docker kill --signal 与程序的信号处理

入口脚本里到底该写 exec 还是 trap?为什么 docker stop 总要等满超时?这篇用同一个会打印信号的 Python 应用,对比不 exec、exec、两种 trap 写法和 --init,并看 HUP 这类非 TERM 信号怎么到达应用。

5.2 秒 / 137
入口脚本不 exec 时 docker stop -t 5 的用时和退出码
只 wait 一次 = 143
trap 转发信号后,脚本因第一次 wait 被信号打断而提前退出
HUP 到不了
只转发 TERM / INT 的 trap 脚本,收不到 docker kill -s HUP

exec 与 trap

实测 · 本机执行输出
  A 不用 exec(sh 启动 python):docker stop -t 5 用时 5.2 秒;Status=exited ExitCode=137;日志:entrypoint 启动,PID 1 | app 启动,PID 7
  B exec python:docker stop -t 5 用时 0.7 秒;Status=exited ExitCode=0;日志:entrypoint 启动,PID 1 | app 启动,PID 1 | 收到信号 SIGTERM | 清理完成,退出
  C1 trap 转发 TERM,只 wait 一次:docker stop -t 5 用时 0.1 秒;Status=exited ExitCode=143;日志:entrypoint 启动,PID 1 | app 启动,PID 7 | entrypoint 结束,第一次 wait 返回 143 | 收到信号 SIGTERM
  C2 trap 转发 TERM,被信号打断后再 wait 一次:docker stop -t 5 用时 0.6 秒;Status=exited ExitCode=0;日志:entrypoint 启动,PID 1 | app 启动,PID 7 | 收到信号 SIGTERM | 清理完成,退出 | entrypoint 结束,python 退出码 0

入口脚本最后一行写 exec 应用,应用取代 sh 成为 PID 1,收到 SIGTERM 直接处理;前提是应用自己处理信号(PID 1 没有默认动作,见 c6)。要保留脚本时用 trap 转发:但 wait 被信号打断时立刻返回 128+信号编号,必须再 wait 一次才能拿到子进程的真正退出码并等它清理完;只写一次 wait 的脚本会比应用先退出,应用被容器终止时直接杀掉(C1)。

--init

实测 · 本机执行输出
  A + --init(sh 脚本不 exec):docker stop -t 5 用时 0.1 秒;Status=exited ExitCode=143;日志:entrypoint 启动,PID 7 | app 启动,PID 8
  B + --init(exec python):docker stop -t 5 用时 0.6 秒;Status=exited ExitCode=0;日志:entrypoint 启动,PID 7 | app 启动,PID 7 | 收到信号 SIGTERM | 清理完成,退出

--init 让 tini 当 PID 1,它把信号转给它的子进程(这里是 sh);sh 没装 trap,TERM 的默认动作就是退出,孙子进程 python 没有机会清理。--init 的价值在收尸和 PID 1 的特殊信号语义(c31、c32),不能代替入口脚本去转发。

其它信号

实测 · 本机执行输出
  A 不用 exec,发 HUP:docker kill -s HUP 用时 1.1 秒;Status=running ExitCode=0;日志:entrypoint 启动,PID 1 | app 启动,PID 7
  B exec python,发 HUP:docker kill -s HUP 用时 1.1 秒;Status=running ExitCode=0;日志:entrypoint 启动,PID 1 | app 启动,PID 1 | 收到信号 SIGHUP
  C2 trap 只转发 TERM/INT,发 HUP:docker kill -s HUP 用时 1.0 秒;Status=running ExitCode=0;日志:entrypoint 启动,PID 1 | app 启动,PID 7

热加载常用的 HUP:exec 形式直达;trap 脚本要把 HUP 也写进 trap 列表才会转发。

分步演示

信号转发
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Docker Desktop(Engine 29.8.2,容器跑在 Docker Desktop 自带的 Linux 虚拟机里,内核与宿主机的 macOS 不同);镜像为 linux/arm64;需要看虚拟机内部时用特权容器 + nsenter;计时用 time / date +%s%N,数字每次运行会有小幅抖动。应用是 python:3.12-alpine 里的小脚本(收到 TERM / INT 后睡 0.5 秒再退出,收到 HUP 只打印);入口脚本通过 --entrypoint 切换;docker stop 的用时用 Python time.time 测;每种情形运行一次;实验后已删除容器和镜像
    • 没有验证:dumb-init、s6 等其它 init 的转发行为、进程组信号(kill -- -PGID)、docker compose 的 stop_signal、应用是 Go / Java 时的信号处理差别、trap 脚本里多个子进程的处理
    京ICP备2026067502号-1京公网安备11010502063487号