Linux信号, since 2020-11-18

(2020.11.18 Wed)
信号是内核向进程传递信息的方式。

按键信号

在Shell中可通过Ctrl+c来中断正在运行的进程,或Ctrl+Z来中止进程。按下这些键,Shell都向进程发出了信号。进程捕捉到信号,根据其含义来执行特定动作。

使用Shell时,在Shell中输入一个命令,等待命令完成。完成后执行其他命令。在进程运行期间,Shell的命令行输入会阻塞(block),此时Shell不再接受新的命令。

$ping localhost > log

命令ping的进程占据了舞台,称为前台进程每个Shell最多有一个前台进程,该进程会阻塞Shell。如果Shell启动前台进程,那么在Shell中的输入就重新定向到前台进程的stdin。Shell的按键信号,也就是发给前台进程的。

除了一个前台进程,一个Shell还可以有多个后台进程,后台进程不会阻塞Shell的命令行

$ping localhost > log &

有&符号的命令被置于后台,按键信号不会发送给后台进程。

Linux使用特定短语来指代信号,可以用按键发出的信号有三个,都是传递给Shell的前台进程的。

  • SIGINT:按快捷键Ctrl+C,中断(interrupt)前台进程
  • SIGTSTP:按快捷键Ctrl+Z,暂停(stop)前台进程
  • SIGQUIT:按快捷键Ctrl+\,退出(quit)前台进程

Kill命令

按键是向前台进程发出信号的快捷方式,也可以用kill命令向特定进程发出信号。首先在一个Shell中运行一个前台进程

$ping localhost > log

另一个Shell中,找到ping命令对应进程的PID:

$ps aux | grep 'ping localhost'

返回进程的PID,如1111,就可以用kill命令中断程序

$kill 1111

事实上,kill命令可以向系统的任何一个进程发信号,无论这个进程是前台进程还是后台进程,以一个后台进程为例

$ping localhost > log & 

后台进程会在Shell上打印出PID,比如8848,有了这个PID,可以用kill命令向进程发信号

$kill -s SIGTSTP 8848 //format: $kill -s xxxx PID, where xxxx is SIGCONT/SIGALRM
$kill -s SIGCONT 8848 //让暂停的进程继续
  • SIGCONT: 通知暂停的进程继续
  • SIGALRM: 定时信号
$kill -l //用下面命令查询完整的信号列表
$man 7 signal //查询更详细的文档

信号机制

信号本质上很简单,是内核传递给进程的一个整数。

整数 信号
2 SIGINT
3 SIGQUIT
14 SIGALRM

用下面指令查询信号对应的整数

$kill -l

在内核的内存空间里,系统给每个进程预留了一块用于存放信号的空间,这个空间是内核和进程之间沟通的信箱,信箱里存放的就是对应信号的整数。

内核自身也可能产生信号,比如出现分母为0的出发运算这样的错误,内核会用SIGFPE信号来通知进行该运算的进程。

其他进程也可以产生信号,比如用户用快捷键发出的中断信号。无论信号是谁发出,最终都由内核进入目标进程的邮箱,也就是专用内存空间,也就产生了信号。

进程何时查看信号

信号产生后, 就躺在目标进程对应的内存里,直到这个进程来看。信号总是在特定时机下被查看。每次进程完成系统调用、即将退出内核的时间,就是查看信号的最好时机。原因简单,信号保存在内核空间,而进程执行系统调用时正好处于内核模式,查看内核空间的代价最小。如果有信号,进程会执行对应该信号的操作,称作信号处理(signal disposition)。从信号生成到信号处理这段时间,信号处于等待(pending)状态。

信号处理signal disposition

信号没有操纵进程的魔力,只是通知进程某种情况的发生。在ping命令的例子中,ping收到信号受,根据Linux系统的管理,针对每种信号都采取了对应了默认操作,但并不绝对。进程信号处理的三种可能

  1. 无视信号(ignore signal):信号被清除,进城本身不采取任何特殊的操作
  2. 默认操作(default action):每个信号对应有一定的默认操作,比如SIGCONT用于继续进程
  3. 捕获信号(catch signal):根据信号,执行程序中自定义的操作

进程收到信号后,根据信号的种类和程序设计,决定采取哪种行动。以bash脚本为例。脚本运行后成为一个进程,这个进程可以处理信号,某脚本test_signal.bash

#!/bin/bash
while true
do
    echo hello
done

运行脚本

$./test_signal.bash > log &

获得脚本的PID,如10412,向脚本发出信号

$kill -s SIGINT 10412

此时进程对信号的处理,采用的是默认操作。可以利用trap命令,让一个bash脚本捕获信号。

#!/bin/bash

trap "echo 'interrupted'; exit 1; " SIGINT

while true
do
    echo hello
done

命令trap后面跟的参数,一个是捕获信号后想要进行的操作,一个是用来说明想要捕捉信号的名称。除了用信号名,也可以用信号编号。根据脚本中的trap命令可知,这个脚本会捕获SIGINT信号,针对该信号进行自定义处理:先打印interrupt,再以状态1退出。

可以配合bash函数,在捕获信号后进行更复杂的操作

#!/bin/bash

WANNA_QUIT=false

function handle_exit {
    if [ "$WANNA_QUIT" = true ]; then
        echo "bye."
        exit 0
    else
        WANNA_QUIT=true
        echo "press Ctrl+c again to quit."
    fi
}

trap "handle_exit;" SIGINT

while true
do
    echo working...
    sleep 1
done

该脚本中定义了一个叫做handle_exit函数,该函数在第一次被调用时不会直接退出,而是提示用户如果退出,需要再次按下快捷键Ctrl+C,即打印提示。而第二次收到SIGINT信号后,将会输出bye.,并退出程序。

脚本一开始就用trap说明了信号捕获的相关操作,但脚本不会停在trap那一行等待信号。脚本继续执行下去,进行循环操作。只有等到信号出现,脚本才会回到trap说明的操作。对于trap命令来说,语句执行的时机和语句发生作用的时机是分离的。回忆ls,echo,which等语句,当语句执行时,语句规定的操作就进行了,这种工作方式是同步(synchronous),而trap的工作方式是异步(asynchronous)。异步编程用于处理像信号这样出现时机不确定的事件。

C程序中的信号

C语言编写的程序也可以捕获信号,C语言信号相关的内容在头文件signal.h里。捕获信号的语句格式

signal(SIGINT, sigint_handler);

signal函数跟着两个参数,第一个是信号标识,在这里捕捉的是SIGINT,第二个是回调函数,这里是sigint_handler。SIGINT常量是在signal.h中定义的,如果打开头文件,那么可以看到有下面这一行

#define SIGINT 2 /*interrupt*/

回调函数和一般的C语言函数类似,例如定义下面这个函数

void sigint_handler() 
{
     printf('Received SIGINT');
}

这个C语言程序运行时,如果遇到SIGINT信号,会打印received SIGINT。下面是完成的C程序。

#include 
#include 
#include 
void sigint_handler() 
{
     printf('Received SIGINT');
}
int main()
{
    signal(SIGINT, sigint_handler);
    while (1)
    {
        printf('waiting...\n');
    }
    return 0;
}

程序不断打印waiting,此时发出信号,即按下Ctrl+C,将触发程序一开顶的信号处理函数sigint_handler。程序将执行该处理函数中的语句,即打印Received SIGINT。

Reference

1 Vamei,周梓昕著,树莓派开始玩转Linux,中国工信出版集团,电子工业出版社

你可能感兴趣的:(Linux信号, since 2020-11-18)