Linux操作系统下的信号

一、引言
首先我们可以简单理解一下信号的概念,信号,顾名思义,就是我们操作系统发送给进程的消息。举个简单的例子,我们在写C/C++程序的时候,当执行a / 0类似的操作的时候,程序直接就挂了,这是什么原因呢?其实本质就是CPU在计算的时候出现异常,触发硬件中断,然后我们操作系统发送了对应的信号给进程,然后我们进程默认就退出了。这一系列东西,我都将在后续的文章中具体的提到。
二、Linux下信号的形式
管他知不知道了不了解,我们都可以先看看Linux下的信号具体长什么样
这里我简单的介绍几种常见的信号
- SIGINT : 这个信号就是我们常用ctrl + c,他默认的作用就是直接将我们的进程退出。
- SIGSEGV:这个就是段错误的信号,比如我们常见的越界,野指针都是这个错误。
- SIGCHLD:子进程退出的时候,我们父进程可以hang住,也就是阻塞等待,也可以轮询非阻塞等待,或者也可以通过这个信号,所以这个信号就是子进程退出的的时候发送给我们父进程的信号。
- SIGKILL:这个信号可以杀死大部分的进程(D状态进程除外),后续我们也会讲解。
三、信号的产生
- 由内核产生
内核在检测到系统事件或异常时会向进程发送信号,例如:
-
硬件异常:如进程访问非法内存(SIGSEGV)、除零错误(SIGFPE)等。
-
资源限制:进程占用资源超限(如 SIGXCPU 表示CPU时间超限)。
-
子进程状态变化:子进程终止时会向父进程发送 SIGCHLD。
-
终端事件:如终端关闭时发送 SIGHUP,用户按下 Ctrl+C 触发 SIGINT。
- 由其他进程发送
进程可以通过系统调用 kill() 向其他进程发送信号:
c
复制
#include <signal.h>
int kill(pid_t pid, int sig); // 向指定PID的进程发送信号
// 实例
kill(1234, SIGTERM) // 向PID为1234的进程发送终止信号。
kill(-1, SIGKILL) // 向所有进程发送 SIGKILL(需权限)。
kill(1234, SIGTERM) 向PID为1234的进程发送终止信号。
kill(-1, SIGKILL) 向所有进程发送 SIGKILL(需权限)。
- 由用户或终端触发
键盘快捷键:
-
Ctrl+C → 发送 SIGINT(中断进程)。
-
Ctrl+Z → 发送 SIGTSTP(暂停进程)。
-
Ctrl+\ → 发送 SIGQUIT(终止并生成核心转储)。
-
命令行工具:
kill -9 PID:发送 SIGKILL(强制终止进程)。
pkill -HUP process_name:发送 SIGHUP(重新加载配置)。
- 进程自身触发
进程可以通过 raise() 或 kill() 向自己发送信号:
c
raise(SIGALRM); // 等同于 kill(getpid(), SIGALRM);
常见用途:
定时器到期(SIGALRM)。
自定义信号处理(如 SIGUSR1/SIGUSR2)。
- 通过条件触发
某些系统调用或条件会隐式产生信号:
alarm():设置定时器,到期后发送 SIGALRM。
sigqueue():发送信号并附加数据(实时信号)。
管道或套接字断开:如写入已关闭的管道会触发 SIGPIPE。
c
复制
#include <signal.h>
#include <stdio.h>
void handler(int sig) {
printf("Received SIGINT!\n");
}
int main() {
signal(SIGINT, handler); // 注册处理函数
while(1) {} // 无限循环
}
四、信号的保存(重要)
1. 操作系统如何管理信号
这里我们首先首先要列出内核的信号相关的数据结构
内核中,我们的进程数据结构(struct task_struct)中维护了三张表:
- block : 阻塞表,表示哪些信号是需要阻塞的
- pending: 未决 的信号,比如我们一个信号被进程接受到,但是block表中阻塞了他,这个时候,我们的pending表中对应的信号的位置就会置一,表示这个信号处于未决的状态。
- handler表:存储我们对应的函数指针数组,记录没有对应的信号的处理方式,我们的signal函数就是将对应的函数指针填充进入这张表中。
2. 我们如何在用户层修改表结构
这里的各种表结构都是位图(操作系统减少内存的消耗),那么既然使用了位图,操作这些表就会比较困难,所以操作系统为我们提供了相关的接口。
这里的sigset就是我们的位图结构,我们可以通过这些接口,操作一个创建出一个定制化的位图结构。
sigpending就是回去当前(调用这个接口的是偶)的pending表。
sigprocmask可以根据我们的需求定制化设置block表的内容,并且可以取出原来的block表。
3. 当信号来到我们的进程的时候,我们的操作如何对其进行组织和管理

- 当信号来到我们的进程的时候,我们的进程首先进行上下文保护,将我们当前执行的情况保存,然后陷入内核。
- 处理本次信号的问题:但是在处理之前,我们需要检查一下block表,1. 如何什么block表表示我们当前的信号是阻塞的,将pending表中对应的位置置一,表示这个信号处于未决的状态,恢复上下文,回到用户态,继续刚才的代码。 2.如何信号没有阻塞,那么回到用户态,执行我们定制化的handler(也可能是退出)。
- 再次回到内核态(为什么这次还要回到内核态呢? 当然是因为我们还要找到刚才代码执行的位置)。
- 这个时候从内核态找到刚才执行的位置,再次回到用户态继续执行刚才的代码。
细节剖析:
- 细节一: 操作系统规定,每次从内核态到用户态的时候都要检查pending表。
这里可以用以个场景来解释pending表检查:
我们的代码最开始的时候block将SIGINT信号进行的阻塞,表示不处理这个信号
这个时候如果我们接收到了SIGINT信号,这个时候pending表中SIGINT被置一,表示
未决, 后续我们如果通过sigprocmask修改block表,SIGINT不阻塞了,这个操作并不会
触发信号检查 (也就是说就算我们这个信号不阻塞了,也不会检查pending表是否有未决的对应信号),也就是说,如果我的后续代码没有任何系统调用,那么即使pending[SIGINT] == 1仍然不会触发任何检查,只有我们在其他地方触发系统调用(getpid() , 或者类似再次SIGINT),才会调用
也就是说任何的系统调用都会触发pending表检查
- 细节二: 我们总是在内核态转成用户态的时候检查了pending表(是否有未决的信号)
五、信号的处理机制
信号既然能够产生,那么我们肯定可以针对自身的需求,定制对应的回调处理函数,操作系统也为我们提供了对应的函数的接口,这个接口的名字就叫做signal
- signum : 就是表示我们想要针对哪个函数定义对应的回调机制。
- handler: 对应的回调函数,这里的参数是一个函数指针
有了这种机制,我们就可以定制化我们信号处理机制。

我这里提供了一个实例代码,我通过signal函数屏蔽了CTRL + C的默认功能,可以看见,当我进行ctrl + c的时候,程序并没有退出,而是执行我定义的handler函数。
一些注意事项
- 虽然操作系统给我们提供了定制化handler的接口,但是也不能让我们为所欲为,比如前面说的SIGKILL接口就不能重新设置,这保证我们总是可以kill -9杀掉这个进程。
- 默认和忽略
c
#define SIG_DFL ((__sighandler_t) 0))
#define SIG_IGN ((__sighandler_t) 1))
操作系统给我们定义了一些宏帮助我们进程默认和忽略设置,比如如果我将signal的第二个参数设置成SIG_IGN就表示我忽略这个信号
基于信号再谈谈操作系统
看到了信号,信号是如何触发的,无非就是内核产生(软件中断),硬件中断,也就是说信号是基于中断产生的,一旦有中断就产生对应的信号,那操作系统呢,也是一样的,操作系统就是基于软件中断(系统调用),硬件中断进行执行,我们可以这样理解,操作系统就是一个死循环。
当各种硬件中断来了(按了键盘)就执行对应的硬件中断函数,当触发了软中断(各种系统调用getpid(), open()),就调用软件中断表中的对应函数。
总结:操作系统,就是基于中断的可执行程序。