ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Shell信号处理与trap:优雅处理中断和资源清理

Shell信号处理与trap:优雅处理中断和资源清理 脚本跑到一半按了 CtrlC临时文件留在/tmp里运维误kill了进程日志写一半就断。这些脏乱局面靠trap解决。trap让你在收到信号时先执行一段清理代码再决定退不退。这一篇讲信号基础、trap 用法和几个真实清理场景。一、信号是什么信号是内核发给进程的一段小通知。CtrlC 发的是 SIGINTkill默认发 SIGTERM。常用的几个信号编号含义能否捕获HUP1终端挂断常用于让进程重读配置能INT2CtrlC能QUIT3Ctrl\能KILL9必杀不能TERM15kill 默认能TSTP20CtrlZ能CHLD-子进程退出能kill -l可以列全。重点记住 KILL 和 STOP 是内核级的trap 拦不住——kill -9就是不给你清理机会能不用就不用。平时写脚本用得到的就这几个INTCtrlC、TERMkill 默认最该响应、EXIT伪信号、ERR伪信号命令失败时触发。HUP 一般留给让进程重读配置的场景Nginx 重载配置就是kill -HUP $(cat /run/nginx.pid)脚本里同理。二、发送信号kill 和 kill -0trap是收信号日常运维还得会发。kill不只是杀进程它是信号投递器命令作用kill 1234给进程 1234 发 SIGTERM默认kill -INT 1234相当于替用户按 CtrlCkill -HUP 1234让进程重读配置kill -0 1234不发信号只检查进程在不在killall nginx按名字给所有 nginx 进程发信号kill -0在脚本里最实用用来判断进程是否还活着ifkill-0$PID2/dev/null;thenecho$PID还在跑elseecho$PID已经退出fi预期输出进程存在12345 还在跑注意kill -0对别人的进程可能因为权限返回失败所以加2/dev/null别把权限报错当进程没了。想让一个脚本在你退出登录后继续跑常见两种写法nohup./longjob.shlongjob.log21./longjob.shlongjob.log21disownnohup让脚本忽略 SIGHUP你退出终端时它不会被带走disown把这个作业从 shell 的作业表里摘掉效果类似。配合后台跑输出落到日志文件这是最朴素的服务化。真正跑生产还是交给 systemd但临时跑个长任务这两条够用。三、trap 基本语法trap命令信号列表收到信号时执行引号里的命令然后继续跑除非命令里exit。想恢复默认动作用trap - 信号。最常见的写法是清理临时文件TMP$(mktemp-tmyapp.XXXXXX)traprm -f $TMP; echo 已清理 $TMPEXIT INTTERMecho开始干活结果写到$TMPsleep100预期正常跑完或中途按 CtrlC都会先打印已清理 …再退出。EXIT是个伪信号脚本退出时无论怎么退都会触发把清理挂在 EXIT 上基本就够了。四、几个实战写法1. 捕获中断友好退出trapecho 用户中断退出; exit 130INT130 是 SIGINT 的标准退出码1282上层脚本好判断是怎么死的。2. 显示收到的是哪个信号handler(){echo收到信号$1exit1}traphandler INTINTtraphandler TERMTERM3. 取消 traptrap- INTTERM恢复成默认动作INT 默认就是终止进程。五、用 trap 防止脚本重入脚本被 cron 拉起来上一次还没跑完下一次又到点了两个实例同时写一份备份文件会出问题。加个锁文件trap 负责解锁#!/bin/bashset-euopipefailLOCK/tmp/myjob.lock# 已经在跑就退出if!mkdir$LOCK2/dev/null;thenecho实例已在运行:$LOCKexit1fi# 不管怎么退都把锁删掉traprmdir $LOCKEXITecho干活中pid$$sleep60预期输出第一次跑干活中pid1234560 秒内再跑一次实例已在运行: /tmp/myjob.lockmkdir是原子操作比先[ -e ]再创建那种两步写法安全两个实例同时进来也只有一个能成功建目录。六、ERR 陷阱出错时打一行上下文set -e让脚本一出错就退但退在哪一行、当时变量是什么往往看不到。挂一个 ERR traperr_report(){echo第$1行命令失败退出码$22}traperr_report $LINENO $?ERRset-euopipefailcp/etc/ghost /tmp/x预期输出cp: cannot stat /etc/ghost: No such file or directory 第 8 行命令失败退出码 1定位比啥也不说就退出快得多。注意 ERR trap 和-e是配合关系不是替代关系-Eerrtrace不开函数里的命令失败不会触发这个 trap。七、完整清理模板#!/bin/bashset-uWORKDIR$(mktemp-d/tmp/cleanup.XXXXXX)LOG/tmp/cleanup.logcleanup(){echo[$(date)] 清理$WORKDIR$LOGrm-rf$WORKDIR}trapcleanup EXITecho工作目录:$WORKDIRecho做点事$WORKDIR/a.txtsleep5预期输出正常跑完工作目录: /tmp/cleanup.abc123/tmp/cleanup.log里多一行[Wed Sep 16 03:00:01 CST 2026] 清理 /tmp/cleanup.abc123。中途按 CtrlC这行照样写进去$WORKDIR也被删掉。八、⚠️ 容易踩的坑trap 里的命令是收到信号那一刻才展开的。想让它用最新变量值写成函数名或用单引号延迟展开别用双引号提前算好那样拿到的是注册 trap 那一刻的旧值。sleep 100被信号打断后直接退出不会跑完剩下的时间。要等满用循环sleep100wait$!wait能被信号唤醒再继续等。trap 不叠加同一信号后写的 trap 覆盖前面的。子进程默认不继承父进程的 trap除非显式trap ...; ( cmd )在子 shell 里重新注册。脚本 trap 了里面(sleep 100)这个子 shell 收到 INT 还是按默认走。别把清理逻辑写得太长太复杂。trap 是应急出口卡住了反而退不出来。九、知识扩展trap 的执行时机Shell 不是一收到信号就立刻打断当前命令。信号先被内核标记为待处理等当前这条命令跑完或进入下一条Shell 才去执行 trap 里的代码。这就是为什么 trap 里的echo有时会延迟到 sleep 结束才出现——它一直在排队。唯一的例外是waitwait本身就是睡在那里等子进程信号一来它立刻返回trap 趁机跑脚本再决定要不要重新 wait。上面那个sleep 100 wait $!的写法就是利用这一点。SIGKILL 和 SIGSTOP 从不交给进程处理内核直接终止 / 暂停。所以任何我 trap 一下就能优雅退出的承诺对kill -9都不成立。写监控脚本时别假设目标进程一定有机会清理外部存储和锁文件要靠看门狗兜底进程挂了但锁文件还在下一次启动得自己检测并清理这种孤儿锁。
返回列表