← 中文资源

简体中文版本

保证 shell 脚本退出时清理一定执行

安装 EXIT 与信号 trap,无论成功、失败还是 Ctrl-C,都释放锁、删临时文件、清理子进程。

阅读英文原文

目标

锁文件和临时目录会在脚本被 Ctrl-C、报错或 kill 杀死时泄漏。一个清理函数加 EXIT trap,再把信号 trap 转换为 exit,就能让所有死亡路径共用同一套告别仪式。验证也只要五行:后台起脚本、发中断、检查文件系统残留。

步骤要点

  1. 先声明资源变量与空子进程数组,定义 cleanup:杀子进程并 wait、删工作目录、删锁文件,随后 trap cleanup EXIT 并把 INT/TERM 映射为 exit 触发同一出口。
  2. 用 run_step 包装每个后台步骤,把 $! 登记进 CHILD_PIDS 数组,中断发生在第三步时前两步的残留进程也会被收割。
  3. 实测中断路径:后台运行两秒后 kill -INT,检查锁与工作目录都已消失而不是靠信仰。
  4. 给 cleanup 加幂等闩锁与 trap 反注册(trap - EXIT INT TERM),信号在清理过程中再来一次也不会递归进入 rm -rf。
  5. 再测错误路径:注入失败让 set -e 中途退出,同样验证零残留,覆盖干净退出之外的两种死亡方式。

排错提示

清理跑了子进程还活着,是 kill 后没 wait,且孙进程要靠 setsid 独立进程组兜终端级信号;EXIT trap 执行两次多半是信号 trap 里调 exit 又触发 EXIT,在 cleanup 里先反注册 trap 加 CLEANED 闩锁;flock 释放了却仍被旧锁挡住,是混用了 flock 与手工锁文件,二选一:flock 靠 fd 关闭自动释放,手工锁靠 trap 加 PID 陈旧检测。

适用边界

SIGKILL 无法被 trap,要用陈旧锁 TTL 做最终一致性设计而不是假装清理总会发生;共享终端里 Ctrl-C 会打到每个进程,靠进程组缓解,trap 单枪匹马做不到。

代码示例

cleanup() {
  kill "${CHILD_PIDS[@]}" 2>/dev/null || true
  wait 2>/dev/null || true
  rm -rf "$WORKDIR"
  rm -f "$LOCKFILE"
}
trap cleanup EXIT
trap 'exit 130' INT TERM   # signals become exits

# proof:
# bash job.sh & sleep 2; kill -INT %1; wait
# /tmp/app.lock   -> gone
# /tmp/app.abc123  -> gone