加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 服务器 > Linux > 正文

LINUX学习:为shell布置陷阱:trap捕捉信号方法论

发布时间:2020-12-13 17:46:30 所属栏目:Linux 来源:网络整理
导读:《LINUX学习:为shell布置陷阱:trap捕捉信号方法论》要点: 本文介绍了LINUX学习:为shell布置陷阱:trap捕捉信号方法论,希望对您有用。如果有疑问,可以联系我们。 本文目次: 1.1 信号阐明 1.2 trap布置陷阱 1.3 布置完善陷阱必备知识 家里有老鼠,快消灭

《LINUX学习:为shell布置陷阱:trap捕捉信号方法论》要点:
本文介绍了LINUX学习:为shell布置陷阱:trap捕捉信号方法论,希望对您有用。如果有疑问,可以联系我们。

本文目次:

1.1 信号阐明

1.2 trap布置陷阱

1.3 布置完善陷阱必备知识


家里有老鼠,快消灭它!哎,又给跑了.老鼠这小东西跑那么快,想直接直接消灭它还真不那么容易.于是,老鼠药、老鼠夹子或老鼠笼就派上用场了,它们都是陷阱,放在那静静地期待着老鼠的光顾.

在shell中,也可以捉"老鼠",捉到"老鼠"后,可以无视它、杀死它或者抓起来逗一番.只需使用内置命令trap(中文就翻译为陷阱、圈套)就可以布置一个陷阱,这个陷阱当然不是捕老鼠的,而是捕获信号.

通常trap都在脚本中使用,主要有2种功效:

(1).忽略信号.当运行中的脚本过程接收到某信号时(例如误按了CTRL+C),可以将其忽略,免得脚本执行到一半就被终止.

(2).捕获到信号后做相应处理.主要是清理一些脚本创建的临时文件,然后退出.

1.1 信号阐明

详细的信号阐明见:信号.常见的信号以及它们的数值代号、阐明如下:

Signal     Value     Comment
─────────────────────────────
SIGHUP        1      终止进程,特别是终端退出时,此终端内的进程都将被终止
SIGINT        2      中断进程,几乎等同于sigterm,会尽可能的释放执行clean-up,释放资源,保留状态等(CTRL+C)
SIGQUIT       3      从键盘发出杀死(终止)进程的信号
 
SIGKILL       9      强制杀死进程,该信号不可被捕捉和忽略,进程收到该信号后不会执行任何clean-up行为,所以资源不会释放,状态不会保留
SIGTERM      15      杀死(终止)进程,几乎等同于sigint信号,会尽可能的释放执行clean-up,保留状态等
 
SIGSTOP      19      该信号是不可被捕捉和忽略的进程停止信息,收到信号后会进入stopped状态
SIGTSTP      20      该信号是可被忽略的进程停止信号(CTRL+Z)

每个信号其真实名称并非是SIGXXX,而是去除SIG后的单词,每个信号还有其对应的数值代号,在使用信号时,可以使用这3种方式中的任一一种.例如SIGHUP,它的信号名称为HUP,数值代号为1,发送HUP信号时,以下3种方式均可.

kill -1 PID
kill -HUP PID
kill -SIGHUP PID

在上面所列的信号列表中,KILL和STOP这两个信号无法被捕获.一般来说,在设置信号陷阱时,只会考虑HUP、INT、QUIT、TERM这4个会终止、中断进程的信号.

1.2 trap布置陷阱

trap的语法格局为:

1.   trap [-lp]
2.   trap cmd-body signal_list
3.   trap '' signal_list
4.   trap    signal_list
5.   trap -  signale_list
 
语法说明:
语法1:-l选项用于列出当前系统支持的信号列表,和"kill -l"一样的作用.
       -p选项用于列出当前shell环境下已经布置好的陷阱.
语法2:当捕获到给定的信号列表中的某个信号时,就执行此处给定cmd-body中的命令.
语法3:命令参数为空字符串,这时shell进程和shell进程内的子进程都会忽略信号列表中的信号.
语法4:省略命令参数,重置陷阱为启动shell时的陷阱.不建议此语法,当给定多个信号时结果会出人意料.
语法5:等价于语法4.
trap不接任何参数和选项时,默认为"-p".

(1).查看当前shell已布置的陷阱.

[root@linuxidc ~]# trap
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU

这3个陷阱都是信号忽略陷阱,当捕捉到TSTP、TTIN或TTOU信号时,将不做任何处理.

(2).设置一个可以疏忽CTRL+C和15信号的陷阱.

[root@linuxidc ~]# trap '' SIGINT SIGTERM
[root@linuxidc ~]# trap
trap -- '' SIGINT
trap -- '' SIGTERM
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU

如许一来,当前的shell就无法被kill -15杀死.

[root@linuxidc ~]# kill $BASHPID;echo kill current bash failed
kill current bash failed

(3).设置一个陷阱,当这个陷阱捕获到15信号时,就打印一条消息.

[root@linuxidc ~]# trap 'echo caught the TERM signal' TERM 
[root@linuxidc ~]# kill $BASHPID
caught the TERM signal

再查看已设置的陷阱,之前设置为忽略TERM信号的陷阱已经被笼罩.

[root@linuxidc ~]# trap
trap -- '' SIGINT
trap -- 'echo caught the TERM signal' SIGTERM
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU

(4).重置针对INT和TERM这两个旌旗灯号的陷阱为初始状态.

[root@linuxidc ~]# trap - SIGINT SIGTERM
[root@linuxidc ~]# trap
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU

(5).在脚本中设置一个能疏忽CTRL+C和SIGTERM信号的陷阱.

[root@linuxidc ~]# cat trap1.sh
#!/bin/bash
# script_name: trap1.sh
#
trap '' SIGINT SIGTERM
sleep 10
echo sleep success

当执行该剧本后,将首先陷入睡眠状态,按下CTRL+C将无效.仍会执行完所有的命令.

[root@linuxidc ~]# ./trap1.sh
^C^C^C^Csleep success

(6).布置一个当剧本中断时能清理垃圾并退出立即剧本的陷阱.

[root@linuxidc ~]# 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 10
echo first sleep success
sleep 10
echo second sleep success

这样,无论是什么情况中断(除非是SIGKILL),脚本总能清理失落临时垃圾.

1.3 布置完美陷阱必备知识

(1).陷阱的守护对象是shell进程自己,不会守护shell环境内的子进程.但如果是信号忽略型陷阱,则会守护整个shell进程组使其忽略给定信号.

以下面这个脚本为例,设置的陷阱会捕获到SIGING和SIGTERM两个信号,捕获到信号时将输出陷阱做出处理的时间点.

[root@linuxidc ~]# 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 10
echo time_end1: $(date +"%F %T")
sleep 10
echo time_end2: $(date +"%F %T")

执行该剧本,并另开一个会话窗口,杀死trap2.sh剧本.

[root@linuxidc ~]# ./trap2.sh
[root@linuxidc ~]# killall -s SIGTERM trap2.sh

执行成果如下.

结果中的trap_handle_time证明,脚本所在shell进程收到SIGTERM信号后,trap成功进行了处理.如果细心的话,会发现trap处理的时间正好是10秒之后,这并不是因为正好10秒之后才发送SIGTERM信号,而是因为trap就是这么工作的,这是另一个必要注意的点,稍后见下文的(2).

再次执行脚本,在另个会话窗口下杀死脚本中正在运行的sleep进程和trap2.sh脚本地点进程.

[root@linuxidc ~]# ./trap2.sh
[root@linuxidc ~]# killall -s SIGTERM sleep ;sleep 3; killall -s SIGINT trap2.sh  # 另一个会话终端下执行此命令

最终将返回如下成果:

time_start: 2017-08-14 12:23:06
Terminated                        # 接收到对sleep发送的SIGTERM信号
time_end1: 2017-08-14 12:23:09    # 没有trap_handle_time,陷阱没有守护sleep进程
trap_handle_time: 2017-08-14 12:23:19   # shell进程自己收到了SIGINT信号,并被陷阱处理了
time_end2: 2017-08-14 12:23:19

结果说明脚本中的trap陷阱没有守护shell内的sleep进程,只守护了shell自己.同样也发现了,虽然是在3秒后发送INT信号给脚本进程,但陷阱同样是在10秒之后才开始处理的.

再改动脚本中的陷阱为信号忽略陷阱.

[root@linuxidc ~]# cat ./trap3.sh
#!/bin/bash
# script_name: trap3.sh
#
trap '' SIGINT SIGTERM
echo time_start: $(date +"%F %T")
sleep 10
echo time_end1: $(date +"%F %T")
sleep 10
echo time_end2: $(date +"%F %T")

执行trap3.sh,并在另一个会话终端下杀死sleep过程.

[root@linuxidc ~]# ./trap3.sh
[root@linuxidc ~]# killall -s SIGTERM sleep;sleep 3;killall -s SIGINT sleep   # 另一个会话终端下执行此命令

成果如下.从时间差可以看出,无论是SIGTERM还是SIGINT信号,sleep进程都被忽略型trap守护了.

time_start: 2017-08-14 12:31:54
time_end1: 2017-08-14 12:32:04
time_end2: 2017-08-14 12:32:14

(2).如果shell中针对某信号设置了陷阱,则该shell进程接收到该信号时,会等待其内正在运行的命令结束才开始处置陷阱.

其实(1)中的几个示例的成果已经证明了这一点.只要是向shell进程发送的信号,都会等待当前正在运行的命令结束后才处理信号,然后继续脚本向下运行.

(3).CTRL+C和SIGINT不是等价的.当某一时刻按下CTRL+C,它是在向整个当前运行的进程组发送SIGINT信号.对shell脚原来说,SIGINT不仅发送给shell脚本进程,还发送给脚本中当前正在运行的进程.

所以,如果shell中设置SIGINT陷阱,不仅会终止脚本中当前正在运行的进程,trap还会立即进行对应的处置.

以下面的剧本trap4.sh为例.

[root@linuxidc ~]# 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 10
echo time_end1: $(date +"%F %T")
sleep 10
echo time_end2: $(date +"%F %T")

如果使用kill命令向trap4.sh发送信号,正常情况下trap会在当前运行的sleep进程完成后才进行相关处理.但如果是按下CTRL+C,先看成果.

[root@linuxidc ~]# ./trap4.sh
time_start: 2017-08-14 13:41:30
^Ctrap_handle_time: 2017-08-14 13:41:31
time_end1: 2017-08-14 13:41:31
^Ctrap_handle_time: 2017-08-14 13:41:32
time_end2: 2017-08-14 13:41:32

结果中显示,两次按下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剧本为例,如下图所示.

为shell布置陷阱:trap捕捉信号方法论

(5).当shell环境下设置了信号忽略陷阱时,子shell在启动时将继承该陷阱,且这些信号忽略陷阱弗成再改变或重置.信号忽略陷阱是子shell唯一继承的陷阱类型.

先在当前shell环境下设置一个疏忽SIGINT的陷阱,和一个不疏忽SIGTERM的陷阱.

[root@linuxidc ~]# trap '' SIGINT
[root@linuxidc ~]# trap 'echo haha' SIGTERM

以下是测试脚本.脚本中首先输出脚本刚启动时的最初陷阱列表,随后改动陷阱并输出新的陷阱列表,最后重置陷阱并输出重置后的陷阱列表.

[root@linuxidc ~]# 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@linuxidc ~]# ./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@linuxidc ~]# kill $BASHPID;echo passed;kill -9 $BASHPID
passed
           # 此处当前bash已被kill -9强制杀死

(7).除了kill -l或trap -l列出的信号列表,trap还有4种特殊的信号:EXIT(或信号代码0)、ERR、DEBUG和RETURN.DEBUG和RETURN这两种信号陷阱无需存眷.

EXIT信号也是0信号,当设置了EXIT陷阱时,每次exit的时候都会被捕获,并做相关处理.

ERR陷阱是在设置了"set -e"时生效的,当设置了"set -e"选项,每次遇到非0退出状态码时会退出当前shell,如果写在脚本中,就是退出脚本.有了它就不用再在脚本中书写对"$?"是否(不)等于0的判断语句,不过它主要用于避免脚本中产生错误时,错误被滚雪球式的不断放大.很多人将这一设置当作写shell脚本的一项行为规范,但我个人不完全认同,很多时候非0退出状态码是无关紧要的,甚至有时候非0状态码才是继续执行的需要条件.

回到话题上.先看看"set -e"的效果.以下面的脚本为例,在脚本中,mv命令少给了一个参数,它是差错命令,返回的是非0状态码.

[root@linuxidc ~]# vim trap8.sh
#!/bin/bash
set -e
echo "right here"
mv ~/a.txt
[ "$?" -eq 0 ] && echo "right again" || echo "wrong here"

如果不设置"set -e",那么会被下一条语句判断,但因为设置了"set -e",使得在mv错误发生时,就立即退出脚本所在的shell.也便是说,对"$?"的判断语句根本便是多余的.结果如下.

[root@linuxidc ~]# ./trap8.sh
right here
mv: missing destination file operand after ‘/root/a.txt’
Try 'mv --help' for more information.

可以设置ERR陷阱,专门捕捉"set -e"起作用时的信号.例如,当命令错误时,做一些临时文件清理动作等.注意,当捕捉到了ERR信号时,脚本不会再继续向下运行,而是trap处理结束后就立即退出.例如:

[root@linuxidc ~]# vim trap8.sh
#!/bin/bash
set -e
trap 'echo continue' ERR
echo "right here"
mv ~/a.txt
[ "$?" -eq 0 ] && echo "right again" || echo "wrong here"
echo haha

执行成果如下:

[root@linuxidc ~]# ./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@linuxidc ~]# vim trap8.sh
#!/bin/bash
set -e
trap 'echo "error line: $LINENO,error cmd: $BASH_COMMAND"' ERR
echo "right here"
mv ~/a.txt

执行成果.

[root@linuxidc ~]# ./trap8.sh  
right here
mv: missing destination file operand after ‘/root/a.txt’
Try 'mv --help' for more information.
error line: 5,error cmd: mv ~/a.txt

(9).处置脚本中启动的后台进程.

通常trap在脚本中的作用之一是在突然被中断时清理一些临时文件然后退出,虽然它会等待脚本中当前正在运行的命令结束,然后清理并退出.但是,很多时候会在脚本中使用后台进程,以加快脚本的速度.而后台进程是独立挂靠在init/systemd下的,所以它不受终端以及shell环境的影响.换句话说,当脚本突然被中断时,即使陷阱捕捉到了该信号,并清理了临时文件后退出,但是那些脚本中启动的后台进程还会继续运行.

这就给脚本带来了一些不可预测性,一个健壮的脚本必须能够正确处理这种情况.trap可以实现比较好的解决这种问题,办法是在trap的命令行中加上向后台进程发送信号的语句,然后再退出.

以下面的剧本为例.

[root@linuxidc ~]# vim trap10.sh
#!/bin/bash
trap 'echo first trap $(date +"%F %T");exit' SIGTERM
echo first sleep $(date +"%F %T")
sleep 20 &
echo second sleep $(date +"%F %T")
sleep 5

该脚本中首先将一个sleep放入后台运行.正常情况下,该脚本执行5秒后就会退出,但在20秒后后台过程sleep才会结束,即使突然发送中断信号TERM触发trap也一样.

于是现在的目标是,在sleep 5的过程中突然中断脚本时,能杀死后台sleep进程.可以使用"!"这个特殊变量.改动后的脚本如下.

[root@linuxidc ~]# vim trap10.sh
#!/bin/bash
trap 'echo first trap $(date +"%F %T");kill $pid;exit' SIGTERM
echo first sleep $(date +"%F %T")
sleep 20 &
pid="$!"
sleep 30 &
pid="$! $pid"
echo second sleep $(date +"%F %T")
sleep 5

执行该脚本,并在另一个会话窗口发送SIGTERM信号给该脚本过程.

[root@linuxidc ~]# ./trap10.sh ; ps aux | grep sleep       
[root@linuxidc ~]# kill trap10.sh    # 另一个会话窗口执行

执行成果如下.可见sleep被正常终止.

first sleep 2017-08-14 21:29:19
second sleep 2017-08-14 21:29:19
first trap 2017-08-14 21:29:24
root      69096  0.0  0.0 112644   952 pts/0    S+   21:29   0:00 grep --color=auto sleep

本文永远更新链接地址

编程之家PHP培训学院每天发布《LINUX学习:为shell布置陷阱:trap捕捉信号方法论》等实战技能,PHP、MYSQL、LINUX、APP、JS,CSS全面培养人才。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读