目标
锁文件和临时目录会在脚本被 Ctrl-C、报错或 kill 杀死时泄漏。一个清理函数加 EXIT trap,再把信号 trap 转换为 exit,就能让所有死亡路径共用同一套告别仪式。验证也只要五行:后台起脚本、发中断、检查文件系统残留。
步骤要点
- 先声明资源变量与空子进程数组,定义 cleanup:杀子进程并 wait、删工作目录、删锁文件,随后 trap cleanup EXIT 并把 INT/TERM 映射为 exit 触发同一出口。
- 用 run_step 包装每个后台步骤,把 $! 登记进 CHILD_PIDS 数组,中断发生在第三步时前两步的残留进程也会被收割。
- 实测中断路径:后台运行两秒后 kill -INT,检查锁与工作目录都已消失而不是靠信仰。
- 给 cleanup 加幂等闩锁与 trap 反注册(trap - EXIT INT TERM),信号在清理过程中再来一次也不会递归进入 rm -rf。
- 再测错误路径:注入失败让 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