timeout退出码的坑:为什么你的脚本判断失灵了
用timeout控制命令超时,最头疼的就是退出码问题。默认情况下,timeout不管命令本身返回什么,超时就给你返回124。这导致脚本里的错误判断逻辑全乱套了。
比如你写了个脚本:
timeout 10s your_script.sh
if [ $? -eq 0 ]; then
echo "成功"
elif [ $? -eq 124 ]; then
echo "超时"
else
echo "失败"
fi
结果your_script.sh返回1(失败),但被timeout捕获后变成了124(超时),逻辑判断就错了。这时候--preserve-status参数就派上用场了。
--preserve-status是什么:保留原始退出码的关键参数
--preserve-status是GNU coreutils提供的timeout参数,作用很简单:让timeout返回被托管命令的退出状态码,而不是默认的124。
默认行为 vs --preserve-status
看个对比示例就明白了:
# 默认行为:超时返回124
timeout 5s sleep 10
echo $? # 输出:124
# 使用--preserve-status:超时返回143(128+SIGTERM)
timeout --preserve-status 5s sleep 10
echo $? # 输出:143
这里143是怎么来的?SIGTERM的信号编号是15,128+15=143。如果命令是正常退出的,那么--preserve-status会原样返回命令的退出码。
实战场景1:脚本错误判断不再误报
这是最常见的使用场景。假设你有个备份脚本,成功返回0,失败返回1,超时需要单独处理:
#!/bin/bash
# backup_with_timeout.sh
# 不使用--preserve-status的问题示例
timeout 3600s /opt/backup.sh
exit_code=$?
if [ $exit_code -eq 124 ]; then
echo "备份超时,可能数据量过大"
send_alert "backup timeout"
elif [ $exit_code -eq 0 ]; then
echo "备份成功"
else
echo "备份失败,退出码: $exit_code"
# 这里其实无法区分是脚本失败还是超时
fi
# 使用--preserve-status的正确姿势
timeout --preserve-status 3600s /opt/backup.sh
exit_code=$?
if [ $exit_code -eq 124 ]; then
echo "备份超时"
send_alert "backup timeout"
elif [ $exit_code -eq 0 ]; then
echo "备份成功"
elif [ $exit_code -eq 1 ]; then
echo "备份失败:磁盘空间不足"
send_alert "disk full"
elif [ $exit_code -eq 143 ]; then
echo "备份被SIGTERM终止"
send_alert "terminated by signal"
else
echo "备份异常,退出码: $exit_code"
fi
这样每种错误都能准确识别,不会误判。
实战场景2:监控脚本退出码采集
在监控系统中,经常需要采集被监控命令的退出码来做告警分级。比如:
#!/bin/bash
# monitor_command.sh
COMMAND=$1
TIMEOUT=$2
# 执行命令并捕获退出码
timeout --preserve-status ${TIMEOUT}s ${COMMAND} 2>&1
exit_code=$?
# 根据退出码做告警分级
case $exit_code in
0)
echo "[INFO] 命令执行成功"
;;
124)
echo "[CRITICAL] 命令超时,可能死锁"
send_dingtalk_alert "CRITICAL" "命令超时: ${COMMAND}"
;;
137)
echo "[WARNING] 命令被强制终止(SIGKILL)"
send_dingtalk_alert "WARNING" "命令被杀: ${COMMAND}"
;;
1)
echo "[ERROR] 命令执行失败"
send_dingtalk_alert "ERROR" "执行失败: ${COMMAND}"
;;
127)
echo "[CRITICAL] 命令不存在"
send_dingtalk_alert "CRITICAL" "命令未找到: ${COMMAND}"
;;
*)
echo "[WARNING] 未知退出码: $exit_code"
send_dingtalk_alert "WARNING" "异常退出码 $exit_code: ${COMMAND}"
;;
esac
exit $exit_code
如果不加--preserve-status,所有失败场景都会被归类为124,监控就失去了精细化能力。
实战场景3:CI/CD流水线中的退出码传递
在CI/CD中,Jenkins/GitLab Runner会根据脚本退出码判断构建是否成功。如果用timeout包装命令,必须保留原退出码:
# .gitlab-ci.yml 示例
test:
script:
- timeout --preserve-status 300s npm test
after_script:
- |
if [ $exit_code -eq 124 ]; then
echo "测试超时,可能存在死循环"
elif [ $exit_code -ne 0 ]; then
echo "测试失败,退出码: $exit_code"
fi
这样CI系统能准确识别是测试本身失败还是超时,避免误报。
退出码速查表:timeout常见返回值
| 退出码 | 含义 | 说明 |
|---|---|---|
| 0 | 命令成功 | 命令在超时前正常退出 |
| 1-125 | 命令失败 | 命令自身返回的错误码(使用--preserve-status时) |
| 124 | 命令超时 | 默认行为,命令被timeout终止 |
| 125 | timeout失败 | timeout命令本身出错 |
| 126 | 命令不可执行 | 权限不足或不是可执行文件 |
| 127 | 命令未找到 | 命令不存在或PATH错误 |
| 128+N | 信号终止 | 128+信号编号(如SIGTERM=15,返回143) |
| 137 | SIGKILL终止 | 128+9(KILL信号不能被捕获) |
注意事项:这些坑要避开
1. 信号编号不是退出码
进程收到信号被终止时,退出码是128+信号编号。比如SIGTERM是15,那么退出码就是143,不是15。很多人会搞混:
# 错误写法
if [ $? -eq 15 ]; then
echo "收到SIGTERM"
fi
# 正确写法
if [ $? -eq 143 ]; then
echo "收到SIGTERM信号被终止"
fi
# 或者用kill -l反查
signal=$(( $? - 128 ))
if [ $signal -eq 15 ]; then
echo "收到SIGTERM"
fi
2. --preserve-status对kill-after的影响
使用-k (--kill-after)参数时,如果命令被SIGKILL强制终止,退出码会是137,不是124:
timeout --preserve-status -k 5s 10s your_command
# 如果命令被SIGKILL杀掉,返回137
# 如果命令正常退出,返回命令自身的退出码
3. 子进程可能捕获信号
有些程序会捕获SIGTERM做清理工作,这种情况下--preserve-status会返回程序自己的退出码,而不是143:
# 假设your_daemon捕获SIGTERM后返回0
timeout --preserve-status 10s your_daemon
echo $? # 输出0,而不是143
总结
timeout --preserve-status的核心价值是保留原始退出码,让脚本能准确判断命令的真实状态。记住这三个典型场景:
- 错误判断:需要区分命令失败和超时两种情况
- 监控采集:精细化识别不同错误类型
- CI/CD:让构建系统正确识别失败原因
用好这个参数,你的脚本就不再会因为退出码问题误判了。
相关阅读
版权声明
本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论