Linux中的信号机制探究
引言
在Linux操作系统中,进程间的通信和控制是系统稳定性和效率的关键。信号机制是Linux进程间通信的一种古老而强大的方式。信号是一种软中断,信号机制是进程间通信的一种方式,采用异步通信方式。本文将对信号使用,机制、原理、实现方式进行探究分析。
信号作用
Linux中存在信号机制,它是一种非常基础的操作Linux系统的方式。它给所有的进程之间提供了一种相互”沟通“的和响应的方式。其中每一个信号都对应着一种特定的动作,比如控制、提醒、请求等。下图是Man pages中列出的每一种信号量的解释。
Linux信号分为POSIX标准信号和POSIX实时信号。对应于 Linux 的信号值为 1-31 和 34-64。信号是异步的,一个进程不必通过任何操作来等待信号的到达。事实上,进程也不知道信号到底什么时候到达。
- POSIX标准信号,不支持排队,信号可能会丢失, 比如发送多次相同的信号, 进程只能收到一次. 信号值取值区间为1~31。
- POSIX实时信号,支持排队, 信号不会丢失, 发多少次, 就可以收到多少次. 信号值取值区间为32~64。
常见POSIX标准信号作用
- SIGHUP - 挂起信号
- 通常由终端挂起或控制进程发送给其子进程。
- 默认行为是终止进程,但进程可以捕获此信号并执行清理操作。
- SIGINT - 中断信号
- 通常由用户通过Ctrl+C发送。
- 默认行为是终止进程,但进程可以捕获此信号以优雅地终止。
- SIGQUIT - 退出信号
- 通常由用户通过Ctrl+。
- 默认行为是终止进程,并生成核心转储(core dump),以便调试。
- SIGUSR1 - 用户定义信号1
- 没有默认行为,完全由应用程序定义用途。
- 常用于进程间通信或触发自定义事件。
- SIGSEGV - 段错误信号
- 当进程访问非法内存时触发。
- 默认行为是终止进程并生成核心转储。
- SIGKILL - 终止信号
- 不可被进程捕获、阻塞或忽略。
- 总是立即终止接收进程。
- SIGTSTP - 停止信号
- 通常由用户通过Ctrl+Z发送。
- 默认行为是停止进程,进程可以被SIGCONT信号恢复。
这些信号各自有着各自的含义,你也可以通过自己的程序去捕获他们(可以捕获的话),去自定义这些信号的行为。如果没有自定义行为,内核将根据信号的默认行为来处理它。
对于自定义信号,这里有一个很简单的例子
from signal import *
import time
def catched(signal,frame):
print("Signal => ", signal, " <=catched" )
def main():
signal(SIGINT,catched)while True:
1)
time.sleep(print("Waiting for signal")
if __name__ == "__main__":
main()
程序会一直等待信号。
当使用Ctrl + C
时

可以发现,Ctrl+C
对应的信号SIGINT
对应数字被捕获了,程序并未被终止。打印的数字为2是因为,每一个信号都有对应的数字与它匹配。
当使用Ctrl + \
发送SIGTSTP
信号时,未被我们的程序捕获,从而被内核捕获,执行默认行为终止进程。
当然,为了避免恶意程序捕获终止信号,导致无法退出的情况出现,Linux中有类似SIGKILL
的信号,不能被捕获,从而杀死进程。
信号产生
信号的产生通常有以下二种情况
硬件方式产生
- 硬件异常 =>
SIGSEGV
程序段错误,CPU检测到内存非法访问等异常,通知内核生成相应信号,并发送给发生事件的进程。 - 硬件中断 =>
SIGINT
使用Ctrl + C
。
- 硬件异常 =>
软件方式产生
通过系统调用,显示发送 =>
SIGKILL
使用kill -n9 pid
Man pages中对于软件发送信号量的简介。
信号状态
信号阻塞 - (signal block)
- 一个信号被阻塞意味着,他不能被传递给其他进程(线程),直到他解除阻塞。
信号的待传递 - (Pending signals)
- 一个信号的待传递是指从一个信号被产生后,到一个信号被传递到目标进程(线程)前的阶段。
信号屏蔽集 - (Signal mask)
- 每一个进程里的线程都有一个信号屏蔽集,传递的信号如果在该集合中的信号会被阻塞。
- 通过
fork
产生的子线程会继承父线程的屏蔽集
信号执行
无论何时,当Linux内核从内核态转换用户态执行时(比如从系统调用返回或者分配线程到CPU执行时),内核都会检查是否存在一个处于待传递、未被屏蔽、传递的对象注册了一个信号处理器的信号。如果存在,内核会执行以下准备步骤:
- 从待传递的信号集合中移除该信号
- 设置信号标志和用户设定的可替换信号栈(如果存在的话)
- 以
signalFrame
的形式保存当前进程(线程)的上下文,将其存储在栈中。保存的信息包括- 程序计数器寄存器,即主程序中断时的下一条指令地址。
- 恢复中断程序所需的特定于架构的寄存器状态。
- 线程当前的信号屏蔽集。
- 线程的替代信号栈设置。
- 更新信号屏蔽集。
signalFrame
结构如图所示
而后内核开始执行信号。具体步骤如下:
- 内核创建为信号处理器创建帧栈,将当前的程序计数器修改为信号处理器的第一条指令,设置信号处理器的返回地址指向用户空间的代码,称为信号蹦床(signal trampoline)
- 内核会把控制还给用户空间的信号处理器。
- 当信号处理结束后,会返回信号蹦床(signal trampoline),其负责执行
sigreturn
,sigreturn
会使用前面存储在栈中的signalFrame
,返回到执行信号之前的上下文。
从内核的视角来看,对于信号处理器的执行和执行其他用户空间中的代码相同,内核并不记录下任何状态信息,所有需要的状态信息都在用户空间的栈和注册器中。
测试
修改信号处理器为睡眠5s,并打印。在处理信号时再次发送SIGINT
信号
def catched(signal,frame):
print("Signal => ", signal, " <=catched" )
= 5
cnt while cnt >= 0:
print(f"Waiting {cnt}s")
-= 1
cnt 1) time.sleep(
结果如下
这里有两个细节
- 当信号被传递后,程序的控制转移到了信号处理器中,不再打印
Waiting for signal
到输出流。 - 当程序在信号处理器中时,程序依然可以被传递信号(该信号不在信号屏蔽集中),且程序控制流会再次传递到新的信号处理器中,就像函数的递归调用一样。但是返回的方式不相同,存储的上下文信息也不相同。