为shell布置陷阱:trap捕捉信号方法论
bash&shell系列文章:http://www.cnblogs.com/f-ck-need-u/p/7048359.html
家里有老鼠,快消灭它!哎,又给跑了。老鼠这小东西跑那么快,想直接消灭它还真不那么容易。于是,老鼠药、老鼠夹子或老鼠笼就派上用场了,它们都是陷阱,放在那静静地等待着老鼠的光顾。
在shell中,也可以捉"老鼠",捉到"老鼠"后,可以无视它、杀死它或者抓起来逗一番。只需使用内置命令trap(中文就翻译为陷阱、圈套)就可以布置一个陷阱,这个陷阱当然不是捕老鼠的,而是捕捉信号。
通常trap都在脚本中使用,主要有2种功能:
(1).忽略信号。当运行中的脚本进程接收到某信号时(例如误按了CTRL+C),可以将其忽略,免得脚本执行到一半就被终止。
(2).捕捉到信号后做相应处理。主要是清理一些脚本创建的临时文件,然后退出。
1.1 信号说明
详细的信号说明见:信号。常见的信号以及它们的数值代号、说明如下:
Signal Value Comment
─────────────────────────────
SIGHUP 终止进程,特别是终端退出时,此终端内的进程都将被终止
SIGINT 中断进程,几乎等同于sigterm,会尽可能的释放执行clean-up,释放资源,保存状态等(CTRL+C)
SIGQUIT 从键盘发出杀死(终止)进程的信号 SIGKILL 强制杀死进程,该信号不可被捕捉和忽略,进程收到该信号后不会执行任何clean-up行为,所以资源不会释放,状态不会保存
SIGTERM 杀死(终止)进程,几乎等同于sigint信号,会尽可能的释放执行clean-up,释放资源,保存状态等 SIGSTOP 该信号是不可被捕捉和忽略的进程停止信息,收到信号后会进入stopped状态
SIGTSTP 该信号是可被忽略的进程停止信号(CTRL+Z)
每个信号其真实名称并非是SIGXXX,而是去除SIG后的单词,每个信号还有其对应的数值代号,在使用信号时,可以使用这3种方式中的任一一种。例如SIGHUP,它的信号名称为HUP,数值代号为1,发送HUP信号时,以下3种方式均可。
kill - PID
kill -HUP PID
kill -SIGHUP PID
在上面所列的信号列表中,KILL和STOP这两个信号无法被捕捉。一般来说,在设置信号陷阱时,只会考虑HUP、INT、QUIT、TERM这4个会终止、中断进程的信号。
1.2 trap布置陷阱
trap的语法格式为:
. trap [-lp]
. trap cmd-body signal_list
. trap '' signal_list
. trap signal_list
. trap - signale_list 语法说明:
语法1:-l选项用于列出当前系统支持的信号列表,和"kill -l"一样的作用。
-p选项用于列出当前shell环境下已经布置好的陷阱。
语法2:当捕捉到给定的信号列表中的某个信号时,就执行此处给定cmd-body中的命令。
语法3:命令参数为空字符串,这时shell进程和shell进程内的子进程都会忽略信号列表中的信号。
语法4:省略命令参数,重置陷阱为启动shell时的陷阱。不建议此语法,当给定多个信号时结果会出人意料。
语法5:等价于语法4。
trap不接任何参数和选项时,默认为"-p"。
(1).查看当前shell已布置的陷阱。
[root@xuexi ~]# trap
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU
这3个陷阱都是信号忽略陷阱,当捕获到TSTP、TTIN或TTOU信号时,将不做任何处理。
(2).设置一个可以忽略CTRL+C和15信号的陷阱。
[root@xuexi ~]# trap '' SIGINT SIGTERM
[root@xuexi ~]# trap
trap -- '' SIGINT
trap -- '' SIGTERM
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU
这样一来,当前的shell就无法被kill -15杀死。
[root@xuexi ~]# kill $BASHPID;echo kill current bash failed
kill current bash failed
(3).设置一个陷阱,当这个陷阱捕捉到15信号时,就打印一条消息。
[root@xuexi ~]# trap 'echo caught the TERM signal' TERM
[root@xuexi ~]# kill $BASHPID
caught the TERM signal
再查看已设置的陷阱,之前设置为忽略TERM信号的陷阱已经被覆盖。
[root@xuexi ~]# trap
trap -- '' SIGINT
trap -- 'echo caught the TERM signal' SIGTERM
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU
(4).重置针对INT和TERM这两个信号的陷阱为初始状态。
[root@xuexi ~]# trap - SIGINT SIGTERM
[root@xuexi ~]# trap
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU
(5).在脚本中设置一个能忽略CTRL+C和SIGTERM信号的陷阱。
[root@xuexi ~]# cat trap1.sh
#!/bin/bash
# script_name: trap1.sh
#
trap '' SIGINT SIGTERM
sleep
echo sleep success
当执行该脚本后,将首先陷入睡眠状态,按下CTRL+C将无效。仍会执行完所有的命令。
[root@xuexi ~]# ./trap1.sh
^C^C^C^Csleep success
(6).布置一个当脚本中断时能清理垃圾并退出立即脚本的陷阱。
[root@xuexi ~]# cat trap1.sh
#!/bin/bash
# script_name: trap1.sh
#
trap 'echo trap handling...;rm -rf /tmp/$BASHPID$BASHPID;echo TEMP file cleaned;exit' SIGINT SIGTERM SIGQUIT SIGHUP
mkdir -p /tmp/$BASHPID$BASHPID/
touch /tmp/$BASHPID$BASHPID/{a.txt,a.log}
sleep
echo first sleep success
sleep
echo second sleep success
这样,无论是什么情况中断(除非是SIGKILL),脚本总能清理掉临时垃圾。
1.3 布置完美陷阱9大招
(1).陷阱的守护对象是shell进程本身,不会守护shell环境内的子进程。但如果是信号忽略型陷阱,则会守护整个shell进程组使其忽略给定信号。
以下面这个脚本为例,设置的陷阱会捕捉到SIGINT和SIGTERM两个信号,捕捉到信号时将输出陷阱做出处理的时间点。
[root@xuexi ~]# cat trap2.sh
#!/bin/bash
# script_name: trap2.sh
#
trap 'echo trap_handle_time: $(date +"%F %T")' SIGINT SIGTERM
echo time_start: $(date +"%F %T")
sleep
echo time_end1: $(date +"%F %T")
sleep
echo time_end2: $(date +"%F %T")
执行该脚本,并另开一个会话窗口,杀死trap2.sh脚本。
[root@xuexi ~]# ./trap2.sh
[root@xuexi ~]# killall -s SIGTERM trap2.sh
执行结果如下。
time_start: -- 12:59:23
trap_handle_time: -- 12:59:33
time_end1: -- ::
time_end2: -- ::
结果中的trap_handle_time证明,脚本所在shell进程收到SIGTERM信号后,trap成功进行了处理。如果细心的话,会发现trap处理的时间正好是10秒之后,这并不是因为正好10秒之后才发送SIGTERM信号,而是因为trap就是这么工作的,这是另一个需要注意的点,稍后见下文的(2)。
再次执行脚本,在另个会话窗口下杀死脚本中正在运行的sleep进程和trap2.sh脚本所在进程。
[root@xuexi ~]# ./trap2.sh
[root@xuexi ~]# killall -s SIGTERM sleep ;sleep ; killall -s SIGINT trap2.sh # 另一个会话终端下执行此命令
最终将返回如下结果:
time_start: -- ::
Terminated # 接收到对sleep发送的SIGTERM信号
time_end1: -- 12:23:09 # 没有trap_handle_time,陷阱没有守护sleep进程
trap_handle_time: -- 12:23:19 # shell进程本身收到了SIGINT信号,并被陷阱处理了
time_end2: -- ::
结果说明脚本中的trap陷阱没有守护shell内的sleep进程,只守护了shell本身。同样也发现了,虽然是在3秒后发送INT信号给脚本进程,但陷阱同样是在10秒之后才开始处理的。
再修改脚本中的陷阱为信号忽略陷阱。
[root@xuexi ~]# cat ./trap3.sh
#!/bin/bash
# script_name: trap3.sh
#
trap '' SIGINT SIGTERM
echo time_start: $(date +"%F %T")
sleep
echo time_end1: $(date +"%F %T")
sleep
echo time_end2: $(date +"%F %T")
执行trap3.sh,并在另一个会话终端下杀死sleep进程。
[root@xuexi ~]# ./trap3.sh
[root@xuexi ~]# killall -s SIGTERM sleep;sleep ;killall -s SIGINT sleep # 另一个会话终端下执行此命令
结果如下。从时间差可以看出,无论是SIGTERM还是SIGINT信号,sleep进程都被忽略型trap守护了。
time_start: -- ::
time_end1: -- ::
time_end2: -- ::
(2).如果shell中针对某信号设置了陷阱,则该shell进程接收到该信号时,会等待其内正在运行的命令结束才开始处理陷阱。
其实(1)中的几个示例的结果已经证明了这一点。只要是向shell进程发送的信号,都会等待当前正在运行的命令结束后才处理信号,然后继续脚本向下运行。(实际上,只有当shell脚本中正在执行的操作是信号安全的系统调用时,才会出现信号无法中断进程的情况,而在shell下的各种命令,我们是没法直接知道哪些命令中正在执行的系统调用是系统调用的)。
但sleep命令发起的sleep()调用,是一个信号安全的,所以上面脚本中执行sleep的过程中,信号不会直接中断它们的运行,而是等待它运行完之后再执行信号处理命令。
(3).CTRL+C和SIGINT不是等价的。当某一时刻按下CTRL+C,它是在向整个当前运行的进程组发送SIGINT信号。对shell脚本来说,SIGINT不仅发送给shell脚本进程,还发送给脚本中当前正在运行的进程。
所以,如果shell中设置SIGINT陷阱,不仅会终止脚本中当前正在运行的进程,trap还会立即进行对应的处理。
以下面的脚本trap4.sh为例。
[root@xuexi ~]# cat trap4.sh
#!/bin/bash
# script_name: trap4.sh
#
trap 'echo trap_handle_time: $(date +"%F %T")' SIGINT
echo time_start: $(date +"%F %T")
sleep
echo time_end1: $(date +"%F %T")
sleep
echo time_end2: $(date +"%F %T")
如果使用kill命令向trap4.sh发送信号,正常情况下trap会在当前运行的sleep进程完成后才进行相关处理。但如果是按下CTRL+C,先看结果。
[root@xuexi ~]# ./trap4.sh
time_start: -- ::
^Ctrap_handle_time: -- ::
time_end1: -- ::
^Ctrap_handle_time: -- ::
time_end2: -- ::
结果中显示,两次按下CTRL+C后,不仅sleep立刻结束了,trap也立即进行处理了。这说明CTRL+C不仅让脚本进程收到了SIGINT信号,也让当前正在运行的进程收到了SIGINT信号。
需要特别说明的是,如果当前正在运行的进程处在循环内,当该进程收到了终止进程后,仅仅只是立即终止当次进程,而不会终止整个循环,也就是说,它还会继续向下执行后续命令并进入下一个循环。如果此时是使用CTRL+C发送SIGINT,则每次CTRL+C时,trap也会一次次进行处理。
注意点(1)(2)(3)很重要,因为搞清楚了它们,才能明白脚本中当前正在运行的进程是先完成还是立即结束,这在写复杂脚本或任务型脚本极其重要。例如大量文档中www.example.com需要替换成www.example.net,假如使用sed进行处理,我们肯定不希望替换了一部分文件的时候被临时终止。
(4).每个陷阱都有守护范围。每一个陷阱只将守护它后面的所有进程,直到遇到下一个相同信号的陷阱。
以shell脚本为例,如下图所示。
(5).当shell环境下设置了信号忽略陷阱时,子shell在启动时将继承该陷阱,且这些信号忽略陷阱不可再改变或重置。信号忽略陷阱是子shell唯一继承的陷阱类型。
先在当前shell环境下设置一个忽略SIGINT的陷阱,和一个不忽略SIGTERM的陷阱。
[root@xuexi ~]# trap '' SIGINT
[root@xuexi ~]# trap 'echo haha' SIGTERM
以下是测试脚本。脚本中首先输出脚本刚启动时的最初陷阱列表,随后修改陷阱并输出新的陷阱列表,最后重置陷阱并输出重置后的陷阱列表。
[root@xuexi ~]# cat trap6.sh
#!/bin/bash
# script_name: trap6.sh
echo old_trap:--------
trap -p
trap 'echo haha' SIGINT SIGTERM
echo new_trap:--------
trap -p
echo "reset trap:------"
trap - SIGINT SIGTERM
trap -p
执行结果如下。
[root@xuexi ~]# ./trap6.sh
old_trap:--------
trap -- '' SIGINT
new_trap:--------
trap -- '' SIGINT
trap -- 'echo haha' SIGTERM
reset trap:------
trap -- '' SIGINT
从结果中可以看出,启动脚本时,父shell中忽略SIGINT的陷阱被继承了,但不忽略信号的陷阱未被继承。而且脚本继承的信号忽略陷阱无法被修改和重置。
(6).交互式的shell下,如果没有定义任何SIGTERM信号的陷阱,则会忽略该信号。
所以,在默认(未定义SIGTERM陷阱)时,无法直接通过15信号杀死当前bash进程。
[root@xuexi ~]# kill $BASHPID;echo passed;kill - $BASHPID
passed
# 此处当前bash已被kill -9强制杀死
(7).除了kill -l或trap -l列出的信号列表,trap还有4种特殊的信号:EXIT(或信号代码0)、ERR、DEBUG和RETURN。DEBUG和RETURN这两种信号陷阱无需关注。
EXIT信号也是0信号,当设置了EXIT陷阱时,每当shell退出时(无论何种方式,除非kill -9杀掉shell)都会被捕捉,并做相关处理。(0信号不执行任何动作,但执行错误检查:当检查发现给定的pid进程存在,则返回0,否则返回1。)
ERR陷阱是在shell(比如脚本)以非0状态码退出时生效的。特别是在设置了"set -e"时,只要某命令的状态码非0,就会直接退出当前shell(比如shell脚本),有了它就不用再在脚本中书写对"$?"是否(不)等于0的判断语句,不过它主要用于避免脚本中产生错误时,错误被滚雪球式的不断放大。很多人将这一设置当作写shell脚本的一项行为规范,但我个人不完全认同,很多时候非0退出状态码是无关紧要的,甚至有时候非0状态码才是继续执行的必要条件。
回到话题上。先看看"set -e"的效果。以下面的脚本为例,在脚本中,mv命令少给了一个参数,它是错误命令,返回的是非0状态码。
[root@xuexi ~]# vim trap8.sh
#!/bin/bash
set -e
echo "right here"
mv ~/a.txt
[ "$?" -eq ] && echo "right again" || echo "wrong here"
如果不设置"set -e",那么会被下一条语句判断,但因为设置了"set -e",使得在mv错误发生时,就立即退出脚本所在的shell。也就是说,对"$?"的判断语句根本就是多余的。结果如下。
[root@xuexi ~]# ./trap8.sh
right here
mv: missing destination file operand after ‘/root/a.txt’
Try 'mv --help' for more information.
可以设置ERR陷阱,专门捕获"set -e"起作用时的信号。例如,当命令错误时,做一些临时文件清理动作等。注意两点:(1).ERR陷阱和set -e没关系,只要shell的退出状态码非0,就会生效,set -e只不过是将shell中(如shell脚本)任何非0退出码的命令都使shell立即非0退出,从而使ERR陷阱生效;(2).当捕获到了ERR信号时,脚本不会再继续向下运行,而是trap处理结束后就立即退出。例如:
[root@xuexi ~]# vim trap8.sh
#!/bin/bash
set -e
trap 'echo continue' ERR
echo "right here"
mv ~/a.txt
[ "$?" -eq ] && echo "right again" || echo "wrong here"
echo haha
执行结果如下:
[root@xuexi ~]# ./trap8.sh
right here
mv: missing destination file operand after ‘/root/a.txt’
Try 'mv --help' for more information.
continue
(8).在trap中两个很好用的变量:BASH_COMMAND和LINENO。BASH_COMMAND变量记录的是当前正在执行的命令行,如果是用在陷阱中,则记录的是陷阱触发时正在运行的命令行。LINENO记录的是正在执行的命令所处行号。
例如:
[root@xuexi ~]# vim trap8.sh
#!/bin/bash
set -e
trap 'echo "error line: $LINENO,error cmd: $BASH_COMMAND"' ERR
echo "right here"
mv ~/a.txt
执行结果。
[root@xuexi ~]# ./trap8.sh
right here
mv: missing destination file operand after ‘/root/a.txt’
Try 'mv --help' for more information.
error line: ,error cmd: mv ~/a.txt
(9).处理脚本中启动的后台进程。
通常trap在脚本中的作用之一是在突然被中断时清理一些临时文件然后退出,虽然它会等待脚本中当前正在运行的命令结束,然后清理并退出。但是,很多时候会在脚本中使用后台进程,以加快脚本的速度。而子shell中的后台进程在终端中断时会独立挂靠在init/systemd下,所以它不受终端的影响,更不受shell环境的影响。换句话说,当脚本突然被中断时,即使陷阱捕获到了该信号,并清理了临时文件后退出,但是那些脚本中启动的后台进程还会继续运行。
这就给脚本带来了一些不可预测性,一个健壮的脚本必须能够正确处理这种情况。trap可以实现比较好的解决这种问题,方法是在trap的命令行中加上向后台进程发送信号的语句,然后再退出。
以下面的脚本为例。
[root@xuexi ~]# vim trap10.sh
#!/bin/bash
trap 'echo first trap $(date +"%F %T");exit' SIGTERM
echo first sleep $(date +"%F %T")
sleep &
echo second sleep $(date +"%F %T")
sleep
该脚本中首先将一个sleep放入后台运行。正常情况下,该脚本执行5秒后就会退出,但在20秒后后台进程sleep才会结束,即使突然发送中断信号TERM触发trap也一样。
于是现在的目标是,在sleep 5的过程中突然中断脚本时,能杀死后台sleep进程。可以使用"!"这个特殊变量。修改后的脚本如下。
[root@xuexi ~]# vim trap10.sh
#!/bin/bash
trap 'echo first trap $(date +"%F %T");kill $pid;exit' SIGTERM
echo first sleep $(date +"%F %T")
sleep &
pid="$!"
sleep &
pid="$! $pid"
echo second sleep $(date +"%F %T")
sleep
执行该脚本,并在另一个会话窗口发送SIGTERM信号给该脚本进程。
[root@xuexi ~]# ./trap10.sh ; ps aux | grep sleep
[root@xuexi ~]# kill trap10.sh # 另一个会话窗口执行
执行结果如下。可见sleep被正常终止。
first sleep -- ::
second sleep -- ::
first trap -- ::
root 0.0 0.0 pts/ S+ : : grep --color=auto sleep
为shell布置陷阱:trap捕捉信号方法论的更多相关文章
- shell——trap捕捉信号(附信号表)
trap捕捉信号有三种形式 第一种:trap "commands" signal-list 当脚本收到signal-list清单内列出的信号时,trap命令执行双引号中的命令. 例 ...
- trap在shell中捕捉信号
一.trap捕捉到信号之后,可以有三种反应方式:(1)执行一段程序来处理这一信号(2)接受信号的默认操作(3)忽视这一信号 二.trap对上面三种方式提供了三种基本形式:第一种形式的trap命令在sh ...
- shell中trap捕获信号
信号是一种进程间通信机制,它给应用程序提供一种异步的软件中断,使应用程序有机会接受其他程序活终端发送的命令(即信号).应用程序收到信号后,有三种处理方式:忽略,默认,或捕捉.进程收到一个信号后,会检查 ...
- linux中脚本扑捉(trap)信号问题
扑捉ctrl+c信号: #!/bin/bash trap ; function trap() { echo "You press Ctrl+C."; echo "Exit ...
- Linux 改进捕捉信号机制(sigaction,sigqueue)
sigaction函数 sigaction函数的功能是用于改变进程接收到特定信号后的行为. int sigaction(int signum, const struct sigaction *act, ...
- shell编程之trap命令
trap command signal trap捕获信号(软中断),command一般是linux命令 若为' '表示发生陷阱时为空指令,'-'表示发生陷阱时采用缺省指令 signal: HUP(1 ...
- Linux信号-信号集&信号屏蔽字&捕捉信号【转】
转自:https://blog.csdn.net/Lycorisradiata__/article/details/80096203 一. 阻塞信号 1. 信号的常见其他概念 实际执行信号的处理 ...
- Linux下捕捉信号
关于 信号signal的知识铺垫 点这里 信号由三种处理方式: 忽略 执行该信号的默认处理动作 捕捉信号 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个自定义函数,这称为捕捉信号. 进程收 ...
- C语言之捕捉信号
我们有时候需要在程序中做一些对于用户或内核发出的信号后的处理,如写回文件等善后处理的事情,或者直接忽略信号(当你按Ctrl+C时我压根不理你).下面是一段信号处理的代码(POSIX C): int c ...
随机推荐
- ZooKeeper快速学习
"一入Java深似海",过去自身对于分布式的接触,始终处于使用别人构建的框架的水平,最多就是在nginx配置一下第4层的负载均衡(最后有介绍).随着java使用深入,本文将重点理解 ...
- Android 应用退到后台
Android 应用退到后台 2016-4-21 10:29:26 Android L moveTaskToBack(boolean nonRoot) 把包含这个Activity的任务转到后台.并不是 ...
- 基于jenkins搭建一个持续集成服务器
1 引言 1.1 编写目的 指导质量管理部,业务测试组同事进行Jenkins环境部署,通过Jenkins解决测试环境不可控,开发测试环境不一致等问题. 1.2 使用对象 质量管理部.基础研发部,集成部 ...
- 国内网站遭遇SYN攻击事如何及时解决问题
1.SYN/ACK Flood攻击:这种攻击方法是经典最有效的DDOS方法,可通杀各种系统的网络服 务,主要是通过向受害主机发送大量伪造源IP和源端口的SYN或ACK包,导致主机的缓存资源被耗 尽或忙 ...
- 机器学习 —— 基础整理(六)线性判别函数:感知器、松弛算法、Ho-Kashyap算法
这篇总结继续复习分类问题.本文简单整理了以下内容: (一)线性判别函数与广义线性判别函数 (二)感知器 (三)松弛算法 (四)Ho-Kashyap算法 闲话:本篇是本系列[机器学习基础整理]在time ...
- SVN分支/合并操作小记
一.前言 说来惭愧,鄙人从事开发多年,使用svn已经好几个年头了,但是却仅限于update.commit.compare之类的操作,最近想到github上学习别人写的NIO源码,顺便去熟悉git的使用 ...
- hadoop学习第一天-hadoop初步环境搭建&伪分布式计算配置(详细)
一.虚拟机环境搭建 我们用的虚拟机为vmware,Linux镜像为centOS6.5. vmware安装 安装没什么多说的,一路下一步,但是在新建虚拟机的时候有两个地方需要注意: 1.分配处理器1个就 ...
- opnet的sink模块学习 分类: opnet 2014-05-18 10:28 161人阅读 评论(0) 收藏
Sink模块的状态机很简单,只有INIT和DISCARD两个,非强制状态只有DISCARD用于包的销毁.Sink模块的作用就是销毁从输入流接收到的包,并且返回关于包的一系列统计量. Init的入口代码 ...
- Neo4j图数据库
01. 图数据库 图数据库是专门存储和检索大量信息网络的存储引擎.它可以有效地将数据存储为节点和关系,并允许高性能检索和查询这些结构.属性可以添加到节点和关系.节点可以用零个或多个标签标注,关系总是定 ...
- 一小时学会ECMAScript6新特性
ECMAScript 简介 简称es,是一套标准,javascript就是使用这套标准的语言.主流的浏览器使用的是ECAMScript5,ECAMScript6(ECAMScript2015)是一涛新 ...