当前位置: 首页 > Linux

bash中的信号处理机制

时间:2023-04-06 12:00:57 Linux

Linux中的信号信号(Signal)是操作系统中常用的进程通信方式。它主要用来描述特定事件的发生。当进程接收到信号时,有几种处理方式:捕获和自定义处理函数:将自定义的回调函数传递给信号系统调用,进程在接收到信号时会执行该回调函数。忽略信号:将SIG_IGN传递给信号系统调用,内核会直接丢弃该信号,因此目标进程将不会收到该信号。执行默认操作:内核定义了每个信号的默认处理方式。如果信号已经被设置为忽略或者自定义处理函数,可以将SIG_DFL传递给信号系统调用,将信号处理方式恢复为默认。信号(SIGINT,SIG_IGN);//忽略信号signal(SIGTERM,SIG_DFL);//恢复信号signal(SIGSTOP,m_handler);//Linux中自定义信号,信号的发送依赖于sigqueue,kill,raise系统调用,信号的处理状态记录在目标进程的task_struct的signal变量中。这个变量的类型是sigset_t,每一位存储一个信号的处理状态,所以也叫信号位图(signalbitmap)。在生成(generate)和交付(delivery)期间会被标记为待处理(pending)。当目标进程在短时间内接收到大量重复信号或使用sigpending系统调用阻塞某个信号时,目标进程信号位图中目标进程中的信号状态将变为pending。Linux5.3.0中对挂起信号有两种处理策略:丢弃:如果目标进程中某个信号的状态为挂起或忽略,内核将直接丢弃此后产生的所有此类信号,直到该信号的状态发生变化.这是早期Unix系统中的处理策略。为了保证兼容性,Linux5.3.0中编号为1-31的信号采用了丢弃处理策略。这些信号即使使用sigqueue发送,也不会排队。由于存在丢失信号的可能性,按照丢失策略处理的信号称为不可靠信号,根据POSIX称为非实时信号。队列:在Linux中改进了信号处理方法。内核会在目标进程的task_struct中维护一个信号队列。如果内核接收到一个信号,并且目标进程中信号的状态是pending,那么新产生的信号会被放入目标进程的信号队列中,这样只要未决信号的数量不超过内核设置的上限,理论上就不会丢失。在Linux5.3.0中,数量为32-64的信号遵循排队处理策略,因此也被称为可靠信号。根据POSIX,它们被称为实时信号。大部分Linux发行版的常用信号都可以通过man7signal查看当前系统支持的信号类型,kill-l可以查看所有Signals及其对应的编号。其中,我们常用的有:(2)SIGINT:发送给前台正在运行的进程终止(中断)其运行的键盘中断信号,一般对应Ctrl+C。(19)SIGSTOP:不可忽略的暂停信号,这是一个以编程方式发送的信号。(20)SIGTSTP:暂停信号,停止当前任务并置于后台,将控制权交给shell,一般对应Ctrl+Z(9)SIGKILL:不能被阻塞、处理和忽略的信号,一般使用强行杀掉一个进程,kill-9(15)SIGTERM:程序结束(terminate)信号,与SIGKILL不同的是这个信号可以被阻塞和处理。通常用于请求程序正常退出。shell命令kill默认会产生这个信号(14)SIGALRM:(1)SIGHUP:当用户终端连接(正常或异常)结束时发送这个信号,通常在终端控制进程结束时,同一个作业中的各个job会话被通知,并且它们不再与控制终端相关联。登录Linux时,系统会为登录的用户分配一个终端(Session)。所有运行在这个终端上的程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux时,前台进程组和输出到终端的后台进程都会收到SIGHUP信号。这个信号默认的动作是终止进程,所以前台进程组和后台有终端输出的进程都会被终止。对于与终端断开连接的守护进程,此信号用于告诉它重新读取配置文件。bash中处理信号bash中一个典型的应用场景是通过内置命令kill向指定进程发送信号,默认发送SIGTERM信号。例如:退出所有名为chrome的进程:>kill`pgrepchrome`>killallchrome>kill`ps-ef|grep铬|awk'{print$2}'`>kill`pidofchrome`另外,还可以使用trap来捕获信号,从而实现对特定信号的处理。语法如下。trap[COMMANDS][SIGNALS]trap捕获到信号后会执行set命令,这里的命令可以是任何有效的Linux命令,也可以是用户自定义的函数。在shell脚本中,trap可以用来在退出时清除临时文件,例如:#!/bin/bashtempfile=$(mktemp)||exittrap'rm-f"$tempfile"'EXIT的另一个经典用法是在守护进程中,捕获SIGHUP并重新读取配置文件,例如:#!/usr/bin/bashif[!-r"$1"];然后echo"Usage:$0"exitfiecho"PID:$$"CONFIG=$1read_config(){echo"readingcfgfrom$CONFIG"source"$CONFIG"}read_configtrap"read_config"HUPwhile:doecho"$var"sleep15done接下来我们可以在两个终端进行测试:#terminalone>bashremove_temp.sh./config/cfg1PID:8807readingcfgfrom./config/cfg1fromcfg1readingcfgfrom./config/cfg1afterchange#Terminal2>cat>cfg1<kill-sHUP8807通过trap命令该命令还可以保存和重置信号,例如:>trap"printfBOOM"INT>^CBOOM>traps=$(trap)#保存信号处理方式>trapINT#将信号重置为默认处理方式>^C>eval$traps#加载信号处理方法>^CBOOMtrap还支持捕获多个信号,例如:#!/usr/bin/bashtrap"echoBoom!"SIGINTSIGTERMecho$PPID$$while:#冒号始终为真,也可以作为占位符dosleep60donetrap不区分大小写,前缀SIG可以忽略,下面的写法是等价的:trap"echo123"SIGINTtrap"echo123"INTtrap"echo123"2trap"echo123"inttrap"echo123"Intspecialbash在执行外部命令时,会增加前台任务的信号处理优先级。当前台任务执行或终止时,bash会处理刚刚接收到的信号,如下例:#Terminal1>sleep100#ThisisaExternalcommand#Inblocking...>#100秒后,一个空行打印并显示提示#终端2#查看终端1的进程树>pstree-ap16003bash,16003`-sleep,17249100#外部命令在子进程中执行>kill-sINT16003#让终端一个人的bash打印一个空行但要小心,在shell中使用Ctrl+C会将SIGINT发送到整个进程组,因此在上面的示例中,如果您在终端1+C中使用Ctrl将导致睡眠立即结束,并打印一个空行。我们也可以把前台任务放到后台让bash先处理信号,但是如果bash退出时还有未完成的后台任务,这些任务就会成为孤儿进程,它们的父进程会成为PID=1的init进程:>(sleep50&sleep50&wait)#processtreebash,15158`-bash,7629|-sleep,763050`-sleep,763150>kill7629#processtreebash,15158>ps-ef|grep"sleep50"remilia111681016:56pts/200:00:00sleep50remilia111691016:56pts/200:00:00sleep50#第三列是PPID,已经变成了1。如果需要在进程退出时终止后台任务,需要记录后台任务的PID,并在退出前kill这些进程:#!/usr/bin/bashBPIDARRAY=()foriin{0..9};dosleep20&BPIDARRAY[$i]=$!donesleep3trap"kill`echo${BPIDARRAY[@]}`"EXITwait其他用法trap不仅可以捕获定义在中的信号名或值,还可以支持以下用法:trap-l:类似于kill-l,用于列出当前系统支持的所有信号。trap-p或trap:列出由trap命令设置的信号处理。>trap-ptrap--'code'EXITtrap--'echoSIGINT'SIGINTtrap"somecode"EXIT:一个编号为0的特殊信号,仅在shell中可用,在bash中代表所有退出条件,在标准shell中,用于捕捉exit。>trap"code"EXIT>^D#退出终端时,会打开vscodetrap"somecode"ERR:CaptureandexecuteBlock.trap"somecode"DEBUG:当在debug模式下执行命令时,在执行每条命令之前会先执行一个预定义的代码块。注意trap设置只适用于当前进程,所以要小心exceptthrough。orsourceexecution脚本会生成子shell,当前shell设置的陷阱不会在这些脚本中继承:>trap"printfbook"2>^Cbook>bash-c"trap-p"#Nooutput>bash>^C#进入孩子函数中设置的trap也是全局有效的,重复设置同一个信号只有最后一个trap有效。需要注意的是,在bash脚本或非交互式bashshell中捕获SIGINT和SIGQUIT时,最好遵循如下:trap'rm-f"$tempfile";陷阱-情报;kill-sINT"$$"'INTbash在收到退出信号时会按照WCE(waitandcooperativeexit)原则进行处理,按Ctrl+C,此时当前进程组会收到SIGINT,所以有如下情况(不管忽略信号的情况):前台子进程处理SIGINT:处理完信号然后kill自己,这样父bash(调用者)就会收到子进程,如果进程通过信号异常退出,它立即退出当前脚本。处理信号,使用exit正常退出,让父bash(调用者)认为子进程正常执行完毕,继续解释脚本。前台子进程不处理SIGINT:这种情况类似于处理信号并杀死自己,父bash(调用者)将立即退出当前脚本。例如,考虑以下脚本:>catping_loop.shforiin`seq254`;doping-c2"192.168.1.$i"done>bashping_loop.sh#如果这样执行,需要按Ctrl+C254次才能完全退出#注意这是bash的特性。在上面的例子中,当按下Ctrl+C时,ping首先收到SIGINT并进行处理,然后正常退出。之后bash还会收到SIGINT和前面命令的退出状态(ping192.168.0.1),发现正常退出,所以继续解释后面的命令。对于sleep,默认处理SIGINT信号的命令,情况就完全不同了:>catsleep_loop.shi=1while["$i"-le100];doprintf"%d""$i"i=$((i+1))sleep10doneecho>bashsleep_loop.sh#如果完成,按一次Ctrl+C退出脚本。在上面的例子中,当按下Ctrl+C时,sleep首先接收到SIGINT并按默认方式处理,之后之后,bash还会收到SIGINT和最后一个命令的退出状态(sleep10)。发现sleep异常退出,于是bash立即退出。我们可以通过更简单的命令加深理解:>(ping192.168.0.1;ping192.168.0.2)bash,7744`-bash,24002`-ping,24011192.168.0.1>kill-22401124002#在另一个终端执行#Sinceping会处理SIGINT,返回0,表示正常退出#bash24002认为ping24026正常退出,解释下一条命令#进程树会变成如下bash,7744`-bash,24002`-ping,24026192.168.0.2>(sleep50;sleep50)bash,7744`-bash,26053`-sleep,2605450>kill-22605426053#sleep会默认处理SIGINT信号#所以bash26053会收到这样的信息thechildprocessexitedabnormallybecauseofSIGINT#bash26053willimmediatelyexit参考内容SignalTrapHow"ExitTraps"CanMakeYourBashScriptsWayMoreRobustandReliable