0

timeout --preserve-status保留原退出码用法:3个实战场景彻底搞懂退出状态传递

2026.07.09 | youres | 36次围观

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辅助作者原创,未经许可,转载请保留原文链接。

发表评论