在写这篇文章前现给大家明确一个事情:我讲的信号和进程间通信的信号量不是一个东西
先给大家四个基本结论,在后续的讲解过程中就会用到和解析这几个基本点:
信号处理,进程在信号没有产生的时候,早就知道信号该如何处理了
信号的处理,不是立即处理,而是可以等一会在处理,合适的时候,进行信号的处理
人能识别信号,是提前被"教育"过的,进程也是如此,进程早已经内置了对于信号的识别和处理方式
信号源非常多 –> 给进程产生信号的,信号源,也非常多
一.理解认识信号
1.1生活角度的信号
- 你在⽹上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的⾏为并不是⼀定要⽴即执⾏可以理解成“在合适的时候去取”。
- 在收到通知,再到你拿到快递期间,是有⼀个时间窗⼝的,在这段时间,你并没有拿到快递,但是你知道有⼀个快递已经来了。本质上是你“记住了有⼀个快递要去取”
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。⽽处理快递⼀般⽅式有三种:1. 执⾏默认动作(幸福的打开快递,使⽤商品)2. 执⾏⾃定义动作(快递是零⻝,你要送给你的⼥朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开⼀把游戏)
- 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
在生活中也存在着很多信号:比如闹钟、电话铃响、红绿灯,狼烟,脸色,肚子疼等等
两个问题:
第一个,为什么我们能认出红绿灯或者闹钟呢?
说白了就是因为有人教过我们,告诉我们那是什么东西,然后我们就记住了。
第二个,假如身边没有闹钟,那我们知道闹钟响了之后要干嘛吗?
肯定是知道的,因为以前有人教过我们,不光说了它是什么,还说了为什么要用它,以及听到之后该怎么做。这两个问题其实就对应了 “是什么”和“怎么办”。至于“为什么”,就是说我们之所以需要认识闹钟,是因为我们需要被提醒。
所以我觉得,“是什么”和“怎么办”这俩加起来,就是人能识别信号的关键。
把这个放到操作系统里看,感觉挺像的。操作系统(OS)就像社会,进程就像人。社会里有很多信号围着人转,操作系统里也有很多信号围着进程转。所以进程也得能认得出各种各样的信号。
其实我想说的就是,进程不光能认识信号,而且不管信号来没来,它都知道该怎么处理。就像我们就算手边没闹钟,也知道闹钟响了要起床一样,提前就知道该干嘛。
1.2技术应用角度的信号
先看一个具体的场景:我们在 Shell 里启动了一个前台进程(比如运行一个程序)。
然后我们按下 Ctrl+C,想把它停下来。这个过程其实是这样的:
键盘被按下 –> 产生了一个硬件中断
操作系统捕捉到这个中断 –>把它解释成一个信号
操作系统把这个信号发给正在运行的那个前台进程
进程收到信号 –> 执行对应的处理动作(比如退出)
一个代码样例:
// sig.cc
#include <iostream>
#include <unistd.h>
int main()
{
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}

- ⽤户输⼊命令,在Shell下启动⼀个前台进程
- 按下 Ctrl+C ,这个键盘输⼊产⽣⼀个硬件中断,被OS获取,解释成信号,发送给⽬标前台进程
- 前台进程因为收到信号,进⽽引起进程退出
b.⼀个系统函数
NAME
signal – ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
signum:信号编号后⾯解释,只需要知道是数字即可
handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法
⽽其实, Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明⼀下,这⾥需要引⼊⼀个系统调⽤函数
开始测试:


问题:为什么 Ctrl+C 不能退出,Ctrl+\\ 却能?
我代码里的意思是:把 SIGINT 信号的处理方式改成了自己写的函数。
而 Ctrl+C 产生的正好就是 SIGINT 这个信号。
所以当你按下 Ctrl+C 的时候,流程是这样的:
内核收到了键盘中断,把它解释成 SIGINT 信号,然后发给你的进程。进程收到之后,查了一下之前注册的信息,发现 SIGINT 已经被你改成自定义处理了,于是就去执行你写的那个函数,打印了一句话。打印完之后,函数返回,进程回到主循环继续跑,完全没有退出的意思。
这就是为什么你按 Ctrl+C 没用,它只是在执行你给它安排的任务而已。
那 Ctrl+\\ 呢?
Ctrl+\\ 产生的是 SIGQUIT 信号,3号。代码里只改了 SIGINT 的处理方式,根本没有动 SIGQUIT。所以当进程收到 SIGQUIT 的时候,查不到自定义处理,就执行默认动作。而 SIGQUIT 的默认动作就是终止进程并生成 core 文件,所以进程直接退出了。
补充:进程崩溃的时候,操作系统也会拍一张"快照",把进程死掉那一刻的内存内容、寄存器状态、调用栈等信息全部 dump 到硬盘上,存成一个文件,这个文件就叫 core 文件。
知识补充:
补充一:进程早就知道该怎么处理信号了
进程在启动的时候,内核已经把每个信号的默认处理方式告诉它了。比如 SIGINT 默认是终止,SIGQUIT 默认是终止+产生 core 文件。然后用 signal 函数可以提前改掉某个信号的处理方式。
所以上面代码里,程序一跑起来,signal(SIGINT, handler) 就已经告诉系统"以后收到 SIGINT 就调 handler,别走默认了"。信号来不来的,这个规则已经定好了。
补充二:信号来了不一定要马上处理
按 Ctrl+C 的时候,信号确实产生了,但会不会立刻执行 handler?不一定。
进程可能正在执行系统调用,比如 sleep 或者 read,这时候信号被标记为"待处理",等进程从内核态返回用户态的时候,才会去检查有没有信号 pending,有的话才去执行对应的动作。
所以信号处理是有延迟的,不是硬中断那种"说停就停"的响应方式,而是等一个合适的时机再去办。
补充三:进程天生就认识这些信号
人能识别危险是因为学过,进程能识别信号是因为内核天生就给它装好了这套规则。什么信号是什么意思,默认该干啥,进程一启动就门儿清。signal 只是改了其中某几个的处理方式而已,底层的信号机制一直都在。
补充四:Ctrl+U 为啥能退出?
但我试了一下ctrl+u可以修改,说白了就一句话:Ctrl+C 对应的信号被你改了,Ctrl+U 对应的信号没改。
-
Ctrl+C 产生 2 号 SIGINT,代码里把它改成了自定义 handler,所以只会打印一句话,进程不退。
-
Ctrl+U 产生 3 号 SIGQUIT,代码里没改它,还是默认行为,所以进程直接退出。
-
如果把 signal(SIGINT, handler) 改成 signal(SIGQUIT, handler),那 Ctrl+U 也会退不了,Ctrl+C 反而能杀掉进程。
你改哪个,哪个就失效;没改的,默认动作照常生效。
补充五:不要把 Ctrl+U 和 Ctrl+\\ 弄混
-
*Ctrl+* 产生的是 SIGQUIT(3 号信号),默认终止+产生 core 文件。
-
Ctrl+U 是终端驱动的行编辑功能(删除光标前的内容),跟信号没有关系,它是一个独立的终端功能,所以在任何情况下 Ctrl+U 都能正常工作,跟信号捕捉无关。
注意的点
第一,Ctrl+C 产生的信号只能发给前台进程。 如果我们在命令后面加个 &,这个进程就会被放到后台运行。这时候 Shell 不用等它结束,可以继续接收新命令、启动新进程。后台进程收不到 Ctrl+C 的信号,所以按 Ctrl+C 对它没用。
第二,Shell 可以同时跑一个前台进程和很多个后台进程。 但只有前台进程能接收到像 Ctrl+C 这种控制键产生的信号。后台进程在偷偷干活,不受这些按键影响。
第三,前台进程在跑的时候,用户随时可能按下 Ctrl+C。 这意味着进程的代码执行到任何位置,都有可能突然收到信号然后终止。所以说,信号相对于进程的正常执行流程,是异步的——就是说它不按顺序来,不一定等进程执行完某个操作才到,而是随时可能插进来。你没法预测它什么时候来,就像你不知道快递什么时候突然敲门一样。
同步和异步
场景是正在上课,老师的快递到了,老师让张三去取,但有两种情况;
同步:我们自习一会,等张三回来再讲。 –> 说明这件事得等张三回来才能继续,顺序是固定的,你等着就行了,知道他回来之后会发生什么。
异步:我们继续上课,不等张三,让他自己去取快递。 –>说明我们该干嘛干嘛,张三的事是突然插进来的,不按顺序走,你没法预料他什么时候回来。
对应放到信号里:
信号是异步的,就跟这个例子里的“张三取快递”一样:
进程正常跑着自己的代码,突然信号来了(就像张三突然要去取快递),进程就得停下来去处理信号,处理完再接着干活。 但进程不知道信号啥时候会来,就像你不知道张三啥时候会突然离开教室一样。
1.3信号概念
信号是一种事件的异步通知机制(属于软中断),专门发给进程的。
拆开来看:
事件:发生了某件事(比如按了Ctrl+C、定时器到了、别的进程发了消息)
通知:操作系统告诉进程“有件事发生了”
异步:进程不知道这件事啥时候会来,随时可能被打断
机制:就是一种方式、一套规则
发给进程:信号不是发给别人的,就是发给进程的
1.4 kill -l查询系统支持的所有信号列表
man 7 signal指令:查看第 7 章节中关于信号 (signal) 的官方文档

信号的分类:
仔细数一下会发现,不是64种,因为中间缺了32和33号,所以实际一共是 62种信号。
它们被分成了两类:
-
1~31号:叫做普通信号
-
34~64号:叫做实时信号(名字里都带RT两个字母)
实时信号响应特别快,优先级很高,像着火了一样必须立刻处理。后面我们主要关注普通信号,实时信号知道有这个东西就行了,类比一下就像每天的闹钟,响了就得处理,但稍微慢一点点也问题不大。
信号的编号和名字

每个信号都有一个数字编号和一个宏定义名字,比如:
-
2号叫 SIGINT(就是我们按Ctrl+C产生的那个信号)
-
9号叫 SIGKILL(强制终止进程)
-
14号叫 SIGALRM(闹钟定时信号)
这些名字其实是在系统头文件 signal.h 里定义的,SIGINT 和写 2 是一个意思,但用名字更直观。
#define SIGINT 2
信号的默认处理方式
每个信号被收到后,进程默认会怎么处理呢?文档里给了五种默认动作(参考下方的说明):

表格说明:
| Term | 终止进程 |
| Ign | 忽略信号,啥也不干 |
| Core | 终止进程并生成核心转储文件(用于调试) |
| Stop | 暂停(挂起)进程 |
| Cont | 如果进程当前是暂停状态,就继续运行它 |
比如我们熟悉的:
-
SIGINT(2号)默认是 Term,终止进程
-
SIGKILL(9号)默认也是 Term,强制终止
-
SIGCHLD(17号)默认是 Ign,忽略它
1.5信号处理常见方式概览
除了使用系统默认的处理动作,我们还可以自己定义信号的处理方式,一般有三种可选方案(后面讲 sigaction 函数时会详细介绍):
第一种:忽略信号
就是告诉操作系统:这个信号来了我不想管,你直接忽略掉就行。
代码里这样写:
signal(SIGINT, SIG_IGN);
SIG_IGN 是一个宏,意思就是 Ignore,忽略。
运行效果:按下 Ctrl+C 之后,进程啥反应没有,继续正常运行,因为 SIGINT 信号被忽略了。
第二种:默认处理
就是让操作系统按照系统默认的方式去处理。每个信号都有自己的默认动作,比如 SIGINT 的默认动作是终止进程。
代码里这样写:
signal(SIGINT, SIG_DFL);
SIG_DFL 也是一个宏,意思就是 Default,默认。
运行效果:按下 Ctrl+C 之后,进程直接退出,这就是默认行为。
第三种:自定义捕捉
就是我们上一节一直在用的方式,自己写一个函数,告诉操作系统:这个信号来了之后,别按默认的来,去执行我写的这个函数。
代码里这样写:
void handler(int signumber)
{
// 你想做的事情
}
signal(SIGINT, handler);
运行效果:按下 Ctrl+C,进程不会退出,而是执行你写的 handler 函数,打印一句话或者干点别的。
(1) 信号的产生
信号的产生方式主要有两种;
第一种是用 kill 命令。比如在终端里输入 kill -2 PID,就是给指定进程发送一个 2 号信号,也就是 SIGINT。这跟我们按 Ctrl+C 本质上是一回事。
第二种就是键盘操作。最常见的莫过于 Ctrl+C 了,按下之后系统就给我们发了一个 SIGINT。
所以不管你用哪种方式,最终结果都是同一个——信号产生了,准备发给某个进程。
(2)信号的识别
进程收到信号后会立刻处理吗?
答案是不一定。
进程收到信号之后,往往不会马上就处理,而是先记下来,等到了一个"合适的时机"再去处理。这就好比你正在打游戏,快递员打电话说快递到了,你不会立刻冲下去拿,而是会说"好,等我打完这一局再去"。
那为什么信号不能立即处理呢?原因其实很简单:信号产生的时刻是无法预料的,它可能在进程执行任何一行代码的时候突然到来。如果进程正在做一些关键的事情,比如写文件、处理重要数据,这时候被中断可能会出问题,所以进程选择先把信号存起来,等手头的事情告一段落再处理。
这个"合适的时机"后面会细说,这里先记住一个点:信号不是来一个处理一个,而是排队等着被处理。
(3)信号的处理
处理方式有三种,前面其实已经介绍过了:
第一种是默认处理。每个信号都有自己的默认行为,大部分是终止进程,但也有些是别的功能,比如暂停进程或者继续运行。
第二种是忽略信号。就是信号来了,啥也不干,当没发生过。这其实也算是一种处理方式。
第三种是自定义捕捉。就是自己写一个函数,告诉系统:这个信号来了,别按默认的来,去执行我写的那个函数。
用闹钟来类比一下应该更好理解:闹钟响了,你起床了,这是默认行为;闹钟响了,你关掉继续睡,这是忽略;闹钟响了,你起来跳个舞,这就是自定义捕捉。不同的人对同一个信号可以有不同的处理方式,进程也是这个道理。
1.6信号的本质
前面我们把信号的产生、识别、处理都过了一遍,现在回过头来问一个核心问题:信号到底是个什么东西?它在计算机里长什么样?存在哪?怎么存的?
1.信号为什么需要被保存?
先说一个关键点:信号不是立即处理的。
前面举过例子,你在打游戏的时候快递到了,你不会立刻冲下去,而是先记着"有快递这回事",等打完这局再说。
进程也是一样。信号可能在进程执行任何一行代码的时候突然到来,如果进程正在干要紧的事,比如写文件、拷贝数据,这时候被中断可能会出问题。所以进程的选择是:先记住有信号来了,等合适的时候再处理。
既然要"先记住",那就必须有个地方来存这个信号。那信号存在哪呢?
2.信号存在哪?
信号是发给进程的,不是发给硬件也不是发给网络的,所以信号一定保存在进程自己的地盘里,也就是进程控制块,也就是我们常说的 task_struct。
每个进程都有自己的一份 task_struct,里面记录了进程的各种信息。信号就存在这里面的一个字段里。
3.信号怎么存的?
存信号的方式,用的是位图。
什么是位图?简单来说就是用比特位来表示某种状态。比如你有31个信号,就用31个比特位,每个位代表一个信号。
-
比特位的位置 –> 代表是几号信号
-
比特位的内容 –> 1 表示这个信号来了,0 表示没来
所以实际上,task_struct 里只需要一个 unsigned int 类型(00000000 … 00000000)的字段就够了,正好32个比特位,用来表示31个普通信号绰绰有余。
这就像你有一个通知栏,来了什么消息就亮起对应的图标,一看就知道哪些消息还没处理。

4.发送信号的本质是什么?
弄清了信号存在哪、怎么存的,那"发送信号"到底是在干什么?
发送信号的本质,就是操作系统去修改目标进程 task_struct 里的信号位图,把对应的比特位从 0 改成 1。
就这么简单,不是什么神秘的操作,也不是传输什么数据包,就是改了一个比特位。
5.为什么只有操作系统能发信号?
既然发送信号就是改位图,那谁有资格去改呢?只有操作系统。
因为 task_struct 是属于内核的数据结构,用户进程是没有权限直接访问的。只有操作系统这个"系统资源的管理者"才有资格去读写它。所以不管信号是怎么产生的,最终真正把信号"发出去"的,一定是操作系统。
6.所有信号最终都是操作系统发的
我们前面说过信号产生的几种方式:
用 kill 命令发信号,kill 命令本身是在 bash 里执行的,它的底层会调用操作系统提供的系统调用接口,通过这个接口去修改目标进程的位图。
按 Ctrl+C 发信号,键盘是硬件,操作系统是硬件的管理者,键盘产生的中断一定先被操作系统拿到,然后由操作系统解释成信号,再写进目标进程的位图。
程序里调用 kill 函数发信号,也是一样的道理,最终还是落到系统调用上。
所以不管信号从哪来、怎么来的,殊途同归:
所有信号的产生,最终都一定要经过操作系统,由操作系统来把信号写进目标进程的位图里。
7.信号从产生到处理的完整过程
第一步:初始状态
一开始,进程 A 正在正常运行。它的 task_struct 里面有一个信号位图,这个时候所有比特位都是 0,表示没有任何待处理的信号。进程 A 继续干自己的活,什么都不知道。
第二步:信号产生(按下 Ctrl+C)
突然,有人按下了 Ctrl+C。这个操作想要终止进程 A。
键盘被按下之后,首先产生的是一个硬件中断。操作系统作为硬件的管理者,第一时间捕获到了这个中断。操作系统拿到键盘发来的数据之后,解读了一下,发现这是 Ctrl+C,对应的是 SIGINT 信号,也就是 2 号信号。
第三步:信号发送(操作系统改位图)
于是操作系统开始找进程 A 的 task_struct,找到了之后,把里面信号位图的第 2 个比特位从 0 改成了 1。
这一步就是所谓的"发送信号"。说白了就是改了一个比特位。
第四步:信号未决(Pending 状态)
这个时候,进程 A 还在正常跑着,它自己并不知道自己的位图已经被改了。信号现在是"待处理"状态,也就是前面说过的 Pending 状态。信号已经来了,但进程还没处理它。
第五步:信号识别(检查位图)
过了一段时间,进程 A 运行到了一个"合适的时机"。这个"合适的时机"通常是从内核态返回用户态的时候,比如系统调用结束了、中断处理完了之类的情况。
这时候进程 A 会检查一下自己的信号位图,发现有比特位是 1,说明有信号在等着。它一看第 2 位是 1,就知道来的是 SIGINT。
第六步:信号处理
然后进程 A 就去执行对应的处理动作。至于具体怎么处理,取决于之前注册的是默认、忽略还是自定义函数。
第七步:处理完毕(清空位图)
处理完之后,进程 A 把信号位图的第 2 位从 1 改回 0,表示这个信号已经处理完了,可以清掉了。然后进程 A 继续正常执行后面的代码
简单小结一下
信号就是一个进程里的比特位。发送信号就是操作系统把这个比特位从 0 改成 1。处理信号就是进程把这个比特位从 1 改成 0。
二.产生信号
1.通过终端按键产⽣信号
Core Dump
1.Core Dump 是啥?
Core Dump 可以理解为操作系统给崩溃的进程拍了一张“死亡快照”。进程在崩溃的那一刻,内存里有什么数据,当前执行到哪条指令,函数调用关系是怎样的,CPU 寄存器里存了什么值,通通被 OS 打包成一个文件存到磁盘上,这个文件就叫 core 文件。
举几个会触发 Core Dump 的常见操作:
-
野指针解引用:int *p = nullptr; *p = 100;
-
除零操作:int a = 10 / 0;
-
栈溢出:递归函数忘记写终止条件
-
使用 abort() 函数主动触发异常终止
终端里如果出现了 (core dumped) 这几个字,就代表 core 文件生成成功了。配合 gdb 调试工具,可以直接定位到崩溃的那一行代码。
2.那平时我们在写代码的时候为啥我从来没见过 core 文件?
因为系统默认是关着的。Linux 对进程能使用的资源有限制,core 文件大小默认是 0,也就是不让生成。
用命令看一下:
ulimit -c
输出是 0,说明 core 文件被禁用了。需要手动打开:
ulimit -c unlimited # 不限制大小
这个设置只对当前终端有效,关了窗口就没了。如果觉得 unlimited 太野了,也可以指定具体大小:
ulimit -c 1024 # 最大 1M(单位 KB)
ulimit -c 10240 # 最大 10M

想看一眼所有资源限制的话:
ulimit -a
会输出一堆东西,包括栈大小、最大进程数等等。core 文件限制在“core file size”那一行。有些 Linux 发行版在执行 ulimit -c unlimited 时会报错 unlimited: invalid number,这时候直接改成具体数字就行了:
ulimit -c 1024
3.写个程序验证一下
写一段肯定会崩的代码:
#include <iostream>
int main()
{
int* p = nullptr;
*p = 100; // 野指针访问,触发段错误
return 0;
}
先不开 core,会只显示“段错误”,没有 (core dumped),当前目录下也没有 core 文件。符合预期,因为 ulimit -c 是 0,被禁用了。
如果ulimit -c已经被修改了,那么结果就是:

然后打开 core:
ulimit -c unlimited
./test
段错误 (core dumped)
这次就出现了 (core dumped),然后用 ls 看一眼,多了一个 core 文件:


刚开始我用ls的时候发现没反应,然后发现这个是根据路径找到错因,我的 core 文件被 Ubuntu 的 Apport 接管了,没有放在当前目录。
所以我首先确认一下 Apport 服务是不是开着:
sudo service apport status

我这个显示没有开,但如果是 running 状态,那就没有关闭会接管。看我刚才那种情况下 core 文件被 Apport 接管,不会直接生成在当前目录。我用这种的方式解决;临时关闭这个Apport服务:
sudo service apport stop
ulimit -c unlimited
./test
关掉 Apport 之后,程序崩溃就会在当前目录生成 core 文件。

不同系统 core 文件命名方式可能不一样,有的是 core,有的是 core.进程PID,可以自己设置命名格式。
然后用 gdb 加载 core 文件,直接看崩溃位置:
gdb test core.ID号
进去之后输入 bt(backtrace 的缩写)查看函数调用栈,就能直接看到崩溃在哪一行:
一下子就定位到了,原来排查段错误可以这么方便。以前遇到段错误全靠打印日志猜,现在不用了,core 文件一看就知道崩在哪。
4.Core Dump 标志位
在 wait/waitpid 的 status 里,bit7 就是 Core Dump 标志位Core Dump 标志位(1 = 生成 core 文件,0 = 没有),记录进程崩溃时有没有生成 core 文件,直接看这张表就清楚了:

| bit0 – bit6 | 终止信号编号 | 进程是被哪个信号干掉的(比如 11 是 SIGSEGV) |
| bit7 | Core Dump 标志 | 1 表示产生了 core 文件,0 表示没有 |
| bit8 – bit15 | 退出码 | 进程正常退出时的返回值(exit(0) 这种) |
WCOREDUMP(status) 就是帮你检查 bit7 是 0 还是 1,不用自己去写位运算。
实验一:waitpid 解析 status,识别 Core Dump 标志位
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t pid = fork();
if (pid == 0)
{
// 子进程:故意崩溃
int* p = nullptr;
*p = 100;
exit(0);
}
else if (pid > 0)
{
int status;
pid_t ret = waitpid(pid, &status, 0);
if (ret == pid)
{
cout << "子进程退出了" << endl;
if (WIFEXITED(status))
{
cout << "正常退出,退出码: " << WEXITSTATUS(status) << endl;
}
else if (WIFSIGNALED(status))
{
cout << "被信号杀死,信号编号: " << WTERMSIG(status) << endl;
if (WCOREDUMP(status))
{
cout << "发生了 Core Dump,生成了 core 文件" << endl;
}
}
}
}
return 0;
}

WCOREDUMP(status) 这个宏就是用来检查 bit7 的。如果进程崩溃时生成了 core 文件,这个宏返回真,否则返回假。
SIGSEGV 编号是 11,段错误,core 文件生成后 WCOREDUMP 返回 1,bit7 被置 1。这些位操作不用自己写,用宏就行了,系统给封装好了直接用。
函数作用:
| WIFEXITED(status) | 判断进程是否正常退出(调用 exit 或者 return) |
| WEXITSTATUS(status) | 获取正常退出时的退出码(0 表示成功) |
| WIFSIGNALED(status) | 判断进程是否被信号干掉的 |
| WTERMSIG(status) | 获取干掉进程的信号编号 |
| WCOREDUMP(status) | 判断是否产生了 core 文件 |
实验总结
由此可见:终止信号 和 core dump 标志是两组独立信息。 无论能不能生成 core 文件,只要进程被信号杀死,低 7 位一定记录对应的信号编号;core 标志仅仅代表是否成功生成快照。
5.线上环境为什么不开 Core Dump?
一开始觉得 core 文件这么好用,为啥不一直开着?
后来想明白了一个场景:假设线上某个服务有 bug,不断崩溃又不断被守护进程重启。每次崩溃都生成一个 core 文件,少则几十 MB,多则几百 MB。没过多久磁盘就满了,服务器连不上,业务直接挂掉。
所以线上环境一般默认关闭,只在本地调试的时候临时开一下。如果线上实在需要排查问题,用 gdb 直接挂载正在运行的进程进行调试可能更稳妥。
如果已经产生了 core 文件,用 gdb 加载查看,排查完后记得及时删掉,别占着磁盘空间。
6.补充:命名方式
Core 文件的命名格式可以通过系统参数修改:
cat /proc/sys/kernel/core_pattern
默认一般是 core 或者 core.%p(%p 是进程 PID)。如果是 core.%p,生成的文件名就是 core.1234 这种格式,方便区分是哪个进程产生的。
实验 二:持续运行信号测试程序
文件名:signal.cc
#include <iostream>
#include <unistd.h>
#include <signal.h>
// 信号捕获回调
void handler(int sig)
{
std::cout << "进程捕捉到了一个信号,正在处理中:"
<< sig << " Pid: " << getpid() << std::endl;
}
int main()
{
// 捕获 Ctrl+C(2:SIGINT) Ctrl+\\(3:SIGQUIT)
signal(SIGINT, handler);
signal(SIGQUIT, handler);
// 不要捕获 SIGFPE(8),捕获后将不会触发core转储
pid_t pid = getpid();
while(true)
{
std::cout << "我是一个进程,我正在运行…, Pid: " << pid << std::endl;
sleep(1);
}
return 0;
}
编译如下:
g++ -g signal.cc -o mysignal
./mysignal
1.程序持续打印 PID。
2. 测试 1:按下 Ctrl+C(信号 2 SIGINT) 输出:进程捕捉到了一个信号,正在处理中:2 Pid: xxx 进程不会退出,继续循环运行。
3. 测试 2:
按下 Ctrl+\\(信号 3 SIGQUIT) 输出:进程捕捉到了一个信号,正在处理中:3 Pid: xxx 进程不会退出,继续循环运行。


4. 测试 3:新开终端执行
开一个新的终端:
kill -8 你的进程PID

使用ls查看目录,可以看到自动生成 core.PID 文件,后缀数字对应崩溃进程 PID。多次重复发送信号,会生成多个独立 core 快照文件。

对进程注册捕获处理的信号(2 号 SIGINT、3 号 SIGQUIT),信号到来时执行我们自定义的回调函数,默认不会杀死进程; 没有设置捕获的 SIGFPE (8 号) 属于具有 Core Dump 属性的致命信号,触发信号默认处理行为:终止进程,条件满足时生成 core 文件。

重点分析: SIGFPE 是外部异步发送的信号,信号可以在程序任意运行时刻递达。本次信号送达时程序正好处于sleep休眠阶段,因此崩溃现场定格在休眠函数内;不代表 sleep 函数出错
实验清理小命令,批量删除所有 core 文件:
rm -f core.*
总给:
补充一下:什么是 Term?
Term 是 Terminate 的缩写,表示终止进程。进程收到 Action 为 Term 的信号后,直接结束,不会生成任何额外文件。
信号表中的 Term 信号:
| SIGHUP | 1 | Term | 终端挂起 |
| SIGINT | 2 | Term | Ctrl+C 中断 |
| SIGKILL | 9 | Term | 强制杀死 |
| SIGTERM | 15 | Term | 默认终止信号 |
| SIGPIPE | 13 | Term | 管道破裂 |
Term 信号的共同特点就是:进程死了就死了,什么也没留下。你只知道进程没了,但不知道它死之前发生了什么、执行到哪一行代码出的问题。
Core 和 Term 对比总结
| 全称 | Terminate | Core Dump |
| 进程是否终止 | 终止 | 终止 |
| 是否生成 core 文件 | 不生成 | 生成 |
| 能否事后调试 | 不能 | 可以用 gdb 分析 |
| 典型信号 | SIGINT(2)、SIGTERM(15)、SIGKILL(9) | SIGSEGV(11)、SIGABRT(6)、SIGFPE(8) |
| 使用场景 | 正常终止、用户主动杀死 | 程序异常崩溃、需要定位 bug |
两种触发 Core Dump 的硬件异常 + 事后调试
1.除零异常 SIGFPE (8)
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void catchSig(int signum)
{
cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
}
int main()
{
signal(SIGINT, catchSig);
signal(SIGQUIT, catchSig);
while(true)
{
cout << "我是一个进程,我正在运行…, Pid: " << getpid() << endl;
sleep(1);
int a = 100;
a /= 0; // 主动制造除零算术异常
cout << "run here…" << endl;
}
return 0;
}
进行编译:

输出:Floating point exception (core dumped),目录生成 core.PID 文件。
这里有警告不要担心直接进行下一步操作
事后调试语法
#方式一:终端一步直接加载(推荐,简洁)
在黑色系统终端输入,不要进入 gdb 交互:
gdb ./mysignal core.ID
方式二:先进 gdb,再加载 core 文件(截图上方正确示例)
1. 终端先进入 gd
gdb ./mysignal
2. 在 `(gdb)` 提示符内使用专属命令 `core-file`
(gdb) core-file core.ID
操作区分总结
外部 Shell:可用 `gdb 程序 core文件` gdb 交互内部:只能用 `core-file 文件名`,不能再写`gdb`

1.什么是事后调试
程序已经崩溃、进程已经彻底退出,无法再复现崩溃现场时,依靠提前生成的core快照文件,重新还原崩溃瞬间全部运行信息,这种程序崩溃后再回头定位 bug的方式,就叫事后调试。
2.我代码里每一处对应的事后调试体现
3.补充完整逻辑对比普通调试
- 普通在线调试:一边运行程序、一边打断点,程序活着才能调试;
- 事后调试:程序已经死掉,依靠崩溃时保存的 core 快照离线复盘,不需要复现崩溃场景,完全是事后追溯。
总结:本次通过`gdb ./mysignal core.3029521`加载核心转储文件实现事后调试:程序早已因除零异常崩溃退出,无需重新运行复现故障,仅依靠崩溃时生成的 core 快照,就能读出进程终止信号 SIGFPE,并直接定位到源码`a /= 0;`崩溃行,完整还原崩溃现场,这就是核心转储的事后调试作用。
2.野指针段错误 SIGSEGV (11)
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void catchSig(int signum)
{
cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
}
int main()
{
signal(SIGINT, catchSig);
signal(SIGQUIT, catchSig);
while(true)
{
cout << "我是一个进程,我正在运行…, Pid: " << getpid() << endl;
sleep(1);
// 下面这行执行直接崩溃,循环直接终止
int* p = nullptr; // 空指针
*p = 100;
cout << "run here…" << endl;
}
return 0;
}
编译运行命令
g++ -o mysignal signal.cc -g
ulimit -c unlimited
./mysignal
运行现象

终端输出:Segmentation fault (core dumped),目录生成 core.PID 文件。
事后调试语法
gdb ./mysignal core.3039676
解析如下:
事后调试体现在这里
程序早就崩溃退出了,不用重新运行程序复现段错误,只靠崩溃时自动保存的 core 快照文件,执行一行 gdb 加载命令,就能查出当初崩溃的信号类型,并且精准定位野指针出错代码行,不需要重复复现故障,这就是野指针场景下核心转储的事后调试。
总结一下:
验证进程等待中的 core dump 标志位:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
sleep(1);
int a = 100;
a /= 0; // 子进程主动制造除零异常,触发SIGFPE
exit(0);
}
int status = 0;
waitpid(id, &status, 0);
// status & 0x7F:取出低7位,代表终止信号编号
// (status >> 7) & 1:取出第7比特,core dump标记位
cout << "父进程: " << getpid() << " 子进程: " << id
<< " exit sig: " << (status & 0x7F)
<< " is core: " << ((status >> 7) & 1) << endl;
return 0;
}

目录会生成 core 文件,代表允许生成 core 快照,可用于 gdb 事后调试。


# 关闭core生成
ulimit -c 0
./mysignal

2.调用系统函数向进程发信号
(1)kill 命令
a. 接口介绍
正确定义:kill 命令是 Linux/Unix 系统下的一个用户态命令行工具,它通过调用系统调用 kill() 来向指定进程发送信号。

b.代码实现
1. 发送信号程序(mykill.cc)
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;
int main(int argc, char* argv[])
{
// 检查参数个数
if(argc != 3)
{
cout << "用法: ./mykill <信号编号> <进程ID>" << endl;
cout << "示例: ./mykill 9 1234" << endl;
return 1;
}
// 解析参数
int signum = atoi(argv[1]);
pid_t pid = atoi(argv[2]);
// 调用系统调用 kill 发送信号
int ret = kill(pid, signum);
// 判断发送结果
if(ret == 0)
cout << "发送信号 " << signum << " 到进程 " << pid << " 成功" << endl;
else
cout << "发送失败,请检查进程是否存在" << endl;
return 0;
}
2. 接收信号程序(test.cc)
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
// 信号处理函数
void handler(int sig)
{
cout << "进程 " << getpid() << " 收到信号: " << sig << endl;
}
int main()
{
// 注册信号处理函数
signal(2, handler); // SIGINT
signal(15, handler); // SIGTERM
signal(10, handler); // SIGUSR1
cout << "测试程序启动,PID: " << getpid() << endl;
cout << "请用 ./mykill 发送信号" << endl;
// 无限循环,等待信号
while(true)
sleep(1);
return 0;
}
3. 编译管理(Makefile)
all:mykill test
mykill:mykill.cc
g++ -o $@ $^ -std=c++11 -g
test:test.cc
g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
rm -f mykill test
4.编译运行
(1) 编译程序

# 方式一:编译全部
make
# 方式二:只编译 mykill
make mykill
# 方式三:手动编译
g++ -o mykill mykill.cc -std=c++11 -g
g++ -o test test.cc -std=c++11 -g
(2)运行测试
终端1 – 运行接收程序:

接下来等待终端发送信号,
终端2 – 发送信号:

(3)运行效果
终端1 输出:

终端2 输出:


mykill 通过 kill() 系统调用向进程发送信号。程序先检查参数个数,然后用 atoi() 解析信号编号和 PID,最后调用 kill() 并根据返回值输出结果。命令行程序的基本框架走了一遍:参数校验、类型转换、系统调用、结果反馈。虽然简陋,但足够说明信号发送的本质。
(2)raise函数
接口介绍

raise() 就是自己给自己发信号。
raise(8); // 给自己发 8 号信号(SIGFPE)
等价于:
kill(getpid(), 8); // 给自己发信号
代码实现:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main()
{
cout << "我开始运行了…" << endl;
sleep(1);
raise(8); // 给自己发 SIGFPE 信号
cout << "这条不会打印" << endl;
return 0;
}
///////////makefile///////
mykill:mykill.cc
g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
rm -f mykill
运行结果:

(3)abort函数
接口介绍
abort() 用于程序遇到致命错误时异常终止,发送 SIGABRT(6) 信号并生成 core dump,不可恢复(即使捕获信号最终也会退出)。适合调试和错误处理场景。


信号 Action 含义
| Term | 终止进程(正常退出) |
| Core | 终止进程 + 生成 core 文件 |
| Stop | 暂停进程(不可捕获) |
| Cont | 继续运行 |
| Ign | 忽略信号 |
关键信号总结
| SIGABRT | 6 | Core | abort() 触发 |
| SIGSEGV | 11 | Core | 野指针触发 |
| SIGKILL | 9 | Term | 强制杀死(不可捕获) |
| SIGSTOP | 17/19/23 | Stop | 暂停(不可捕获) |
代码如下:
#include <iostream>
#include <cstdlib>
using namespace std;
int main()
{
cout << "程序开始运行…" << endl;
abort(); // 异常终止
cout << "这条不会打印" << endl;
return 0;
}
运行编辑:

补充:abort() 固定发 6 号信号,raise() 可以指定信号,alarm() 延时发 14 号信号(后面会讲),三者都是给自己发信号。
如何理解系统调用接口?
系统调用接口是用户程序进入内核的入口:用户调用 kill() 等函数,通过软中断陷入内核,内核验证参数后在目标进程 PCB 中设置信号标记位,进程在合适的时机(从内核态返回用户态)检查并处理信号,执行默认动作、忽略或调用用户注册的处理函数。整个过程是异步的,信号不会立即处理,而是被"挂起"等待进程调度时机。
3.由软件条件产生信号
软件条件不是错误,是某种条件被触发时,OS 向进程发信号通知它。说白了就是:程序运行过程中遇到了某种软件层面的情况,操作系统发现了,就发个信号告诉进程"你该处理一下了"。
举个例子:管道读端关闭了,写端还在写,OS 检测到这个情况就会给写端发 SIGPIPE(13) 信号,把写进程干掉。这就好比你把水倒进一个堵住的管子,系统检测到"没人接收"这个条件,就通知你停止。
再比如 alarm(),你设置一个定时器说"3秒后提醒我",时间到了 OS 就给你发 SIGALRM(14) 信号。整个过程没有硬件参与,纯粹是软件逻辑触发的。
SIGPIPE 是典型的软件条件信号。用管道验证一下:创建匿名管道,父进程读、子进程写。父进程把读端关了,子进程还在往管道里写数据。这时候 OS 检测到"管道没有读端了但有人还在写",就给子进程发 SIGPIPE,子进程就被终止了。父进程用 waitpid 拿到退出状态,提取终止信号就能看到是 13。alarm() 也是软件条件产生信号的例子,设置一个闹钟,时间到了 OS 就给你发 SIGALRM。alarm() 还有个特点:再次调用会覆盖前一个,返回剩余秒数。传 0 就是取消闹钟。
软件条件信号常见的就这几个:
| SIGPIPE | 13 | 管道读端关了还在写 |
| SIGALRM | 14 | alarm() 定时到了 |
| SIGCHLD | 17 | 子进程退出或暂停 |
| SIGCONT | 18 | 暂停的进程被恢复 |
alarm
a.接口介绍
这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。


alarm 函数用于给进程设置定时闹钟,作用是告知内核:等待指定秒数后,向当前进程投递 SIGALRM 信号,该信号默认行为是直接终止进程。
举个生活化例子辅助理解:假设我设置 30 分钟后的闹钟,睡至第 20 分钟时被中途打断,打算重新设 15 分钟后响铃。此时第一次闹钟原本剩余的 10 分钟,就是本次 alarm 函数的返回值。
函数有一条特殊规则:若传入参数 seconds 等于 0,代表清除当前已存在的闹钟,函数依旧会返回上一轮闹钟剩余的倒计时秒数。 对应程序场景举例:代码先调用 alarm (30),程序运行 20 秒后再次执行 alarm (15),这次调用的返回结果就是上一个闹钟剩下的 10 秒;如果此时执行 alarm (0),则直接取消 15 秒的新闹钟,同时返回剩余倒计时时长。
代码如下:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
alarm(1); // 1秒后发信号终止进程
int count = 0;
while(true)
{
cout << count++ << endl;
}
return 0;
}
运行结果如下:

为什么1秒只打印了5万多?
因为cout是I/O操作,每次打印都要把数据从内存搬到屏幕,中间还要经过终端显示。在云服务器上,还涉及网络传输,速度就慢下来了。
用云服务器跑,数据要经过本地–>服务器–>网络的链路,1秒能打5万行已经不错了。
如果去掉I/O,只做纯内存累加,速度就快很多了。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int count = 0;
// 信号处理函数
void catchSig(int signum)
{
cout << "final count: " << count << endl;
exit(0);
}
int main()
{
// 1秒后发送 SIGALRM 信号
alarm(1);
// 注册信号处理函数
signal(SIGALRM, catchSig);
// 疯狂累加
while(true)
{
count++;
}
return 0;
}

b.闹钟定时性功能
每次闹钟响就打印当前累加值,然后重新设置1秒定时器,实现每秒打印一次。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
uint64_t count = 0;
void catchSig(int signum)
{
cout << "final count: " << count << endl;
alarm(1); // 重新设闹钟,继续统计下一秒
}
int main()
{
signal(SIGALRM, catchSig);
alarm(1); // 1秒后触发
while(true)
{
count++;
}
return 0;
}

程序每秒打印一次当前累加值,可以看到1秒大概能累加5亿次。按 Ctrl+C 终止程序,不终止的话会一直打印下去。
为什么每次都是5亿左右?
因为1秒内累加次数基本稳定,和机器性能有关。云服务器大概每秒5亿次,本地机器可能更高或更低
补充:uint64_t 的作用
用 uint64_t 而不是 int 是因为累加值会很大,每秒5亿,几秒就超过 int 的21亿上限了。uint64_t 能存到1844亿亿,跑多久都不会溢出。
C. 补充功能
软件条件控制逻辑
这段代码展示的是:定时器触发 –>执行一堆回调函数 → 再重新设闹钟,形成一个循环。
什么是软件条件控制逻辑?
这里的"软件条件"就是定时器到期这件事。alarm(1)设了一个软件条件:1秒后触发。时间到了,OS发信号,进程响应,执行预设的动作,然后重新设置条件,形成循环。
本质上就是:软件层面设定条件 –> OS检测条件满足 –>发信号通知 –>进程响应处理 –> 重新设定条件,如此往复。
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
typedef function<void()> func;
vector<func> callbacks;
uint64_t count = 0;
// 打印累加值
void showCount()
{
cout << "final count: " << count << endl;
}
// 打印日志
void showLog()
{
cout << "这个是日志功能" << endl;
}
// 执行 who 命令查看登录用户
void logUser()
{
pid_t pid = fork();
if(pid == 0)
{
execl("/usr/bin/who", "who", nullptr);
exit(1);
}
wait(nullptr);
}
// 信号处理函数
void catchSig(int signum)
{
for(auto &f : callbacks)
{
f();
}
alarm(1); // 重新设闹钟
}
int main()
{
// 注册信号处理函数
signal(SIGALRM, catchSig);
alarm(1); // 1秒后触发
// 添加回调函数
callbacks.push_back(showCount);
callbacks.push_back(showLog);
callbacks.push_back(logUser);
// 疯狂累加
while(true)
{
count++;
}
return 0;
}

如何理解软件条件给进程发送信号?
问:什么是软件条件给进程发送信号?
答:程序运行过程中,OS 检测到某种软件层面的条件成立了(或不再满足),就主动给相关进程发个信号通知它。这个条件不是硬件错误,是纯软件逻辑层面的。
问:能举个例子吗?
答:管道读端关闭了还在写。OS 检测到"管道无读端但有写端在写"这个软件条件,就给写端进程发 SIGPIPE(13) 信号,默认把写进程干掉。
问:检测到条件后具体怎么发信号的?
答:两步走。第一步,OS 先识别到某种软件条件触发了(比如定时器到期、管道破裂);第二步,OS 构建信号(确定发给谁、发哪个信号),然后发送给目标进程。
问:软件条件信号和硬件异常信号有啥区别?
答:软件条件信号是 OS 检测到的软件状态触发的,比如 SIGPIPE、SIGALRM,本质是逻辑层面的条件满足;硬件异常信号是 CPU 检测到的硬件错误触发的,比如 SIGSEGV(野指针)、SIGFPE(除零),本质是指令执行出错。
问:常见的软件条件信号有哪些?
答:SIGPIPE(13) 往没有读端的管道写数据、SIGALRM(14) alarm() 定时器超时、SIGCHLD(17) 子进程退出或暂停。
总结一下就是:
答:条件触发 –> OS 识别 –> 构建信号 –> 发送给目标进程 –> 进程执行对应处理动作。
如何简单快速理解系统闹钟
系统闹钟,其实本质是OS必须⾃⾝具有定时功能,并能让⽤⼾设置这种定时功能,才可能实现闹钟这样的技术。现代Linux是提供了定时功能的,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是:
struct timer_list {
struct list_head entry; // 链表节点,用于挂载到定时器链表中
unsigned long expires; // 超时时间(jiffies 节拍数)
void (*function)(unsigned long); // 超时后执行的回调函数
unsigned long data; // 传给回调函数的参数
struct tvec_t_base_s *base; // 定时器所属的基数(管理定时器的内核数据结构)
};
各字段说明
| entry | 内核链表节点,把定时器串到定时器链表里统一管理 |
| expires | 什么时候到期,单位是 jiffies(内核时钟节拍) |
| function | 定时时间到了要执行的函数指针 |
| data | 传给 function 的参数(比如传个指针或数值) |
| base | 指向管理这个定时器的基结构,用来控制定时器的生命周期 |
我们不在这部分进⾏深究,为了理解它,我们可以看到:定时器超时时间expires和处理⽅法 function。操作系统管理定时器,采⽤的是时间轮的做法,但是我们为了简单理解,可以把它在组织成为"堆结构"。
简单总结:timer_list 就是内核的闹钟结构体,本质和用户态 alarm() 加 signal() 是一回事,只不过在 内核层实现。用户程序设置闹钟,最后就是内核里创建和调度 timer_list 实例来完成定时。
4.硬件异常产生信号
硬件异常,说白了就是硬件发现了不对劲,然后告诉 OS,OS 再给进程发信号。
先说除0:CPU内部有个状态寄存器,每次算完都会记一下结果。除0这事儿在CPU看来就是溢出了,状态寄存器里有个溢出标志位会标记这次计算有问题,这时候CPU就会通知OS。
再说野指针和越界:程序里用的地址都是虚拟地址,需要MMU(内存管理单元)这个硬件帮忙转成物理地址。转的时候MMU会检查权限,比如你访问的地址根本不存在,或者没有写权限,MMU就报错通知OS。OS一看,这是哪个进程的地址?就是哪个进程干的。
OS怎么知道是谁干的?
如果是除0,当前哪个进程在跑就是谁干的。如果是野指针,当前用的是哪个进程的页表就是谁干的。所以OS能精准找到肇事进程,然后发信号过去。
硬件异常发生后,进程一定会退出吗?
默认会退出,但也不一定。比如注册了信号处理函数就能捕获,程序可以继续跑。不过一般不建议这么干,因为出错的进程状态已经不正常了,继续跑可能出更多问题。
一句话总结:硬件异常被硬件检测到并通知内核,内核向当前进程发送适当的信号,默认动作是终止进程。
代码如下:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signum)
{
sleep(1);
cout << "获得了一个信号:" << signum << endl;
}
int main()
{
signal(SIGFPE, handler); // 捕获除0异常
int a = 100;
a /= 0; // 除0,触发 SIGFPE
while(true)
sleep(1);
return 0;
}

为什么会出现死循环?
因为硬件异常根本没有被解决。
a /= 0 触发除零异常后,CPU 的状态寄存器里一直记录着"溢出"这个错误状态。handler 执行完返回后,CPU 再次执行 a /= 0 这行代码,又触发同样的异常,再次收到 SIGFPE,再次执行 handler…… 无限循环。
错误代码
void handler(int signum)
{
sleep(1);
cout << "获得了一个信号: " << signum << endl;
// 处理完返回,异常还在,死循环
}
int main()
{
signal(SIGFPE, handler);
int a = 100;
a /= 0; // 每次都在这行触发异常
while(true) sleep(1);
}
修正方法
void handler(int signum)
{
sleep(1);
cout << "获得了一个信号: " << signum << endl;
exit(1); // 直接退出,不返回触发异常的代码
}
int main()
{
signal(SIGFPE, handler);
int a = 100;
a /= 0;
while(true) sleep(1);
}
小结一下:
捕获信号后返回异常点,异常条件没变,再次触发同一硬件异常,再次收到信号,如此反复形成死循环。只有退出进程或在 handler 里修复异常原因,才能打破这个循环。
5.问题总结
信号相关核心问题总结
1. 所有信号最终都要由 OS 执行?
因为 OS 是进程的管理者,只有 OS 知道所有进程的状态,也只有 OS 有权限修改进程 PCB 中的信息。
2. 信号是否立即处理?
不是,信号是在合适的时机处理的。进程可能正在执行更重要的操作(比如内核态系统调用),不会因为收到信号就立刻打断当前工作,而是等回到用户态时再统一处理待处理的信号。
3. 信号需要记录吗?记录在哪?
需要。信号是异步事件,可能在任何时刻发生,进程不可能立刻处理。信号需要暂时记录在进程的 PCB 中,具体是用位图(pending 表)来存储,哪个信号来了就把对应比特位设为 1。
4. 进程如何知道信号该做什么?
程序员在代码里已经通过 signal() 或 sigaction() 注册好了信号处理函数,处理方式记录在 PCB 的 handler 表里。进程收到信号时查表就知道是该默认终止、忽略还是调用自定义函数。
5. OS 向进程发送信号的本质?
OS 找到目标进程的 PCB,把 pending 位图中对应信号编号的比特位从 0 改成 1,这就是发送信号的本质。进程在合适的时机检查 pending 表,发现有信号待处理,再根据 handler 表执行对应的动作。
小结一下:OS 负责在目标进程 PCB 中设置 pending 位图标记信号,进程在合适的时机检查并执行对应的处理动作。






