文章目录
- 概述
- 信号类型
- 可靠信号与不可靠信号
- Fatal信号与Non Fatal信号
- 不可捕获/忽略信号
- 信号工作机制
- 信号处理方式
- 信号嵌套处理
- 信号使用
- 信号发送
- kill命令
- 注册信号处理函数
- 信号安全与函数可重入性
- 可重入函数
- 线程安全与可重入性
- 相关参考
概述
Linux信号机制是进程间通信的一种方式,用于在不同进程之间传递信息,它通过向目标进程发送一个特定的信号,来触发目标进程执行相应的处理操作。信号本质上是在软件层次上对中断机制的一种模拟,可以认为是用户态下的中断机制,它为用户进程提供了一种处理异步事件的方法;用户进程可以注册自定义的信号处理函数,在进程响应外部信号时,会自动调用回调进行处理。
信号类型
Linux定义了64种信号类型,每个信号都有唯一的编号进行标识。在系统中,通过kill -l命令可以查看所有的信号类型。
可靠信号与不可靠信号
Linux信号可以分为不可靠信号和可靠信号。
- 不可靠信号:又称非实时信号,是指在信号传递过程中可能丢失或产生不可预测行为的信号,这意味着当一个进程接收到该信号时,无法确保该信号一定会被进程处理。1-31号信号为不可靠信号。
- 可靠信号:又称实时信号,是保证传递和处理的信号。当一个进程接收到可靠信号时,系统会确保该信号不会丢失,并且会等待进程处理完该信号后再继续执行其他操作,Linux使用队列来保存待处理的信号,保证它们按照接收的顺序被进程处理。34-64号信号为可靠信号。
在日常开发及维护时,见到的基本是不可靠信号:
Fatal信号与Non Fatal信号
对于每一个信号,系统都有默认的处理行为,根据信号的默认处理行为,可以将信号分为 Fatal(致命)信号和 Non-Fatal(非致命)信号。
- Fatal信号:Fatal 信号是指那些默认处理行为会导致进程终止的信号。当进程收到这类信号且没有注册自定义处理函数时,进程会被终止。
- Non Fatal信号:Non-Fatal 信号是指那些默认处理行为不会导致进程终止的信号。这些信号通常用于控制进程状态或通知特定事件。
不可捕获/忽略信号
在Linux系统中,用户通常可以捕获信号,并自定义处理信号处理行为;但有两个信号比较特殊,它们既不能忽略,也不能捕获,只能执行默认处理:
- SIGKILL (9):强制终止进程
- SIGSTOP (19):暂停进程
信号工作机制
与中断的实时响应(CPU执行完一条指令后,就会响应中断请求)不同,进程对信号的处理有一定的滞后性。原因在于,当应用进程接收到其它进程发送的信号时,不会立即做出响应,只有等当前进程陷入到内核空间时,才会进行信号检测。Linux系统中,应用程序处理信号的流程示意如下:
可以看到,系统对信号的检测与响应总是发生在内核态,只有当前进程由于系统调用、中断或异常而进入系统空间以后,从系统空间返回到用户空间的前夕,内核才会进行信号的处理。
检测到信号后,内核在用户栈创建新层,将返回地址指向信号处理函数,确保函数在用户态执行,避免权限泄露;信号处理程序执行完成后,会执行sigreturn
系统调用再次切换到内核态,再由恢复原系统调用或代码执行点。
信号处理方式
在进程接收到一个信号时,可以告诉内核按照下列三种方式之一进行处理:
- 忽略信号:对信号不做任何处理(但 SIGKILL 和 SIGSTOP 不能被忽略);
- 捕获信号:注册自定义的信号处理函数;
- 默认处理:执行系统定义的默认动作。Linux信号的默认处理行为可以分为以下几类:
- Terminate:终止进程;
- Coredump:终止进程并生成核心转储文件;
- Ignore:忽略信号;
- Stop:暂停进程;
- Continue:继续运行进程。
信号嵌套处理
默认情况下,信号处理函数运行期间,如果再次接收到相同信号,信号会被自动阻塞,直到当前处理函数执行完毕。
信号使用
在日常开发或维护过程中,所涉及到信号的使用方式,主要是如何发送信号以及自定义程序的信号的处理行为。
信号发送
Linux系统使用kill系统调用向指定进程发送信号,原型定义如下:
#include <sgnal.h>
int kill(int pid, int sign);
kill函数的pid参数根据取值有多重含义:
- pid>0:将此信号发送给进程ID为pid的进程;
- pid=0:将此信号发送给进程组ID和该进程相同的进程;
- pid<0:将此信号发送给进程组内进程ID为pid的进程;
- pid=-1:将此信号发送给系统所有的进程。
kill命令
在Linux系统中,kill命令用于向进程发送信号,以终止或控制进程。默认情况下,kill命令发送的是SIGTERM(终止信号),这通常会让进程自行清理资源并优雅地终止。如果进程没有响应SIGTERM,可以使用SIGKILL(杀死信号)强制终止进程。
kill [-signum] [PID]
注册信号处理函数
Linux系统下可以通过signal
或sigaction
函数可以注册信号处理函数。signal原型如下:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
sigaction是 signal的增强版,提供了更精细的控制,包括:
* 自定义信号处理函数
* 设置信号屏蔽字(阻塞其他信号)
* 指定信号处理标志(如是否自动重置处理函数)
sigaction函数原型定义如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
信号安全与函数可重入性
信号安全是指线程在信号处理函数当中,不管以任何方式调用你的函数如果不死锁不修改数据,那就是信号安全的。在信号处理函数中应避免调用不可重入的函数(如 printf、malloc),否则可能导致程序崩溃。
可重入函数
一个函数或代码段若能在被中断后安全地再次调用(如信号处理函数中调用),则称为可重入的。不可重入的代码会导致数据损坏或死锁。
可重入代码的条件:
- 不使用全局或静态变量:依赖局部变量或通过参数传递状态。
- 不调用不可重入函数:如malloc()、printf()、非线程安全的库函数。
- 避免锁的嵌套:信号处理函数中不应获取锁(可能引发死锁)。
线程安全与可重入性
可重入函数是线程安全函数的一个真子集。可重入函数一定是线程安全的。尽管线程安全和可重入有时会被不正确地用作同义词,但它们之间有清晰的技术差别。线程安全函数可能使用同步机制(如锁)来保护共享数据,而可重入函数则完全不使用共享数据。
相关参考
- 《Unix环境高级编程》
- Linux signal 图文详解(一)信号简介、信号注册
- 干货】linux内核信号的处理过程