trap 命令详解与参数实测(Ubuntu 22.04)

以 Ubuntu 22.04(bash 5.1.16)实测逐用法讲解 bash 内建 trap:INT/TERM/HUP 捕获与 kill 触发、trap -p/-l 查看、trap - 重置、trap '' 忽略、EXIT/ERR/DEBUG/RETURN 四类事件(临时目录清理模板、出错行号定位、命令计数、函数返回钩子)、信号号与信号名等价、子 shell 继承规则(捕获型重置、忽略型继承)、EXIT trap 与退出码,附 trap 与常见清理方案对比。

Linux命令Linux系统UbuntuUbuntu 22.04命令行Shell2026/10/16

trap 命令详解与参数实测(Ubuntu 22.04)

目标:全面掌握 bash 内建命令 trap 的每一种用法——捕获 SIGINT/SIGTERM/SIGHUP 并用 kill 触发、trap -p/trap -l/无参数查看、trap - 重置、trap '' 忽略、EXIT/ERR/DEBUG/RETURN 四类伪信号事件(mktemp 临时目录清理模板、$LINENO 出错行号定位、简单命令计数、函数返回钩子)、信号号与信号名等价、子 shell 与后台任务的 trap 继承规则(捕获型被重置、忽略型才继承)、EXIT trap 与退出码的交互,以及 SIGKILL 不可捕获的边界,全部命令与回显已在 Ubuntu 22.04(bash 5.1.16)实测通过。

一、环境信息

本文所有命令均在以下环境实测,回显逐字照抄:

  • 操作系统:Ubuntu 22.04.5 LTS(PRETTY_NAME="Ubuntu 22.04.5 LTS")
  • 内核架构:x86_64
  • Shell:GNU bash 5.1.16(1)-release(软件包 bash 5.1-6ubuntu1.1)
  • 执行身份:root
  • 演示脚本目录:/root/demo/trap/(sigdemo.sh、cleanup.sh、job.sh 等由测试脚本动态生成)
  • 信号发送工具:内建 kill(kill -INT/-TERM/-9),前台中断模拟用 coreutils 的 timeout -s INT

二、命令概览

2.1 语法与测试数据

trap 为信号或 shell 事件注册处理动作,是脚本「优雅退出」的唯一内建手段。语法形态(来自实测 help trap 首行):

trap [-lp] [[arg] signal_spec ...]
  • trap '命令' 信号:捕获——收到信号时执行命令,执行完回到原流程(或按命令里的 exit 退出);
  • trap '' 信号:忽略——本 shell 及其子命令都收不到该信号;
  • trap - 信号 或 trap 信号:重置为 shell 启动时的默认处置;
  • trap '命令' EXIT/ERR/DEBUG/RETURN:事件钩子(非真信号),分别对应退出、命令失败、每条简单命令前、函数返回;
  • trap -p/trap:列出当前注册;trap -l:信号名与编号清单。

本文测试数据约定:信号触发统一用脚本内 kill -INT/-TERM $$ 自我发送,或对后台子进程 kill 外部发送;mktemp -d 提供真实临时目录验证清理。每个小节均可独立执行复现。

💡 说明:trap 的两个易混点提前给出实测结论——其一,捕获型 trap 不会传给 ( ) 子 shell(会被重置为 shell 启动时的值),只有 trap '' 忽略型才会继承(3.11);其二,EXIT trap 里的普通命令不会改写脚本的最终退出码,想改必须在 trap 里显式 exit N(3.13)。

2.2 help 全量输出(逐行照抄)

以下为 help trap 的完整实测输出:

$ help trap
trap: trap [-lp] [[arg] signal_spec ...]
    Trap signals and other events.

    Defines and activates handlers to be run when the shell receives signals
    or other conditions.

    ARG is a command to be read and executed when the shell receives the
    signal(s) SIGNAL_SPEC.  If ARG is absent (and a single SIGNAL_SPEC
    is supplied) or `-', each specified signal is reset to its original
    value.  If ARG is the null string each SIGNAL_SPEC is ignored by the
    shell and by the commands it invokes.

    If a SIGNAL_SPEC is EXIT (0) ARG is executed on exit from the shell.  If
    a SIGNAL_SPEC is DEBUG, ARG is executed before every simple command.  If
    a SIGNAL_SPEC is RETURN, ARG is executed each time a shell function or a
    script run by the . or source builtins finishes executing.  A SIGNAL_SPEC
    of ERR means to execute ARG each time a command's failure would cause the
    shell to exit when the -e option is enabled.

    If no arguments are supplied, trap prints the list of commands associated
    with each signal.

    Options:
      -l        print a list of signal names and their corresponding numbers
      -p        display the trap commands associated with each SIGNAL_SPEC

    Each SIGNAL_SPEC is either a signal name in <signal.h> or a signal number.
    Signal names are case insensitive and the SIG prefix is optional.  A
    signal may be sent to the shell with "kill -signal $$".

    Exit Status:
    Returns success unless a SIGSPEC is invalid or an invalid option is given.

关键信息解读:四类事件钩子的语义原文——EXIT 在 shell 退出时、DEBUG 在每条简单命令之前、RETURN 在函数或 . /source 引入的脚本结束时、ERR 在「-e 开启时会导 致退出的失败」上(注意:实测 ERR 在没有 set -e 时同样触发普通命令失败,见 3.6 与 FAQ Q2);kill -signal $$ 是帮助原文给出的标准触发手法(3.1 即按此实测)。

2.3 版本信息与内建归属

trap 是纯 shell 内建命令,版本随 bash 发布:

$ type -a trap
trap is a shell builtin

$ bash --version | head -1
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)

$ dpkg -l bash | tail -1
ii  bash           5.1-6ubuntu1.1 amd64        GNU Bourne Again SHell

⚠️ 注意:系统里没有 /usr/bin/trap 独立程序(type -a 只返回 shell builtin);发信号用的 kill 也是 bash 内建,另有 /usr/bin/kill 外部版本,行为一致。

三、参数逐项实测

3.1 捕获 SIGINT/SIGTERM/SIGHUP 并用 kill 触发

三个最常用的「进程终止类」信号各注册处理动作,脚本内自我发送验证:

cat sigdemo.sh
bash sigdemo.sh; echo 退出码=$?

实测回显:

$ cat sigdemo.sh
#!/bin/bash
trap 'echo "捕获到 SIGINT (Ctrl+C), 不退出"' INT
trap 'echo "捕获到 SIGTERM, 收尾后退出"; exit 143' TERM
trap 'echo "捕获到 SIGHUP"; exit 129' HUP
echo "pid=$$ 就绪"
kill -INT $$
kill -TERM $$
echo "不会执行到这里"

$ bash sigdemo.sh; echo 退出码=$?
pid=27377 就绪
捕获到 SIGINT (Ctrl+C), 不退出
捕获到 SIGTERM, 收尾后退出
退出码=143

💡 要点:INT 的 handler 只打印不退出,脚本继续走到下一条 kill -TERM;TERM 的 handler 执行后 exit 143,最终退出码 143(=128+15,符合「信号终止」惯例)。最后的 echo "不会执行到这里" 没有输出,因为 TERM 分支主动退出了。信号名写 INT/TERM/HUP 即可,SIG 前缀可省(help 原文),3.9 验证了纯数字同样合法。

3.2 trap -p / 无参数查看与 trap - 重置

trap -p 与裸 trap 输出一致,都列出已注册项;trap - SIG 恢复默认:

cat plist.sh
bash plist.sh

实测回显:

$ cat plist.sh
#!/bin/bash
trap 'echo INT-handler' INT
trap 'echo TERM-handler' TERM
echo "--- trap -p ---"
trap -p
echo "--- trap(无参数) ---"
trap
echo "--- trap - INT 重置后 trap -p ---"
trap - INT
trap -p
$ bash plist.sh
--- trap -p ---
trap -- 'echo INT-handler' SIGINT
trap -- 'echo TERM-handler' SIGTERM
--- trap(无参数) ---
trap -- 'echo INT-handler' SIGINT
trap -- 'echo TERM-handler' SIGTERM
--- trap - INT 重置后 trap -p ---
trap -- 'echo TERM-handler' SIGTERM

💡 要点:注册时写的 INT 在 trap 表里显示为规范名 SIGINT;trap - INT 后表里只剩 TERM 一项,再收到 SIGINT 就走默认处置(终止)。排障时 trap -p 是确认「trap 到底设没设上」的第一入口——尤其在父子 shell 场景(3.11)。

3.3 忽略信号:trap '' INT

空字符串动作 = 忽略:信号被丢弃,脚本与其子命令都不感知:

cat ignore.sh
bash ignore.sh; echo 退出码=$?

实测回显:

$ cat ignore.sh
#!/bin/bash
trap '' INT
echo "SIGINT 已被忽略"
kill -INT $$ && echo "kill -INT 发出后脚本仍在运行"
trap - INT
echo "INT 已恢复默认(此后再收到 Ctrl+C 会终止)"
$ bash ignore.sh; echo 退出码=$?
SIGINT 已被忽略
kill -INT 发出后脚本仍在运行
INT 已恢复默认(此后再收到 Ctrl+C 会终止)
退出码=0

💡 要点:kill -INT $$ 发出后脚本照常打印下一行、最终退出码 0——SIGINT 被完全丢弃。trap '' 与 trap 'echo 处理' 的区别:前者连子命令也一起忽略(继承规则见 3.11 的 5.3),后者只改变脚本自身的行为。

3.4 EXIT 清理临时目录模板(正常路径)

mktemp + EXIT trap 是「无论怎么退出都清理」的标准模板:

cat cleanup.sh
bash cleanup.sh; echo 退出码=$?

实测回显:

$ cat cleanup.sh
#!/bin/bash
workdir=$(mktemp -d)
trap 'echo "[EXIT] 清理临时目录 $workdir"; rm -rf "$workdir"' EXIT
echo "工作目录: $workdir"
echo data > "$workdir/data.txt"
echo "正常收尾"
$ bash cleanup.sh; echo 退出码=$?
工作目录: /tmp/tmp.k4rAjUZysx
正常收尾
[EXIT] 清理临时目录 /tmp/tmp.k4rAjUZysx
退出码=0

💡 要点:[EXIT] 行出现在所有业务输出之后——EXIT trap 在脚本退出动作时才执行;变量 $workdir 在注册时是普通展开(trap 定义于 mktemp 之后),因此清理的是真实路径。

3.5 出错路径同样触发 EXIT 清理(set -e)

EXIT 的价值在异常路径:set -e 下任何一步失败退出,清理照样执行:

cat cleanup_err.sh
bash cleanup_err.sh; echo 退出码=$?

实测回显:

$ cat cleanup_err.sh
#!/bin/bash
set -e
workdir=$(mktemp -d)
trap 'echo "[EXIT] 出错也不留垃圾, 清理 $workdir"; rm -rf "$workdir"' EXIT
echo "步骤1 完成"
false
echo "不会执行"
$ bash cleanup_err.sh; echo 退出码=$?
步骤1 完成
[EXIT] 出错也不留垃圾, 清理 /tmp/tmp.f8zOtBwNKl
退出码=1

💡 要点:false 触发 set -e 退出(退出码 1),EXIT trap 依然完整执行了清理。对照 3.4:正常路径与异常路径共用同一份清理逻辑,这正是「trap EXIT 而不是在每个出口手动 rm」的理由。

3.6 trap ERR:打印出错命令与行号($LINENO/$BASH_COMMAND)

ERR 钩子配合 $LINENO/$BASH_COMMAND/$? 能打出精确定位日志:

cat errline.sh
bash errline.sh; echo 退出码=$?

实测回显:

$ cat errline.sh
#!/bin/bash
trap 'echo "[ERR] 第 $LINENO 行: 命令 [$BASH_COMMAND] 失败, 退出码=$?"' ERR
echo "第一行正常"
false
cat /root/demo/trap/不存在的文件.txt
echo "脚本继续运行(没有 set -e 时 ERR 只报告不退出)"
$ bash errline.sh; echo 退出码=$?
第一行正常
[ERR] 第 3 行: 命令 [false] 失败, 退出码=1
cat: /root/demo/trap/不存在的文件.txt: No such file or directory
[ERR] 第 4 行: 命令 [cat /root/demo/trap/不存在的文件.txt] 失败, 退出码=1
脚本继续运行(没有 set -e 时 ERR 只报告不退出)
退出码=0

💡 要点:两次失败各触发一次 ERR,行号 3/4 与 $BASH_COMMAND 的原文完全对应;没有 set -e 时 ERR 只报告不退出,脚本继续、最终退出码 0。注意 help 原文把 ERR 描述成「-e 开启时会导 致退出的失败」,而实测表明普通失败(不在条件上下文)即使没开 -e 也会触发——ERR 的真实语义与 bash 手册一致:只要失败不是被 &&/||/if 条件吸收就会触发(详见 FAQ Q2)。

3.7 trap DEBUG:统计执行了多少条简单命令

DEBUG 钩子在每条简单命令前触发,可用来做执行计数/追踪:

cat debugcount.sh
bash debugcount.sh

实测回显:

$ cat debugcount.sh
#!/bin/bash
count=0
trap 'count=$((count+1))' DEBUG
echo "第一条"
echo "第二条"
true
ls /root/demo/trap > /dev/null
trap - DEBUG
echo "捕获到的 DEBUG 触发次数: $count"
$ bash debugcount.sh
第一条
第二条
捕获到的 DEBUG 触发次数: 5

💡 要点:计数 5 = 四条业务命令(两个 echo、true、ls)+ trap - DEBUG 这条命令本身(它执行前 DEBUG 还在)。DEBUG trap 的开销是每条命令一次,只用于排障,不要留在生产脚本里。

3.8 trap RETURN:函数返回时触发(需 functrace)

RETURN 默认只在 . /source 的脚本结束时触发;函数返回触发需要 set -o functrace:

cat rettrap.sh
bash rettrap.sh

实测回显:

$ cat rettrap.sh
#!/bin/bash
set -o functrace
trap 'echo "[RETURN] 一个函数刚返回"' RETURN
myfunc() {
  echo "函数体第一行"
  echo "函数体第二行"
}
myfunc
echo "主流程结束"
$ bash rettrap.sh
函数体第一行
函数体第二行
[RETURN] 一个函数刚返回
主流程结束

💡 要点:[RETURN] 行紧随函数体输出之后、主流程之前。functrace 会连带把 DEBUG/ERR 也传染给函数内命令(性能与副作用都要评估),生产中 RETURN hook 多用于函数级的资源计数或调用链审计。

3.9 信号号与信号名等价:trap 15 与 trap TERM

help 原文说 signal_spec 可以是 <signal.h> 的名字或编号,纯数字实测:

cat sig15.sh
bash sig15.sh; echo 退出码=$?

实测回显:

$ cat sig15.sh
#!/bin/bash
trap 'echo "15 号信号(=SIGTERM)被捕获"' 15
echo "pid=$$ 就绪"
kill -15 $$
echo "捕获后继续运行"
$ bash sig15.sh; echo 退出码=$?
pid=27429 就绪
15 号信号(=SIGTERM)被捕获
捕获后继续运行
退出码=0

💡 要点:trap ... 15 与 trap ... TERM 完全等价,kill -15 也照常被拦。可移植性建议:脚本里写信号名(不同架构的实时信号编号可能不同),数字编号留给交互排障。

3.10 trap -l:信号名与编号清单

trap -l

实测回显:

$ trap -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

💡 要点:x86_64 上共 31 个标准信号 + 34~64 的实时信号段(32/33 被系统内部占用,清单里没有)。最常用的退出信号都在前 15 位:HUP(1)、INT(2)、QUIT(3)、TERM(15);9) SIGKILL 与 19) SIGSTOP 是仅有的两个不可捕获/不可忽略的信号(4.4 实测 KILL)。

3.11 子 shell 的 trap 继承规则:捕获型被重置、忽略型才继承

这是最容易被误解的一组行为。捕获型 trap 不会被 ( ) 子 shell 继承(重置为脚本启动时从父进程继承的值):

trap 'echo "[T] 子shell 收到 TERM"' TERM
( kill -TERM $BASHPID; echo "子shell 还活着" )
echo "父脚本仍存活"; trap - TERM

实测回显:

$ trap 'echo "[T] 子shell 收到 TERM"' TERM
$ ( kill -TERM $BASHPID; echo "子shell 还活着" )
Terminated
$ echo "父脚本仍存活"; trap - TERM
父脚本仍存活

忽略型 trap '' 则会被子 shell 继承(同一信号换个注册方式):

trap '' TERM
( kill -TERM $BASHPID; echo "子shell 还活着(忽略型继承生效)" )
trap - TERM

实测回显:

$ trap '' TERM
$ ( kill -TERM $BASHPID; echo "子shell 还活着(忽略型继承生效)" )
子shell 还活着(忽略型继承生效)
$ trap - TERM

忽略型还会传递给外部子进程,且子进程无法恢复(被忽略的信号按 POSIX 不可重新捕获):

trap '' INT
bash -c 'kill -INT $$; echo "外部命令收到 INT 没死, \$?=$?"'
trap - INT

实测回显:

$ trap '' INT
$ bash -c 'kill -INT $$; echo "外部命令收到 INT 没死, \$?=$?"'
外部命令收到 INT 没死, $?=0
$ trap - INT

💡 要点:三段实测给出完整规则——捕获型 trap 对子 shell 是「重置为默认」(第一段:子 shell 被 TERM 杀死,只留下 Terminated 报告,父脚本因 $BASHPID 精准定位而安然无恙);忽略型对子 shell 与外部命令都是「继续忽略」(第二、三段)。因此:想让子 shell 有信号处理,必须在子 shell 内部重新 trap;想让整个进程树忽略 Ctrl+C(如长时间无人值守作业),父层 trap '' 一次即可。

3.12 后台子进程设置自身 trap + 外部 kill 触发

bash child.sh & 是新进程(不是子 shell), trap 不会从父脚本继承——子脚本自己注册后即可优雅退出:

cat child.sh
bash child.sh &
kill -TERM <后台子进程 PID>; wait

实测回显:

$ cat child.sh
#!/bin/bash
trap 'echo "child($$): 收到 TERM, 清理后退出"; exit 0' TERM
echo "child($$) 就绪"
while :; do sleep 0.2; done
$ bash child.sh &   # 启动后台进程
$ kill -TERM <后台子进程 PID>; wait
child(27422) 就绪
child(27422): 收到 TERM, 清理后退出
wait 返回=0

💡 要点:外部 kill -TERM 命中后台子进程,其自身注册的 trap 执行了清理并 exit 0,wait 返回 0。这是守护类脚本的通用模式:脚本启动时注册 TERM 处理,运维侧 kill -TERM <pid> 即可触发优雅收尾(与 4.4 的 SIGKILL 对比)。

3.13 EXIT trap 与退出码:不覆盖、可改写

EXIT trap 里的普通命令不改变脚本最终退出码(与 exit 篇 3.7 交叉验证):

cat nopass.sh
bash nopass.sh; echo 最终退出码=$?

实测回显:

$ cat nopass.sh
#!/bin/bash
trap 'echo "[EXIT] 执行清理动作"' EXIT
echo "主体完成"
exit 21
$ bash nopass.sh; echo 最终退出码=$?
主体完成
[EXIT] 执行清理动作
最终退出码=21

trap 里主动 exit N 则会改写最终退出码:

cat passthru.sh
bash passthru.sh; echo 最终退出码=$?

实测回显:

$ cat passthru.sh
#!/bin/bash
trap 'rc=$?; echo "[EXIT] 清理, 触发时退出码=$rc, 主动 exit 0" ; exit 0' EXIT
echo "主体完成"
exit 21
$ bash passthru.sh; echo 最终退出码=$?
主体完成
[EXIT] 清理, 触发时退出码=21, 主动 exit 0
最终退出码=0

💡 要点:第一段 trap 里只有 echo(成功),最终退出码仍是触发时的 21——bash 保留了退出前状态;第二段 trap 主动 exit 0,最终码变成 0。trap 内 rc=$? 读到的是「即将退出时」的状态,需要统一改写退出码(如把所有失败归一为 1)时用它。

3.14 技术拓展:trap 与常见清理方案对比

脚本清理的四种主流做法横向对比(行为差异均来自本文实测):

方案写法要点覆盖路径局限
EXIT trap(本文主角)trap 'rm -rf "$workdir"' EXIT正常结束、exit、set -e 失败、INT/TERM 后的退出全覆盖(3.4/3.5)需注意与退出码的交互(3.13);trap 动作本身若失败被忽略
每个出口手动清理每处 exit 前写 rm -rf ...只有写到的出口出口一多必然漏;可读性差;信号退出(Ctrl+C)覆盖不了
set -e + ERR traptrap 'echo 第$LINENO行失败' ERR失败即报告(3.6),配合 -e 退出只管「报错」,不负责清理资源;条件上下文中的失败不触发
systemd 服务(ExecStopPost=)单元文件里配 ExecStopPost= 清理命令服务停止/崩溃后由 PID 1 兜底只适用于系统服务形态;脚本裸跑时不存在

关键解读:单脚本场景 EXIT trap 是唯一能「一处声明、路径全覆盖」的方案(3.4/3.5 两段实测),ERR trap 负责定位错误行号、EXIT trap 负责资源回收,两者搭配使用;长驻服务交给 systemd 的 ExecStopPost 兜底,脚本内的 trap 退化为进程内清理。

四、实战示例

4.1 场景一:信号安全的多步作业模板(临时目录 + 三类 trap 协作)

把 3.4/3.5/3.1 的能力组合成生产级模板:

cat job.sh
bash job.sh; echo 退出码=$?

实测回显:

$ cat job.sh
#!/bin/bash
workdir=$(mktemp -d)
finish() {
  rc=$?
  rm -rf "$workdir"
  echo "[finish] 清理完成, 退出码 $rc"
}
trap finish EXIT
trap 'echo "[INT] 收到中断, 走 EXIT 清理"; exit 130' INT
trap 'echo "[TERM] 收到终止, 走 EXIT 清理"; exit 143' TERM
echo "作业开始, 临时目录 $workdir"
echo step1 > "$workdir/step1.txt"
echo "作业完成"
$ bash job.sh; echo 退出码=$?
作业开始, 临时目录 /tmp/tmp.FVxyTEptW7
作业完成
[finish] 清理完成, 退出码 0
退出码=0

💡 要点:模板的三层设计——INT/TERM 只负责「置退出码后 exit」(128+N 惯例:130/143),统一由 EXIT hook 执行清理;finish 里 rc=$? 保留触发时的状态。信号到达、set -e 失败、正常结束三种路径都收敛到同一个清理出口。

4.2 场景二:后台任务收 kill -INT——SIGINT 被忽略,trap 拦不住

异步(后台)启动的脚本,SIGINT 被自动忽略且无法用 trap 覆盖:

cat longjob2.sh
bash longjob2.sh & kill -INT <后台PID>; wait

实测回显:

$ cat longjob2.sh
#!/bin/bash
trap 'echo "[清理] 收到 INT, 清理后退出"; exit 130' INT
echo "长作业运行中, pid=$$"
sleep 2
echo "正常完成"
$ bash longjob2.sh & kill -INT <后台PID>; wait   # trap 设了也拦不住
长作业运行中, pid=28837
正常完成
wait 返回=0

💡 要点:kill -INT 发出去了,脚本却跑完 sleep 2 正常结束(wait 返回 0)——bash 对异步命令默认忽略 SIGINT/SIGQUIT,且「进入时已被忽略的信号」按 POSIX 规则不可被重新捕获,trap 注册形同虚设。要让后台任务可被 INT 打断,前台方式启动(见 4.3)或改用 TERM 信号(后台任务对 TERM 无此豁免,3.12 实测)。

4.3 场景三:timeout -s INT 前台模拟 Ctrl+C,验证 INT trap 生效

前台执行时 INT trap 正常工作;用 timeout -s INT 定时发送等效于 Ctrl+C:

timeout -s INT 0.5 bash longjob2.sh; echo 退出码=$?
timeout --preserve-status -s INT 0.5 bash longjob2.sh; echo 退出码=$?

实测回显:

$ timeout -s INT 0.5 bash longjob2.sh; echo 退出码=$?
长作业运行中, pid=28863
[清理] 收到 INT, 清理后退出
退出码=124
$ timeout --preserve-status -s INT 0.5 bash longjob2.sh; echo 退出码=$?
长作业运行中, pid=28873
[清理] 收到 INT, 清理后退出
退出码=130

💡 要点:两种写法 trap 都触发了([清理] 行打印、脚本以 130 退出);默认情况下 timeout 用自己的约定返回 124(超时),加 --preserve-status 才透传子进程的 130。排查 INT 处理逻辑时注意区分「trap 有没有触发」与「外层工具返回什么码」两件事。

4.4 场景四:SIGKILL(9) 不可捕获——与 TERM 的对照

TERM 可被 trap 优雅处理,SIGKILL 让一切 trap 失效:

cat daemon.sh
bash daemon.sh & kill -TERM <pid>; wait
bash daemon.sh & kill -9 <pid>; wait

实测回显:

$ cat daemon.sh
#!/bin/bash
trap 'echo "[daemon] 收到 TERM, 优雅退出"; exit 0' TERM
echo "daemon pid=$$ 运行中"
while :; do sleep 0.2; done
$ bash daemon.sh & kill -TERM <pid>; wait   # TERM 被 trap 捕获
daemon pid=28844 运行中
[daemon] 收到 TERM, 优雅退出
wait 返回=0
$ bash daemon.sh & kill -9 <pid>; wait   # SIGKILL 直接终止, trap 失效
daemon pid=28849 运行中
/root/demo/trap/trp6.sh: line 72: 28849 Killed                  bash daemon.sh
wait 返回=137

💡 要点:TERM 路径打印优雅退出、wait 返回 0;kill -9 路径进程直接被内核终结(bash 只能报告 Killed),wait 返回 137(=128+9)。SIGKILL/SIGSTOP 由内核直接处理,任何 trap、任何编程语言都拦不住——「优雅退出」体系的意义就是把 kill -9 之外的所有路径都纳入收尾流程。

五、注意事项与常见问题

Q1:kill -9 为什么杀掉了设了 trap 的脚本? SIGKILL(9) 由内核直接终止进程,不经过用户态信号处理,trap 完全无效(4.4 实测对比:TERM 被捕获返回 0,-9 直接 Killed 返回 137)。同理 SIGSTOP 也不可捕获。设计上只能对 TERM/INT/HUP 做优雅处理,并把 kill -9 视为「放弃收尾」的最后手段。

Q2:ERR trap 到底什么时候触发?没开 set -e 也会触发吗? 会。3.6 实测在没有 set -e 的脚本里 false 与 cat 不存在的文件 各触发一次 ERR;help 原文「-e 开启时会导 致退出的失败」的措辞容易误导,bash 手册的准确语义是:触发条件与 set -e 的判定条件相同——&&/|| 列表中被短路吸收的失败、if/while 条件中的失败不触发,独立的失败命令都触发。ERR 默认只报告不退出,配合 set -e 才有「报告+退出」的组合效果。

Q3:EXIT trap 里的命令会不会把脚本的退出码改掉? 普通命令不会:bash 保留触发退出时的状态(3.13 实测 trap 里 echo 成功后最终码仍是 21;exit 篇 3.7 独立实测 9)。只有 trap 里显式 exit N 才改写(3.13 实测 21→0)。想让清理动作「顺带」报告清理成败时再考虑 exit,一般场景不需要。

Q4:父脚本设置的 trap,( ) 子 shell 里为什么不起作用? 捕获型 trap 不会被子 shell 继承——子 shell 里的信号处置被重置为脚本启动时从父进程继承的值(3.11 第一段实测:子 shell 收 TERM 直接被杀)。忽略型 trap '' 是唯一会继承的形态(3.11 第二、三段实测,且外部子进程无法解除忽略)。需要在子 shell 内有信号处理就必须在子 shell 内重新 trap。

Q5:后台启动的脚本设了 INT trap,为什么 Ctrl+C/kill -INT 拦不住? bash 对异步命令默认忽略 SIGINT/SIGQUIT,且进入时已忽略的信号不可重新捕获——trap 注册无效(4.2 实测脚本跑完正常退出、wait 返回 0)。可行替代:前台执行(4.3 的 timeout 方式)、对后台任务改用 TERM(3.12 实测可捕获),或在任务内部显式恢复 INT 处置前先评估进程树。

Q6:trap -p 显示的是 SIGINT,我注册时写的是 INT,是两个信号吗? 同一个。信号名对大小写不敏感、SIG 前缀可省(help 原文),但 trap 表按 <signal.h> 规范名显示(3.2 实测 trap -- 'echo INT-handler' SIGINT)。排障时以 trap -p 的输出为准;trap -l(3.10)可查名字与编号对应关系。

六、参考链接