1.认识信号
1.1信号的概念
信号是进程之间事件异步通知的一种方式,属于软中断
就是内核可以随时给进程发信号,进程收到后要么默认处理、要么忽略、要么自己写代码处理。生活例子:你正在写文档(正常执行),突然:
有人拍你肩膀(中断)→ 你回头看一下(默认动作:终止/暂停)
手机闹钟响了(闹钟信号)→ 你关掉它(可能自定义处理)
有人喊“着火了”(紧急信号)→ 你立刻跑(不可忽略的强信号)
这些都像 Linux 的 SIGINT、SIGALRM、SIGKILL。
在Linux中使用kill -l就可以看到所有的信号,我们把前31个称为普通信号(也称为不可靠信号),这些信号都是大写的宏,前面的数字就是宏的对应值

使用man 7 signal往下翻就可以看到一张表,包含了信号的默认动作,大多数都是终止。这里说到了man,man就是Linux的手册,而7号手册就可以用来查信号。

2.信号的产生
2.1通过终端按键产生信号
很多人可能都使用过ctrl+c直接退出一个进程,但是很多人不知道原理,其实这就是信号。我们来梳理一下这个过程,这个过程如下:
1.键盘按键产生硬件中断,终端驱动程序捕获到中断;
2.终端驱动程序将你所输入的按键转化为对应的信号;
3.终端将信号发送给了操作系统,告诉操作系统,你要向该进程发送某个信号;
4.操作系统接收到请求后,找到当前进程,修改PCB中的未决信号集,完成信号产生;
这里有一个关键规则:终端组合键产生的信号,只能发送给当前前台进程。后台进程(通过&启动的进程)无法接收终端组合键产生的信号,这是为了避免后台进程被用户误操作中断。
通过终端按键产生信号就是:ctrl+c(信号2),ctrl+\(信号3),内核向进程发送信号,然后进程做出对应的处理,这两个信号都是终止进程用的
2.2使用系统命令向进程发送信号
kill命令底层实际调用kill()函数来向进程发送信号,比如就是 kill -9 pid,这个pid就是进程的编号,就像是每个人(比喻为进程)上学都有一个学号,这个pid就像是我们每个学生的学号。kill -9 pid就是终止这个pid对应的进程

2.3使用函数产生信号
除了通过终端和命令,我们还可以在 C/C++ 代码中调用特定函数,主动产生信号并发送给进程。Linux 系统提供了三个核心函数:kill()、raise()、abort(),分别用于 “向指定进程发送信号”、“向当前进程发送信号”、“强制当前进程异常终止”。
2.4由软件条件产生信号
软件条件触发是指:信号的产生源于程序的运行状态或软件逻辑,而非用户操作或硬件异常。
这里举个例子:就是设计一个定时器,然后时间一到,直接给对应的进程发送信号,这就是由软件条件产生信号
2.5硬件异常产生信号
硬件异常触发是指:信号的产生源于 CPU 或其他硬件设备的错误,如除零操作、非法内存访问、总线错误等。硬件检测到错误后,会通知 OS,OS 将其映射为对应的信号,发送给当前进程。
这类信号的本质是:硬件错误通过 OS 转换为软件层面的信号,让进程有机会处理错误(如打印日志、保存数据),若不处理则执行默认动作(通常是终止进程并生成 Core Dump)。
这里举个例子:除0操作->触发SIGFPE信号(8号信号)
当进程执行 “除以零” 的算术运算时,CPU 的运算单元会检测到该错误,通知 OS,OS 将其映射为SIGFPE信号(Floating-point exception,浮点异常),默认处理动作是 “终止进程并生成 Core Dump”。
#include
#include
#include
using namespace std;
// 自定义SIGFPE信号处理函数
void sigfpe_handler(int signum)
{
cout << "捕获到信号:" << signum << "(SIGFPE),发生除零错误!" << endl;
// 退出进程(避免无限循环触发信号)
exit(1);
}
int main()
{
cout << "进程PID:" << getpid() << ",尝试执行除零操作..." << endl;
// 注册SIGFPE信号的处理函数
signal(SIGFPE, sigfpe_handler);
sleep(1); // 延迟1秒,便于观察
// 执行除零操作
int a = 10;
int b = 0;
int c = a / b; // 除零错误,触发SIGFPE信号
// 以下代码不会执行
cout << "计算结果:" << c << endl;
return 0;
}
运行这段代码,终端输出如下

这里需要主义的是,为何会一直触发信号?
如果我们不在处理函数中退出进程,会发现 SIGFPE 信号会被无限触发。原因是:除零错误发生后,CPU 的状态寄存器会记录该错误状态,若不清理该状态,OS 会持续检测到错误,不断发送 SIGFPE 信号。
因此,在处理 SIGFPE 信号时,通常需要在处理函数中调用exit()或_exit()终止进程,避免无限循环。
2.6总结思考
1.上⾯所说的所有信号产⽣,最终都要有OS来进⾏执⾏,为什么?因为OS是进程的管理者
2.信号的处理是否是⽴即处理的?不是,是在合适的时候
3.信号如果不是被⽴即处理,那么信号是否需要暂时被进程记录下来?记录在哪⾥最合适呢?需要被记录下来,这个会记录到进程的PCB中的未决信号集合,我们可以把他理解为一个待办事项薄,专门用于挂起那些已经到达,但尚未传递给进程的信号。
4.⼀个进程在没有收到信号的时候,能否能知道,⾃⼰应该对合法信号作何处理呢?能知道,在信号没到来之前,对每个信号如何处理,进程都是知道的,作何处理也是知道的
5.如何理解OS向进程发送信号?能否描述⼀下完整的发送处理过程?
这个过程我们来详细梳理一下:
1.信号的产生,可以是
硬件异常:缺页,非法指令,除零->内核生成对应信号
终端按键:Ctrl+C->驱动产生SIGINT信号
系统调用:kill(),tkill(),raise()
2.内核找到目标进程的task_struct(PCB)
通过进程的pid找到PCB,然后调查调用者权限
3.信号的过渡
如果信号是SIGKILL或者是SIGSTOP->强制处理,不能忽略或者堵塞
如果信号是忽略,那么直接丢弃信号,不记录
4.将信号记录到未决信号集
每个进程的PCB中都有位图,用来记录那些普通信号正在未决,这一步之后,信号就是处于未决状态,从用户态看起来,此时信号还没被处理,只是内核知道了。
5.如果进程是沉睡的(可中断睡眠),且信号是不会被堵塞的,需要唤醒进程。到此发送动作在内核中已经就完成了。
6.到这里就是信号的递达和处理阶段(发生在进程返回用户态前)
时机是:内核态--->用户态切换时
内核查看blocked掩码:每个进程都有一个blocked掩码
根据信号的处理动作执行信号
3.信号的保存
一个信号从产生到消失,会经过几个状态:
- 生成:内核在目标进程的 pending 位图中记录(并可能唤醒进程)。
- 未决(pending):信号被“保存”在内核中,尚未递达到进程。
- 递达(delivered):内核将信号交给进程处理(执行默认动作或用户函数)。
- 清理:处理后,内核清除 pending 中对应的位(实时信号出队)。
所以“信号的保存”实质上就是指在未决阶段暂存在内核数据结构中。
信号的保存位置是每个进程内核控制块中的未决位图和队列(pending),以及阻塞掩码(blocked);用户通过 sigpending 查看“已保存但未递达”的信号,通过 sigprocmask 控制信号的“保存期”。
4.可重入函数
什么是可重入?一个函数如果满足一下条件,就是可重入的:
只使用局部变量(栈上分配),或对全局/静态变量的访问是原子的或已经通过锁保护(但在信号处理函数中锁本身也危险)。
不调用任何不可重入的函数(例如 malloc、printf、fopen 等)。
不修改自己的代码(现代系统很少见),也不修改其他共享数据结构除非通过原子操作。
为啥需要可重入函数呢?你之前已经了解了信号捕捉:当进程收到信号时,内核会中断主程序,转去执行信号处理函数。而信号处理函数与主程序共享同一进程地址空间,所以它们可能访问相同的全局变量、堆区或文件描述符。
问题:如果主程序正在执行一个不可重入函数(比如 malloc 正在修改堆的空闲链表),此时信号来了,处理函数里又调用了 malloc,就会导致堆链表崩溃 —— 因为 malloc 内部维护的静态状态被第二次调用破坏。
因此:信号处理函数中只能调用“可重入”且“异步信号安全”的函数。
这里有一句话:一个线程安全函数不一定是可重入的;但一个可重入函数一定是线程安全的,说明可重入不等于线程安全

Comments NOTHING