GNU timeout:先发 TERM,超时后再用 KILL 兜底

121 次浏览6 条回复

有些命令收到 TERM 后会做清理,但也可能一直不退出。GNU timeout 的 --kill-after 可以先给它一个正常收尾窗口,超过窗口再发 KILL。

环境:Linux、Bash 4.4+、GNU coreutils 9.x。保存为 worker.sh:

#!/usr/bin/env bash
set -Eeuo pipefail

trap 'printf "got TERM, but keep running\n" >&2' TERM
while :; do
  sleep 1
done

验证命令:

chmod +x worker.sh

set +e
timeout --signal=TERM --kill-after=1s 2s ./worker.sh
status=$?
set -e

printf 'exit=%s\n' "$status"
test "$status" -eq 137

这里运行 2 秒后先发 TERM,脚本故意不退出;再等 1 秒,timeout 用 KILL 结束它,所以状态是 137。实际任务如果能在 TERM 后正常收尾,就不会走到强制终止。这个参数是 GNU coreutils 的用法,macOS 自带环境不能直接照搬。

这个 137 判断很适合这段 demo。放进调用脚本时最好也把 124 留出来:触发超时后,如果程序收到 TERM 并正常收尾,timeout 默认仍返回 124;只有走到后面的 KILL 才是 137。如果加了 --preserve-status,前一种情况又会保留子进程状态,监控脚本里最好固定一种约定。

再补一个排日志的小点:137 只能说明最终是 SIGKILL,单看退出码不能证明一定是 timeout 的兜底触发。GNU coreutils 这版可以加 --verbose:

timeout --verbose --signal=TERM --kill-after=1s 2s ./worker.sh

它会把自己发送 TERM、KILL 的动作写到标准错误。CI 里把这段标准错误一起留存,遇到外部 kill -9 或 OOM 时会更容易分清来源。

还有个容易漏掉的场景是被调命令会再拉起子进程。同样在 GNU coreutils 9.x,timeout 默认会管理这组进程;但用了 --foreground(比如交互程序需要直接读终端)后,子进程不会跟着被超时。跑批脚本如果必须开这个选项,最好让被调程序自己转发 TERM 并 wait 子进程,不然父进程退出后,后台任务可能还在继续跑。

Theodore.KaiLv1#1

这个 137 判断很适合这段 demo。放进调用脚本时最好也把 124 留出来:触发超时后,如果程序收到 TERM 并正常收尾,timeout 默认仍返回 124;只有走到后面的 KILL 才是 137。如果加了 --preserve-status,前一种情况又会保留子进程状态,监控脚本里最好固定一种约定。

再补一个返回码协议的坑:124 也不是 timeout 独占的。目标程序如果在超时前自己 exit 124,调用方看到的仍然是 124:

set +e
timeout 2s bash -c 'exit 124'
printf 'exit=%s\n' "$?"

所以需要严格区分“任务自己失败”和“确实超时”时,最好在任务的退出码约定里保留 124;否则只看 $? 无法消除这个歧义。

还有一点:一旦真的走到 KILL,目标进程里的 EXIT/TERM trap 或 finally 都不会再执行。必须删除的临时文件,最好让外层调用脚本持有并清理。沿用楼主的 Linux、Bash 4.4+、GNU coreutils 9.x 环境,可以这样包一层:

tmp=$(mktemp -d)
trap 'rm -rf -- "$tmp"' EXIT

set +e
timeout --signal=TERM --kill-after=1s 2s ./worker.sh
status=$?
set -e

printf 'exit=%s\n' "$status"

这样即使 worker 被强制结束,清理逻辑仍由没有被 timeout 管理的外层 shell 执行。

fernLv1#5

还有一点:一旦真的走到 KILL,目标进程里的 EXIT/TERM trap 或 finally 都不会再执行。必须删除的临时文件,最好让外层调用脚本持有并清理。沿用楼主的 Linux、Bash 4.4+、GNU coreutils 9.x 环境,可以这样包一层:

tmp=$(mktemp -d)
trap 'rm -rf -- "$tmp"' EXIT

set +e
timeout --signal=TERM --kill-after=1s 2s ./worker.sh
status=$?
set -e

printf 'exit=%s\n' "$status"

这样即使 worker 被强制结束,清理逻辑仍由没有被 timeout 管理的外层 shell 执行。

外层持有清理责任这个思路很稳,不过 EXIT 也不是持久化保证:外层 shell 自己收到 KILL,或者机器重启,trap 一样来不及跑。对会反复执行的任务,可以再加一层“启动时回收”:临时目录使用固定前缀和本次运行的唯一后缀,只清理确认没有活跃锁且超过保留时间的旧目录。这样即时清理靠 trap,异常中断留下的目录由下一次运行兜底。