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 将立即退出

参考内容

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

  1. linux命令行使用for循环,小弟我使用过的Linux命令之for - Bash中的For循环

    我使用过的Linux命令之for - Bash中的For循环 我使用过的Linux命令之for - Bash中的For循环 本文链接:http://codingstandards.iteye.com/ ...

  2. linux 信号_Linux中的信号处理机制 [四]

    信号与线程 Unix的信号机制在诞生之初,生活在只有进程(process)的相对单纯的环境中.自从Unix世界有了线程(thread)的概念,信号就被赋予了发往进程中某个特定线程的能力,当然,这也增加 ...

  3. linux相等路径,关于linux:如何检查Bash中两条路径是否相等?

    在Bash中检查两条路径是否相等的最佳方法是什么? 例如,给定目录结构 ~/ Desktop/ Downloads/ (symlink to ~/Downloads) Downloads/ photo ...

  4. linux作业控制 信号,bash中的作业控制机制

    作业控制 在shell中通过command &可以创建后台作业, 通过jobs -l命令可以查看当前shell中维护的作业列表, 包括他们的作业号, 进程号, 运行状态. 其中作业号(jobI ...

  5. Linux 命令之 let -- bash 中用于计算的工具,用于执行一个或多个表达式

    文章目录 一.命令介绍 二.命令示例 自增操作 自减操作 shell 脚本中的运算表达式 一.命令介绍 let 命令是 BASH 中用于计算的工具,用于执行一个或多个表达式,变量计算中不需要加上 $ ...

  6. linux教程for语句,Bash 中的 For 循环详解

    循环是编程语言的基本概念之一.当你想要一遍又一遍地运行一系列命令直到达到某个条件后终止退出时,循环很方便. 在诸如 Bash 之类的脚本语言中,循环对于自动执行重复性任务非常有用.在 Bash 脚本中 ...

  7. 浅谈Linux中的信号处理机制(三)

    一晃眼,已经到9月底了,都来不及去感慨时间匆匆.最近常常会想明年的今天我将会在那里干着什么样的工作?对未来又是憧憬又是担忧,压力山大.无论如何现在还是踏踏实实的学习吧,能这样安安静静学习的日子也不多了 ...

  8. linux查看java进程_linux中查看java进程

    linux中查看java进程 查看进程可以使用 ps -ef|grep 'java -jar' ps -ef|grep java [root@vm-linux-x86 ~]# ps -ef|grep ...

  9. linux shell 三元运算符,Bash中的三元运算符(?:)

    有没有办法做这样的事情 int a = (b == 5) ? c : d; 用Bash? #1楼 如果条件只是检查是否设置了变量,那么甚至还有一个较短的形式: a=${VAR:-20} 将要分配给a的 ...

最新文章

  1. 融资 6 亿美元后,商汤发布 5 大 AI 产品
  2. IOS设计模式的六大设计原则之开放-关闭原则(OCP,Open-Close Principle)
  3. Go语言入门——dep入门
  4. 【今晚9点】:对话袁荣喜——一名C程序员的打怪之路
  5. [react] 请描述下你对react的新特性Hooks的理解?它有哪些应用场景?
  6. 在多任务(RTOS)环境中使用看门狗
  7. 架构师前辈告诉你:代码该如何才能自己写得容易,别人看得也不痛苦
  8. 获取项目中的文件流InputStream
  9. 如何商业智能平台BI的成本
  10. Ubuntu install flash
  11. Delphi通过Map文件查找内存地址出错代码所在行
  12. 微信撤回软件安卓版_微信强制撤回app
  13. 转:谦逊不是罕见的美德,而是人人可习得的能力
  14. 零基础学Python课后实战第五章
  15. X3D制作简易三维动画
  16. Delphi 写入txt文件
  17. Oracle OCP学习——Catalog的配置与使用
  18. 关于网站搜索引擎优化技巧,杭州SEO公司清法网络有话说
  19. html 带弧度的三角形怎么写的,带圆角三角型的css实现
  20. 2014多校联合-第七场

热门文章

  1. JAVA数组扁平化整合_一文搞定数组扁平化(超全面的数组拉平方案及实现)
  2. TI DSP TMS320F28335 CCS初级踩坑
  3. Python-中文编码
  4. 7时过2小时是几时_餐后2小时血糖正常值是多少?
  5. Java源码学习笔记之lang包——包装类Integer.class
  6. 职场中上级如何和下级面谈
  7. 第一章 解密编程,带你入门编程世界
  8. Vue CKEditor5 快速了解并使用
  9. GPS时间同步系统揭秘各种时间同步方式
  10. 熬夜再战Android之修炼Kotlin-为什么要搞她?