本文完整阅读约需 23 分钟,如时间较长请考虑收藏后慢慢阅读~

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

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),见下表:

编号 信号名称 缺省操作 解释 POSIX
1 SIGHUP Terminate Hang up controlling terminal or process Yes
2 SIGINT Terminate Interrupt from keyboard Yes
3 SIGQUIT Dump Quit from keyboard Yes
4 SIGILL Dump Illegal instruction Yes
5 SIGTRAP Dump Breakpoint for debugging No
6 SIGABRT Dump Abnormal termination Yes
6 SIGIOT Dump Equivalent to SIGABRT No
7 SIGBUS Dump Bus error No
8 SIGFPE Dump Floating-point exception Yes
9 SIGKILL Terminate Forced-process termination Yes
10 SIGUSR1 Terminate Available to processes Yes
11 SIGSEGV Dump Invalid memory reference Yes
12 SIGUSR2 Terminate Available to processes Yes
13 SIGPIPE Terminate Write to pipe with no readers Yes
14 SIGALRM Terminate Real-timerclock Yes
15 SIGTERM Terminate Process termination Yes
16 SIGSTKFLT Terminate Coprocessor stack error No
17 SIGCHLD Ignore Child process stopped or terminated, or got signal if traced Yes
18 SIGCONT Continue Resume execution, if stopped Yes
19 SIGSTOP Stop Stop process execution Yes
20 SIGTSTP Stop Stop process issued from tty Yes
21 SIGTTIN Stop Background process requires input Yes
22 SIGTTOU Stop Background process requires output Yes
23 SIGURG Ignore Urgent condition on socket No
24 SIGXCPU Dump CPU time limit exceeded No
25 SIGXFSZ Dump File size limit exceeded No
26 SIGVTALRM Terminate Virtual timer clock No
27 SIGPROF Terminate Profile timer clock No
28 SIGWINCH Ignore Window resizing No
29 SIGIO Terminate I/O now possible No
29 SIGPOLL Terminate Equivalent to SIGIO No
30 SIGPWR Terminate Power supply failure No
31 SIGSYS Dump Bad system call No
31 SIGUNUSED Dump Equivalent to SIGSYS No

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

在Linux/UNIX下,由于SIGINT与SIGTSTP信号较为常用,这两个信号可以分别使用Ctrl+CCtrl+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模块。下文以SIGINTSIGTERM为例,简述该模块的使用。

代码如下:

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