linux信号传递给进程,bash中的信号处理机制

Linux 中的信号

信号(Signal)是操作系统中常用的进程通信手段, 主要用来描述特定事件的发生, 进程接收到信号时有以下几种处理方式:

捕获并自定义处理函数: 给signal系统调用传递自定义回调函数, 进程在接收到信号时会执行该回调函数.

忽略信号: 给signal系统调用传递SIG_IGN, 内核会直接丢弃该信号, 因此目标进程不会收到该信号.

执行默认操作: 内核对每个信号定义了默认的处理方式, 如果已经给信号设置了忽略或自定义处理函数, 可以给signal系统调用传递SIG_DFL将该信号的处理方式恢复为默认.

signal(SIGINT, SIG_IGN); // 忽略信号

signal(SIGTERM, SIG_DFL); // 恢复信号

signal(SIGSTOP, m_handler); // 自定义信号

在Linux中, 信号的发送依赖于sigqueue, kill, raise系统调用, 信号的处理状态被记录在目标进程的task_struct的signal变量中, 该变量的类型为sigset_t, 每一位存储一个信号的处理状态, 因此又被称为信号位图(signal bitmap). 处于产生(generate)和递送(delivery)之间的信号状态会被标记为未决(pending). 当目标进程短时间内接收到大量重复的信号或使用sigpending系统调用阻塞某个信号时, 目标进程信号位图中该信号的状态就会变为未决. 在Linux 5.3.0中对处于未决状态的信号有两种处理策略:

丢弃: 如果目标进程中某种信号的状态为未决或忽略, 内核会直接丢弃之后产生的所有该种信号, 直到该信号的状态改变. 这是早期的Unix系统中的处理策略, 为了保证兼容性, Linux 5.3.0中编号为1-31的信号遵循丢弃处理策略, 即使用sigqueue发送这些信号, 它们也不会去排队. 因存在丢失信号的可能, 按丢失策略处理的信号被称为不可靠信号, 按照POSIX则被称为非实时信号.

排队: 在Linux中对信号的处理方式做了改进, 内核会在目标进程的task_struct的中维护一个信号队列, 如果内核接收到了一个信号, 且目标进程中该信号的状态为未决, 则会将这个新产生的信号放入目标进程的信号队列中, 这样只要挂起的信号个数没有超过内核设定的上限, 理论上就不会丢失. Linux 5.3.0中编号为32-64的信号遵循排队处理策略, 因此又被称为可靠信号, 按照POSIX则被称为实时信号.

常用信号

大多数Linux发行版可以通过man 7 signal查看当前系统中支持的信号种类, kill -l可以查看所有信号及其对应的数字. 其中我们常用的有:

(2) SIGINT: 给处在前台的正在运行的进程发送的键盘中断信号, 终止(interrupt)其运行, 一般对应Ctrl + C.

(19) SIGSTOP: 不可忽略的暂停信号, 是一种以编程方式发送的信号.

(20) SIGTSTP: 暂停信号, 将当前任务stop并放到后台, 把控制权交给shell, 一般对应Ctrl + Z

(9) SIGKILL: 不可被被阻塞, 处理和忽略的信号, 一般用于强行杀死某个进程, kill -9

(15) SIGTERM: 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理. 通常用来要求程序自己正常退出. shell命令kill缺省产生这个信号。

(14) SIGALRM:

(1) SIGHUP: 本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。 登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

在 bash 中处理信号

bash中的一个典型的应用场景是通过内置命令kill向指定进程发送信号, 默认发送SIGTERM信号. 例如: 退出所有名称为chrome的进程:

> kill `pgrep chrome`

> killall chrome

> kill `ps -ef | grep chrome | awk '{ print $2 }'`

> kill `pidof chrome`

此外, 还可以使用trap对信号进行捕捉, 从而实现对特定信号的处理, 其语法如下.

trap [COMMANDS] [SIGNALS]

trap在捕捉到信号之后会执行设置的命令, 这里的命令可以是任何有效的Linux命令,或一个用户定义的函数. 在shell脚本中, trap可以被用于在退出时清除临时文件, 例如:

#!/bin/bash

tempfile=$(mktemp) || exit

trap 'rm -f "$tempfile"' EXIT

另一种经典的用法是在守护进程中, 捕捉SIGHUP并重读配置文件, 例如:

#!/usr/bin/bash

if [ ! -r "$1" ]; then

echo "Usage: $0 "

exit

fi

echo "PID: $$"

CONFIG=$1

read_config () {

echo "reading cfg from $CONFIG"

source "$CONFIG"

}

read_config

trap "read_config" HUP

while :

do

echo "$var"

sleep 15

done

接下来我们可以在两个终端中进行测试:

# 终端一

> bash remove_temp.sh ./config/cfg1

PID: 8807

reading cfg from ./config/cfg1

from cfg1

reading cfg from ./config/cfg1

after change

# 终端二

> cat > cfg1 << EOF

var="after change"

EOF

> kill -s HUP 8807

通过trap命令也可以保存与重设信号, 例如:

> trap "printf BOOM" INT

> ^CBOOM

> traps=$(trap) # 保存信号处理方式

> trap INT # 重设信号为默认处理方式

> ^C

> eval $traps # 加载信号处理方法

> ^CBOOM

trap还支持捕捉多个信号, 例如:

#!/usr/bin/bash

trap "echo Boom!" SIGINT SIGTERM

echo $PPID $$

while : # 冒号永远为真 也可作为占位符

do

sleep 60

done

trap对大小写不敏感,而且可以忽略前缀SIG, 以下写法是等价的:

trap "echo 123" SIGINT

trap "echo 123" INT

trap "echo 123" 2

trap "echo 123" int

trap "echo 123" Int

特殊情况

bash在执行外部命令时, 会提高前台任务的信号处理优先级, 当前台任务执行完毕或被终止时, bash才会处理刚才收到的信号, 如下例:

# 终端一

> sleep 100 # 这是个外部命令

# 在阻塞中...

> # 100秒之后打印了一个空行并显示提示符

# 终端二

# 查看终端一的进程树

> pstree -ap 16003

bash,16003

`-sleep,17249 100 # 外部命令在子进程中执行

> kill -s INT 16003 # 让终端一的 bash 打印空行

但要注意, 在shell中使用Ctrl + C会对整个进程组发送SIGINT, 因此在上例中如果在终端一中使用Ctrl + C会导致sleep立即结束, 并打印一个空行. 我们也可以把前台任务放到后台来让bash优先处理信号, 但如果bash退出时仍有未完成的后台任务, 则这些任务会变为孤儿进程, 它们的父进程会变为PID=1的init进程:

> (sleep 50 & sleep 50 & wait)

# 进程树

bash,15158

`-bash,7629

|-sleep,7630 50

`-sleep,7631 50

> kill 7629

# 进程树

bash,15158

> ps -ef | grep "sleep 50"

remilia 11168 1 0 16:56 pts/2 00:00:00 sleep 50

remilia 11169 1 0 16:56 pts/2 00:00:00 sleep 50

# 第三列是 PPID, 已经变成 1 了

如果需要在进程退出时将后台任务也终止, 则需要记录后台任务的PID并在退出前将这些进程kill:

#!/usr/bin/bash

BPIDARRAY=()

for i in {0..9}; do

sleep 20 &

BPIDARRAY[$i]=$!

done

sleep 3

trap "kill `echo ${BPIDARRAY[@]}`" EXIT

wait

其他用法

trap不仅可以捕捉定义在中的信号名或者数值, 还支持以下用法:

trap -l: 类似于kill -l, 用于列出当前系统支持的所有信号.

trap -p或trap: 列出通过trap设置的信号处理命令.

> trap -p

trap -- 'code' EXIT

trap -- 'echo SIGINT' SIGINT

trap "some code" EXIT: 仅在shell中可用的编号为0的特殊信号, 在bash中代表处理所有的退出情况, 而在标准shell中则用于捕捉exit.

> trap "code" EXIT

> ^D # 退出终端的同时会打开 vscode

trap "some code" ERR: 捕捉执行出现错误的命令, 在命令执行出现错误时执行预定义的代码块.

trap "some code" DEBUG: 以调试模式执行命令时, 将在每个命令执行前执行预定义的代码块.

注意事项

trap设置只作用于当前进程, 因此要注意除了通过.或source执行的脚本都会产生child shell, 这些脚本中不会继承当前shell设置的trap:

> trap "printf book" 2

> ^Cbook

> bash -c "trap -p" # 无输出

> bash

> ^C # 对子进程无效

在函数中设置的trap也是全局生效的, 重复设置同一个信号则只有最后一次trap有效. 需要特别注意的是, 在bash脚本中或者非交互式bash shell中捕获SIGINT和SIGQUIT时最好按照如下方式:

trap 'rm -f "$tempfile"; trap - INT; kill -s INT "$$"' INT

bash在接收到退出信号时按照WCE (wait and cooperative exit)原则进行处理, 按下Ctrl + C时, 当前进程组都会收到SIGINT, 这样就有以下几种情况(不考虑忽略信号的情况):

前台子进程处理SIGINT:

处理信号然后自己kill, 这样父bash(调用者)就会接收到子进程通过信号非正常退出, 则立即退出当前脚本.

处理信号并用exit正常退出, 这样父bash(调用者)就认为子进程正常执行完毕, 从而继续解释脚本.

前台子进程不处理SIGINT: 这种情况与处理信号并自己kill相似, 父bash(调用者)会立即退出当前脚本.

举例来说, 考虑下面这个脚本:

> cat ping_loop.sh

for i in `seq 254`; do

ping -c 2 "192.168.1.$i"

done

> bash ping_loop.sh

# 如果这样执行, 你需要按 254 次 Ctrl + C 才能完全退出

# 注意这个是 bash 的特性

在上例中, 按下Ctrl + C时, ping先收到SIGINT并处理然后正常退出, 之后bash也会收到SIGINT和上一条命令(ping 192.168.0.1)的退出状态, 发现正常退出因此继续解释之后的命令. 而对于sleep这种对SIGINT信号进行默认处理的命令, 情况则完全不同:

> cat sleep_loop.sh

i=1

while [ "$i" -le 100 ]; do

printf "%d " "$i"

i=$((i+1))

sleep 10

done

echo

> bash sleep_loop.sh

# 如果这样执行, 按一次 Ctrl + C 就可以退出脚本

在上例中, 按下Ctrl + C时, sleep先收到SIGINT并按照默认方式处理, 之后bash也会收到SIGINT和上一条命令(sleep 10)的退出状态, 发现sleep非正常退出因此bash立即退出. 我们可以通过更简单的命令加深理解:

> (ping 192.168.0.1; ping 192.168.0.2)

bash,7744

`-bash,24002

`-ping,24011 192.168.0.1

> kill -2 24011 24002 # 在另一个终端执行

# 由于 ping 会处理 SIGINT 并返回 0, 表示自己正常退出

# bash 24002 认为 ping 24026 正常退出就解释下一条命令

# 进程树会变成下面这样

bash,7744

`-bash,24002

`-ping,24026 192.168.0.2

> (sleep 50; sleep 50)

bash,7744

`-bash,26053

`-sleep,26054 50

> kill -2 26054 26053

# sleep 会以默认方式处理 SIGINT 信号

# 因此 bash 26053 会收到子进程因为 SIGINT 非正常退出的信息

# bash 26053 将立即退出

参考内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值