shell编程(二十一)trap捕捉信号

一    小故事

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

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

二   trap的两个核心功能

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

(1)'忽略信号';当运行中的脚本进程接收到某信号时(例如误按了CTRL+C),可以将其忽略,免得脚本执行到一半就被终止('屏蔽用户非法输入的信号')

要点:'防止意外手动中断shell脚本的执行'

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

三    信号的常见说明

Signal     Value     Comment
─────────────────────────────
SIGHUP        1      '挂起进程',特别是'终端掉线或者用户退出时',此终端内的进程都将被终止
SIGINT        2      '中断进程',几乎等同于SIGTERM(15),会尽可能的释放执行clean-up,释放资源,保存状态等'(CTRL+C)'
SIGQUIT       3      从键盘发出杀死(终止)进程的信号'(CTRL+\)'
SIGKILL       9      '强制杀死进程',该信号不可被捕捉和忽略,进程收到该信号后不会执行任何clean-up行为,所以资源不会释放,状态不会保存-->'内核级别,类似关机的poweroff'
SIGTERM      15      '杀死(终止)进程',几乎等同于sigint信号,会尽可能的释放执行clean-up,释放资源,保存状态等'(系统通知应用程序关闭,类似关机中的shutdown)'
备注:'kill命令的默认不带参数发生的信号就是SIGTERM,让程序友好的退出'
SIGSTOP      19      该信号是不可被捕捉和忽略的进程停止信息,收到信号后会进入stopped状态
SIGTSTP      20      该信号是可被忽略的进程停止信号'(CTRL+Z)'

说明: 'nginx热升级会用到'

参考

平滑升级原理

四    查看信号的常见方式

trap -l
# 64个信号
kill -l

信号说明

每个信号其'真实名称并非是SIGXXX',而是去除SIG后的单词,每个信号还有其对应的数值代号,在使用信号时,可以使用这3种方式中的任一一种

'例如':SIGHUP,它的'信号名称为HUP','数值代号'为1,发送HUP信号时,以下3种方式均可

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

补充说明

在上面所列的信号列表中,'KILL和STOP这两个信号无法被捕捉'

一般来说,在设置信号陷阱时,只会考虑'HUP、INT、QUIT、TERM'这4个会终止、中断进程的信号

五  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"

# 备注:单引号和双引号都可以

查看陷阱列表

列出中断信号与键盘的对应关系 

六    trap常见操作

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

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

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

trap '' SIGINT SIGTERM

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

说明: 之前设置为忽略TERM信号的陷阱'已经被覆盖'

(4)重置信号的陷阱为初始状态(信号复位

信号复位:trap命令后面跟一个'信号(非真实)'或者'数字',可以把信号复位为默认动作,一旦调用了函数,函数设置的陷阱可以被调用这个函数的shell识别,同时在函数外设置的陷阱也可以被函数识别

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

#!/bin/bash
# script_name: ceshi1-trap.sh
# ''或者':'都可以屏蔽信号 -->最好定义成函数,然后调用
trapper(){
    trap '' SIGINT SIGTERM
}
# trap '' SIGINT SIGTERM
trapper
sleep 10
echo sleep success

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

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

#!/bin/bash
# 理解:捕获SIGINT SIGTERM SIGQUIT SIGHUP信号后立即执行删除临时文件的命令
trap 'echo trap handling...;rm -rf /tmp/wzj_$BASHPID;echo TEMP file cleaned;exit' SIGINT SIGTERM SIGQUIT SIGHUP
mkdir -p /tmp/wzj_$BASHPID/
touch /tmp/wzj_$BASHPID/{a.txt,a.log}
sleep 10
echo first sleep success
sleep 10
echo second sleep success
'效果':无论是什么情况中断(除非是SIGKILL),脚本总能清理掉'临时垃圾'

(7)布置完美陷阱必备知识

陷阱(trap)的'守护对象是shell进程本身','不会守护shell环境内的子进程'

'备注':但如果是'信号忽略型陷阱',则会'守护整个shell进程'组使其忽略给定信号

案例1:设置的陷阱会捕捉到SIGING和SIGTERM两个信号,捕捉到信号时将输出陷阱做出处理的时间点。

#!/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脚本

结果说明

结果中的'trap_handle_time证明-->'脚本所在shell进程收到SIGTERM信号后,trap成功进行了处理

'进一步会发现':trap处理的时间正好是10秒之后,这'并不是'因为正好10秒之后才发送SIGTERM信号,'而是'因为trap就是这么工作的

# 注意时间的显示对比
'继续':'再次执行脚本',在'另一个会话窗口下''杀死'脚本中正在运行的'sleep进程'和ceshi.sh脚本所在进程

'终端2'

killall -s SIGTERM sleep ;sleep 3; killall -s SIGINT ceshi.sh

# 说明-->会TREM正在执行的sleep命令

分析结果

time_start: 2020-04-21 16:50:23
Terminated                             '接收到对sleep发送的SIGTERM信号'
time_end1: 2020-04-21 16:50:26         没有trap_handle_time,'陷阱没有守护sleep进程'
trap_handle_time: 2020-04-21 16:50:36  'shell进程本身收到了SIGINT信号,并被陷阱处理了'
time_end2: 2020-04-21 16:50:36

结果说明

(1):脚本中的trap陷阱'没有守护shell内的sleep进程','只守护了shell本身'

(2):虽然是在3秒后发送INT信号给脚本进程,'但陷阱同样是在10秒(后一个sleep 10)之后才开始处理的'

进一步实验

'修改':脚本中的陷阱为信号'忽略陷阱'
#!/bin/bash
# script_name: ceshi2.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")

操作如下

(1):终端1执行ceshi2.sh

(2):另一个会话终端下杀死sleep进程

# 终端2

killall -s SIGTERM sleep;sleep 3;killall -s SIGINT sleep

结果分析

time_start: 2020-04-21 17:06:54
time_end1: 2020-04-21 17:07:04
time_end2: 2020-04-21 17:07:14

从时间差可以看出,'无论是SIGTERM还是SIGINT信号',sleep进程都'被忽略型trap守护了(起作用了)'

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

上面示例的结果已经'证明了这一点':只要是向shell进程发送的信号,都会等待当前正在运行的命令结束后才处理信号,然后继续脚本向下运行。

实际上只有当shell脚本中正在执行的操作是'信号安全的系统调用时','才会出现信号无法中断进程'的情况,而在shell下的各种命令,我们是'没法直接知道哪些命令中'正在执行的'系统调用是系统调用的'

但sleep命令发起的'sleep()调用',是一个信号安全的,所以上面脚本中执行sleep的过程中,'信号不会直接中断它们的运行',而是'等待'它运行完之后再执行信号处理命令

案例3

说明:CTRL+C和SIGINT'不是等价的';当某一时刻按下CTRL+C,它是在'向整个当前运行的进程组'发送SIGINT信号

对shell脚本来说,SIGINT'不仅'发送给shell脚本进程,'还发送'给脚本中当前'正在运行的进程'

所以如果'shell中设置SIGINT陷阱',不仅会终止脚本中当前正在运行的进程,trap还会立即进行对应的处理
#!/bin/bash
# script_name: ceshi3.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命令向ceshi3.sh发送信号,正常情况下'trap会在当前运行的sleep进程'完成后才进行相关处理,但如果是按下CTRL+C先看结果

分析

两次按下CTRL+C后,'不仅'sleep立刻结束了,trap也立即进行处理了'--->'这说明CTRL+C'不仅'让'脚本进程'收到了SIGINT信号,也让'当前正在运行的进程(这里指的是sleep)'收到了SIGINT信号

'特别说明':如果当前正在运行的进程处在'循环内',当该进程收到了终止进程后,仅仅只是立即'终止当次进程',而'不会终止整个循环',也就是说它还会继续向下执行后续命令并进入下一个循环。如果此时是使用CTRL+C发送SIGINT,则每次CTRL+C时,'trap也会一次次进行处理'

体会案例1、2、3,因为搞清楚了它们,才能明白'脚本中当前正在运行的进程'是'先完成'还是'立即结束',这在'写复杂脚本'或'任务型脚本'极其重要

例如大量文档中www.example.com需要'替换成'www.example.net,假如使用sed进行处理,我们肯定'不希望替换了一部分文件'的时候被临时终止

案例4:陷阱的守护范围

每个陷阱都有'守护范围',每一个陷阱'只将守护它后面的所有进程',直到遇到下一个'相同信号'的陷阱

案例5:信号忽略的继承

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

第一步

先在当前shell环境下设置一个'忽略SIGINT的陷阱'和一个'不忽略SIGTERM的陷阱'
​
trap '' 2

trap 'echo Java' 15

第二步

'测试脚本':脚本中首先输出脚本刚启动时的'最初陷阱列表',随后'修改陷阱'并输出新的陷阱列表,最后'重置陷阱'并输出重置后的陷阱列表
#!/bin/bash
# script_name: ceshi4.sh
echo old_trap:--------
trap -p
# 修改
trap 'echo haha' SIGINT SIGTERM
echo new_trap:--------
# 无法修改忽略型的SIGINT-->结果看
trap -p
echo "reset trap:------"
# 重置最初状态
trap - SIGINT SIGTERM
trap -p

分析

启动脚本时,'父shell中忽略SIGINT的陷阱被继承了',但'不忽略信号的陷阱未被继承',而且脚本'继承的信号(GIGINT)忽略陷阱'无法被修改和重置'

案例6:交互

交互式的shell下,如果'没有定义'任何SIGTERM信号的陷阱,则会'忽略该信号'

所以在默认(未定义SIGTERM陷阱)时,'无法直接通过15信号'杀死当前bash进程
kill $BASHPID;echo passed;kill -9 $BASHPID

# 此处当前bash已被kill -9强制杀死(类似ctrl+d或者exit)

案例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状态码
#!/bin/bash
set -e
echo "right here"
mv ~/a.txt
# 上面的错误时,下面的就不会执行了
[ "$?" -eq 0 ] && echo "right again" || echo "wrong here"

分析原因

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

ERR信号

可以设置ERR陷阱,'专门捕获"set -e"起作用时的信号'

例如: 当命令错误时,做一些'临时文件清理动作'等

注意两点:

   (1)ERR陷阱和set -e没关系,只要'shell的退出状态码非0就会生效',set -e只不过是将shell中(如shell脚本)任何非0退出码的命令都使shell立即非0退出,从而使ERR陷阱生效;
  
   (2)当捕获到了ERR信号时,脚本'不会再继续向下运行',而是trap处理结束后就立即退出
#!/bin/bash
set -e
# ERR信号捕获非0的退出
trap 'echo continue' ERR
echo "right here"
mv ~/a.txt
# 这个并没有执行
[ "$?" -eq 0 ] && echo "right again" || echo "wrong here"
echo Java

执行结果

案例八:trap的变量

在trap中两个很好用的变量:'BASH_COMMAND'和'LINENO';

BASH_COMMAND变量记录的是'当前正在执行的命令行',如果是用在陷阱中,则记录的是'陷阱触发时正在运行的命令行'

LINENO记录的是正在执行的命令所处'行号'

# 脚本调试中很重要!!!

测试脚本

set -e

你写的每个脚本都应该在文件开头加上set -e,这句语句告诉bash如果任何语句的执行结果不是true则应该退出。

这样的'好处'是防止错误像滚雪球般变大导致一个致命的错误,而这些错误本应该在'之前就被处理掉'

如果要'增加可读性',可以使用'set -o errexit',它的作用与set -e相同
#!/bin/bash
set -e
# 也定义一个函数吧
trap 'echo "error line: $LINENO,error cmd: $BASH_COMMAND"' ERR
echo "right here"
mv ~/a.txt

案例九处理脚本中启动的后台进程

通常trap在脚本中的作用之一是在'突然被中断时清理一些临时文件然后退出',虽然它会'等待脚本中当前正在运行的命令(now)结束',然后清理并退出

但是很多时候会在脚本中'使用后台进程',以'加快脚本的速度',而'子shell中的后台进程'在'终端中断时'会独立挂靠在'init/systemd下',所以它不受终端的影响,更不受shell环境的影响

'换句话说',当脚本突然被中断时,即使陷阱捕获到了该信号,并清理了临时文件后退出,但是那些'脚本中启动的后台进程'还会继续运行

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

测试脚本

#!/bin/bash
# 15信号说明
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才会结束(end)',即使突然发送中断信号TERM触发trap也一样

'现在需求是':在sleep 5的过程中突然中断脚本时,能'杀死后台sleep进程',可以使用"!"这个特殊变量

改进脚本

#!/bin/bash
trap 'echo first trap $(date +"%F %T");kill $pid;exit' SIGTERM
echo first sleep $(date +"%F %T")
# 放入后台执行
sleep 20 &
# 体会这个的作用
pid="$!"
# 打印输出-->后台shell的进程号
echo $pid
sleep 30 &
pid="$! $pid"
echo second sleep $(date +"%F %T")
# 注意这个作用当前的shell环境
sleep 5
'测试流程':执行该脚本,并在另一个会话窗口发送SIGTERM信号给该脚本进程

具体测试

'执行该脚本',并在'另一个会话窗口'发送SIGTERM信号给该'脚本进程'
# 终端1
 ./ceshi9.sh ; ps aux | grep sleep     
# 另一个会话窗口执行
killall -15 ceshi9.sh

# kill ceshi9.sh -->这个会报错

执行结果

说明:'后台sleep被正常终止'

参考1

参考2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值