在软件开发,尤其是命令行软件开发过程中,我们经常会遇到需要响应用户中止热键的情况:如Ctrl+C(中止程序)、Ctrl+Z(Shell下发送SIGTSTP信号)等,这些热键最终会以进程间信号的方式通过操作系统传递给进程。一个完善的软件应该能通过合适的方式处理各种信号,并执行用户所需的任务,以Python为例,它提供了signal模块便于我们捕获、处理信号,本文将为读者介绍常见的进程间信号与Python下使用signal响应进程间信号的方法。

本文原载于未命名小站,由作者本人同步至知乎,转载请注明原作者博客地址或本链接,谢谢!封面图片由Sai Kiran Anagani 拍摄。

0x01 什么是信号

在计算机行业中,信号有多重含义,例如线程同步中的信号量(Semaphore),以及进程间通信的信号。本文主要讲述后者,即操作系统中各个独立进程/应用程序间或操作系统-进程间的通讯方式。

进程间信号中的信号,本质上是一系列整数,以Linux为例,可以在源码的arch/x86/include/uapi/asm/signal.h中看到x86架构下各信号的定义(链接),也可以在kernel/signal.c中看到操作系统对于进程间信号的处理与传递流程(链接)。

0x02 进程间信号

上一节我们提到了进程间信号的本质是一系列整数,这些整数被存放在各进程的PCB(Process Control Block,进程控制块,注意不是印刷电路板喔)中。

其简要流程为: 1. 操作系统将信号量注册在PCB的信号队列中 2. 唤醒进程(如果其处于休眠状态) 3. 向进程发送一个中断,使其陷入内核态(这也是信号被称为软中断的原因) 4. 在从内核态恢复到用户态的过程中检测信号队列中是否有信号 5. 如果有信号,退回到用户态,执行信号对应的信号响应函数 6. 继续返回内核态,检查队列中是否有其他信号,有则返回5 7. 所有信号处理结束后,恢复内核态,任务处理结束

1. 进程间信号的发展历史

进程间信号最先出现于UNIX系统,每个信号都有自己的系统调用,后续修改为统一的signal()与kill()调用。最初的进程间信号系统是异步的,而且没有队列的概念,即不同信号间很容易产生冲突,导致应用程序来不及处理前一个信号。POSIX规范后来改进了这一设计,另外规定了实时信号,靠队列的方式避免了信号冲突的问题。

2. POSIX信号规范

为了统一各系统下进程间信号与其整数的统一,POSIX规范规定了19个信号及其对应整数与行为,见下表:

Signal Value Action Comment
───────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no
readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background process

而在Linux下,除了以上19个信号,还有其他共32个信号,编号从1开始(注意不是0),见下表:

3. Linux/UNIX下热键与信号量的对应

在Linux/UNIX下,由于SIGINT与SIGTSTP信号较为常用,这两个信号可以分别使用Ctrl+C与Ctrl+Z快捷键触发,Windows支持前者,但不支持后者。此外在Linux/UNIX下还有一个不常用的Ctrl+\快捷键,用于发送SIGQUIT信号。

需要注意的是,在Linux/UNIX中,以上快捷键均可使用stty命令查看与修改。具体请参考该链接或Linux Man Page: stty(1)。

0x03 Python响应进程间信号

上面简要介绍了一下进程间信号。实际上关于信号还有很多细节可挖,但考虑到读者可能无法一口气接受那么多内容,我们拿Python来举例(毕竟Python和PHP一样算是C语言的『方言』),介绍一下如何响应信号。

1. 使用异常捕获

首先介绍一个简单的方式,即异常捕获。Python脚本运行过程中按下中断键(如Ctrl+C)会触发一个KeyboardInterrupt异常,我们只要在需要处理中断的代码段外使用try...except...将其包裹起来即可,如下:

try:
# Some code
except KeyboardInterrupt:
# Another code

2. 使用signal模块

使用上文异常捕获的方式存在若干不足。一方面,对于一个庞大的系统来说,可能在不同的执行阶段对于退出有不同的处理方式;另一方面,尽管使用Ctrl+C热键触发SIGINT中断是最常见的方式,但并非所有SIGINT信号都是通过热键触发,也并非所有信号都是SIGINT。Python为了实现信号的安装,引入了signal模块。下文以SIGINT与SIGTERM为例,简述该模块的使用。

代码如下:

import signal
def bye(signum, frame):
print("Bye bye")
exit(0)
signal.signal(signal.SIGINT, bye)
signal.signal(signal.SIGTERM, bye)
while True:
pass

当执行过程中按下Ctrl+C或在其他终端窗口中输入kill -2 [pid](2的含义见上表)时,可以看到bye(signum, frame)函数被调用,并成功退出。


通过strace工具同样可以看到signal.signal()函数对应的系统调用,即rt_sigaction():

本文简述了进程间信号与Python下响应进程间信号的方法,提供了很多链接,旨在抛砖引玉。写作过程中可能出现部分错误,欢迎在评论中提出你的意见。如需更进一步的了解,请期待随后的相关源码解析。