(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系统的管理,针对每种信号都采取了对应了默认操作,但并不绝对。进程信号处理的三种可能
- 无视信号(ignore signal):信号被清除,进城本身不采取任何特殊的操作
- 默认操作(default action):每个信号对应有一定的默认操作,比如SIGCONT用于继续进程
- 捕获信号(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,中国工信出版集团,电子工业出版社