/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

大家好,欢迎来到 huangjin007_ 的博客
⭐
个人主页:huangjin007_
🔥
文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持能从冰封的土地里培育出十万朵怒放的蔷薇
Linux 系统篇(三十六) —— 信号是如何产生的?
文章目录
- Linux 系统篇(三十六) —— 信号是如何产生的?
-
- 一、前言
- 二、Linux 信号产生的五种方式
- 三、通过终端按键产生信号
-
- 3.1 Ctrl+C:为什么能够终止进程?
- 3.2 Ctrl+\\:为什么能够产生 Core Dump?
- 3.3 Ctrl+Z:为什么程序没有结束?
- 3.4 理解 OS 如何知道键盘有数据?
- 3.5 前台进程与后台进程
- 3.6 孤儿进程为什么不能直接通过 Ctrl+C 终止?
- 四、通过系统命令产生信号
-
- 4.1 kill 命令的基本使用
- 五、通过系统调用产生信号
-
- 5.1 kill():向指定进程发送信号
-
- 实现自己的 mykill 命令
- 5.2 raise():给自己发送信号
- 5.3 abort():让进程异常终止
- 六、由软件条件产生信号
-
- 6.1 SIGPIPE:管道读端关闭后继续写入
- 6.2 alarm():设置系统闹钟
-
- alarm() 的返回值
- 6.3 alarm() 的基本验证:体会 IO 效率问题
- 6.4 如何设置重复闹钟?
- 七、如何理解软件条件?
- 八、系统闹钟的底层原理
-
- 8.1 操作系统为什么需要管理定时器?
- 8.2 定时器的数据结构
- 8.3 多个定时器如何组织?
- 九、硬件异常产生信号
-
- 9.1 除零错误:SIGFPE
- 9.2 野指针:SIGSEGV
- 9.3 为什么操作系统能够发现这些错误?
- 9.4 为什么捕捉 SIGSEGV 后,程序可能一直打印?
- 十、Core Dump:进程异常终止后的现场保存
-
- 10.1 什么是 Core Dump?
- 10.2 为什么有时候没有 Core 文件?
- 10.3 如何查看子进程是否产生 Core Dump?
- 十一、总结
- 结语:
一、前言
在上一篇博客中,我们初步认识了 Linux 信号,了解了信号的基本概念、生命周期,以及信号的三种处理方式:
- 默认处理(SIG_DFL)
- 忽略处理(SIG_IGN)
- 自定义捕捉(Catch)
我们知道,信号的生命周期可以简单概括为:
信号产生 → 信号保存 → 信号处理
其中,信号产生是整个生命周期的起点。
那么,一个问题就来了:
信号究竟是怎么产生的?是谁产生的?
例如:
- 按下 Ctrl+C,为什么程序会终止?
- 执行 kill -9 pid,为什么能够杀死指定进程?
- 程序发生除零错误,操作系统为什么会知道?
- 调用 alarm(),为什么过一段时间进程就会收到信号?
这些看似毫不相关的现象,其实都与 Linux 信号的产生机制有关。
本篇就从这些实际问题出发,一步步理解 Linux 信号的五种产生方式。
二、Linux 信号产生的五种方式
从产生来源来看,Linux 信号主要有以下五种产生方式:
| 键盘产生 | Ctrl+C、Ctrl+\\、Ctrl+Z | SIGINT、SIGQUIT、SIGTSTP |
| 系统调用产生 | kill()、raise()、abort() | 根据调用参数决定 |
| 系统命令产生 | kill -9 pid | SIGKILL |
| 硬件异常产生 | 除零、非法内存访问 | SIGFPE、SIGSEGV |
| 软件条件产生 | alarm() 超时、管道读端关闭 | SIGALRM、SIGPIPE |
虽然这些信号的产生方式不同,但它们最终都需要操作系统参与。
信号不是硬件或者用户直接修改进程的数据结构,而是由操作系统负责产生、记录和递达的。
三、通过终端按键产生信号
3.1 Ctrl+C:为什么能够终止进程?
在 Linux 中,我们经常使用 Ctrl+C 终止正在运行的程序。
例如:
while :; do
echo "I am running…"
sleep 1
done
运行之后,程序会不断输出内容。
此时按下:
Ctrl + C
程序就会终止。
那么问题来了:
我们明明只是按下了一个键,为什么进程就终止了?
实际上,Ctrl+C 并不是直接杀死进程,而是通过终端产生一个信号:
SIGINT(信号编号为 2)
其默认处理动作是 Term,也就是终止进程。
我们可以编写一个简单的程序验证:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是进程: " << getpid()
<< ", 我获得了一个信号: "
<< signumber << std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
// 捕捉 SIGINT 信号
signal(SIGINT, handler);
while (true)
{
std::cout << "I am a process, I am waiting signal!"
<< std::endl;
sleep(1);
}
return 0;
}
编译运行:
g++ sig.cc -o sig
./sig
运行结果: 
可以看到,按下 Ctrl+C 后,程序并没有终止,而是执行了我们自定义的处理函数。
这是因为我们通过 signal() 修改了 SIGINT 的处理动作。
如果注释掉:
signal(SIGINT, handler);
再次运行程序,按下 Ctrl+C,进程就会按照默认动作终止。
这里需要记住:
Ctrl+C 产生的是 SIGINT 信号,而进程是否终止,取决于它对这个信号设置的处理动作。
3.2 Ctrl+\\:为什么能够产生 Core Dump?
除了 Ctrl+C,还有一个比较特殊的组合键:
Ctrl + \\
它会产生:
SIGQUIT(信号编号为 3)
SIGQUIT 默认的处理动作是:
终止进程,并产生 Core Dump。
我们只需要把上面程序中的信号修改一下:
signal(SIGQUIT, handler);
运行程序后按下 Ctrl+\\:

程序没有终止,是因为我们捕捉了 SIGQUIT。
如果注释掉捕捉代码:
// signal(SIGQUIT, handler);
再次运行并按下 Ctrl+\\:

为什么会出现 Core Dump?
简单来说,当进程因为某些信号异常终止,并且该信号的默认动作支持 Core Dump 时,操作系统可以将进程的部分内存状态保存到磁盘,供后续调试使用。
关于 Core Dump,后面会专门介绍。
3.3 Ctrl+Z:为什么程序没有结束?
还有一个常见的组合键:
Ctrl + Z
它产生的是:
SIGTSTP(信号编号为 20)
与前面两个信号不同,SIGTSTP 的默认动作不是终止进程,而是停止进程。
我们修改一下程序:
signal(SIGTSTP, handler);
运行程序并按下 Ctrl+Z:

由于我们捕捉了 SIGTSTP,所以程序并没有停止。
注释掉捕捉代码,再次运行:

此时进程已经被暂停了,但并没有退出。
可以使用:
jobs
查看当前 Shell 管理的作业:

如果想让它继续运行,可以使用:
bg %1
让它在后台继续运行。
或者:
fg %1
将它重新放到前台运行。
这里需要注意:
Ctrl+Z 只是让进程停止,并不是结束进程。
3.4 理解 OS 如何知道键盘有数据?
前面我们知道:
- Ctrl+C 产生 SIGINT。
- Ctrl+\\ 产生 SIGQUIT。
- Ctrl+Z 产生 SIGTSTP。
那么问题来了:
操作系统是怎么知道我们按下了这些组合键的?
这就涉及键盘输入的处理过程。
可以简单理解为(具体的过程我们之后会讲):
例如:

这里需要特别注意:
键盘产生的信号,通常发送给当前终端的前台进程组,而不是任意一个进程。
3.5 前台进程与后台进程
我们平时执行程序,通常有两种方式:
./test
以及:
./test &
它们有什么区别?
| ./test | 前台运行 |
| ./test & | 后台运行 |
前台进程和后台进程最大的区别之一,就是它们对终端输入的使用方式不同。
前台进程:
- 可以从终端读取标准输入。
- 可以向终端输出内容。
- 接收终端发送的键盘信号。
后台进程:
- 可以向终端输出内容。
- 默认不能直接读取终端标准输入。
- 不会因为终端上的 Ctrl+C 而直接收到对应信号。
为什么后台进程不能直接读取终端输入?
因为一个终端在同一时刻只能有一个前台进程组。
如果后台进程尝试读取控制终端,通常会收到 SIGTTIN 信号,并按照默认动作停止。
所以,前台与后台的区别并不是能不能输出,而主要涉及终端输入的控制权。
3.6 孤儿进程为什么不能直接通过 Ctrl+C 终止?
假设存在父子进程关系:
父进程
|
fork()
|
+—- 子进程
如果父进程先退出,子进程就会成为孤儿进程。
孤儿进程会被系统中的其他进程(通常是 PID 1 对应的 init 或 systemd)收养。
但是这里有一个容易混淆的地方:
孤儿进程并不等于后台进程。
父进程退出后,子进程是否还能接收 Ctrl+C,取决于它是否仍然属于终端的前台进程组。
如果它已经不属于当前终端的前台进程组,那么终端按键产生的 SIGINT 就不会发送给它。
因此,不能简单地认为所有孤儿进程都无法通过 Ctrl+C 终止。
四、通过系统命令产生信号
前面我们通过键盘产生了信号。
那么,如果不想通过键盘操作,能不能主动给指定进程发送信号?
当然可以。
Linux 提供了 kill 命令。
4.1 kill 命令的基本使用
首先编写一个简单的死循环程序:
#include <iostream>
#include <unistd.h>
int main()
{
while (true)
{
sleep(1);
}
return 0;
}
编译运行:
g++ sig.cc -o sig
让程序在后台运行:
./sig &
通过下面的命令查看进程 PID:
ps ajx | grep sig
假设得到:
PPID PID PGID SID TTY
211805 213784 213784 211805 pts/0
其中 PID 为:
213784
现在,我们向这个进程发送 SIGSEGV 信号:
kill -SIGSEGV 213784
也可以使用信号编号:
kill -11 213784
因为 SIGSEGV 对应的编号就是 11。
此时进程会按照 SIGSEGV 的默认处理动作终止。
五、通过系统调用产生信号
前面使用的 kill 命令,本质上也是通过系统调用实现的。
Linux 为我们提供了几个常用接口:
- kill()
- raise()
- abort()
下面逐个认识。
5.1 kill():向指定进程发送信号
函数原型:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
参数说明:
- pid:指定目标进程。
- sig:需要发送的信号编号。
返回值:
- 成功:返回 0。
- 失败:返回 -1,并设置 errno。
需要注意,kill() 并不一定意味着杀死进程。
它真正的作用是发送指定信号,最终如何处理,取决于目标进程对该信号的处理动作。
例如:
kill(pid, SIGINT);
表示向指定进程发送 SIGINT。
另外,kill() 的第一个参数还有不同含义:
| pid > 0 | 向 PID 对应的进程发送信号 |
| pid == 0 | 向调用进程所在的进程组发送信号 |
| pid == -1 | 向调用者有权限发送信号的进程发送信号 |
| pid < -1 | 向进程组 ID 为 pid 的进程组发送信号 |
这里的 kill() 需要具备相应权限,否则发送会失败。
实现自己的 mykill 命令
理解了函数原型之后,我们可以尝试自己实现一个简单的 kill 命令。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
// mykill -signumber pid
int main(int argc, char* argv[])
{
if (argc != 3)
{
std::cerr << "Usage: "
<< argv[0]
<< " -signumber pid"
<< std::endl;
return 1;
}
// 去掉信号编号前面的 –
int number = std::stoi(argv[1] + 1);
pid_t pid = std::stoi(argv[2]);
// 发送信号
int ret = kill(pid, number);
if (ret == –1)
{
perror("kill");
return 1;
}
return 0;
}
编译:
g++ mykill.cc -o mykill
使用:
./mykill -9 213784
这样,我们就实现了一个非常简单的信号发送工具。
5.2 raise():给自己发送信号
如果想让一个进程给自己发送信号,应该怎么办?
可以使用 raise()。
函数原型:
#include <signal.h>
int raise(int sig);
作用:
向调用 raise() 的进程发送指定信号。
可以近似理解为:
kill(getpid(), sig);
其中:
getpid()
用于获取当前进程的 PID。
来看一个例子:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "获取了一个信号: "
<< signumber << std::endl;
}
int main()
{
// 捕捉 2 号信号
signal(SIGINT, handler);
while (true)
{
sleep(1);
// 每隔一秒给自己发送一次 SIGINT
raise(SIGINT);
}
return 0;
}
运行结果: 
可以看到,进程不需要依赖键盘,也不需要其他进程,就能够通过 raise() 给自己发送信号。
5.3 abort():让进程异常终止
前面两个接口都可以发送指定信号。
那么,如果我们想让当前进程异常终止呢?
可以使用 abort()。
函数原型:
#include <stdlib.h>
void abort(void);
abort() 的作用是:
使当前进程异常终止,并产生 SIGABRT 信号(编号 6)。
来看一个例子:
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "获取了一个信号: "
<< signumber << std::endl;
}
int main()
{
signal(SIGABRT, handler);
while (true)
{
sleep(1);
abort();
}
return 0;
}
运行结果:
获取了一个信号: 6
Aborted
这里有一个比较特殊的地方:
即使我们已经捕捉了 SIGABRT,进程最终还是会终止。
因为 abort() 的设计目的就是使当前进程异常终止,捕捉处理函数并不能阻止它最终终止进程。
六、由软件条件产生信号
前面介绍的信号,大多是通过键盘或者主动调用函数产生的。
但实际上,还有一类信号,它们并不是由用户主动发送的,而是由某些软件条件触发的。
例如:
- 管道的读端关闭后,写端继续写入。
- 定时器到达指定时间。
这就是软件条件产生的信号。
6.1 SIGPIPE:管道读端关闭后继续写入
还记得之前学习的匿名管道吗?
管道通常用于具有亲缘关系的进程之间进行通信。
例如:

如果进程 B 关闭了读端,而进程 A 仍然尝试向管道写入数据,会发生什么?
操作系统会产生:
SIGPIPE(信号编号 13)
其默认处理动作是终止进程。
注意:
SIGPIPE 的产生条件是管道没有读者时仍然写入,而不是管道缓冲区已满。
这是两个完全不同的概念。
6.2 alarm():设置系统闹钟
接下来介绍一个非常重要的函数:alarm()。
它可以让操作系统在指定时间后向当前进程发送 SIGALRM 信号。
函数原型:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
它的作用是:
调用 alarm(seconds),告诉操作系统:
在 seconds 秒之后,向当前进程发送 SIGALRM 信号。
SIGALRM 的编号为 14,默认处理动作是终止进程。
例如:
alarm(5);
表示设置一个 5 秒后的闹钟。
5 秒后,当前进程就会收到 SIGALRM。
如果:
alarm(0);
则表示取消之前设置的闹钟。
alarm() 的返回值
alarm() 的返回值比较容易考察。
它返回:
之前设置的闹钟还剩余的秒数。
如果之前没有设置闹钟,则返回 0。
例如:
alarm(5);
sleep(2);
unsigned int ret = alarm(10);
std::cout << ret << std::endl;
大致执行过程:
t = 0:
alarm(5)
设置一个 5 秒后的闹钟
返回 0,因为之前没有闹钟
t = 2:
已经过去 2 秒
原来的闹钟还剩 3 秒
再次调用 alarm(10)
返回 3
同时将原来的闹钟替换为 10 秒后的新闹钟
所以可以总结为:
- 第一次设置闹钟,通常返回 0。
- 再次设置闹钟,返回旧闹钟剩余时间。
- 新闹钟会替换旧闹钟。
- alarm(0) 可以取消闹钟。
6.3 alarm() 的基本验证:体会 IO 效率问题
我们先来看一个实验。
需求:
让程序在 1 秒钟内不断计数,1 秒后通过 SIGALRM 终止进程。
第一种方式:大量 IO 操作。
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
int count = 0;
alarm(1);
while (true)
{
std::cout << "count : "
<< count << std::endl;
count++;
}
return 0;
}
运行结果:

第二种方式:减少 IO 操作。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
int count = 0;
void handler(int signumber)
{
std::cout << "count : " << count << std::endl;
exit(0);
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
count++;
}
return 0;
}
运行结果:

可以发现,第二种方式在相同时间内完成的计数明显更多。
为什么?
因为第一种方式每次循环都要进行输出操作,而 IO 操作通常涉及缓冲区管理,甚至可能发生用户态与内核态之间的切换。
第二种方式只进行计数,减少了大量 IO 开销。
因此,我们可以得出一个结论:
IO 操作通常比简单的内存计算开销更大,频繁 IO 会明显影响程序运行效率。
6.4 如何设置重复闹钟?
前面我们发现,alarm() 设置的闹钟是一次性的。
当时间到达后,内核会发送 SIGALRM,原来的闹钟就结束了。
那么,如果我们想实现每隔 1 秒执行一次任务,应该怎么办?
可以在信号处理函数中重新设置闹钟。
这里还需要认识一个函数:pause()。
函数原型:
#include <unistd.h>
int pause(void);
pause() 的作用:
让当前进程挂起,直到捕捉到一个信号,并且对应的信号处理函数返回。
如果信号处理函数正常返回,pause() 会返回 -1,并将 errno 设置为 EINTR。
来看一个例子:
#include <iostream>
#include <unistd.h>
#include <signal.h>
int gcount = 0;
void handler(int signo)
{
std::cout << "收到信号: " << signo << std::endl;
// 重新设置 1 秒后的闹钟
alarm(1);
}
int main()
{
signal(SIGALRM, handler);
// 设置第一次闹钟
alarm(1);
while (true)
{
// 等待信号
pause();
std::cout << "我醒来了…" << std::endl;
gcount++;
std::cout << "gcount: "
<< gcount << std::endl;
}
return 0;
}
运行之后,我们会发现程序每隔一段时间就会被唤醒一次。
整个过程可以理解为:

七、如何理解软件条件?
前面我们介绍了 SIGPIPE 和 SIGALRM。
它们虽然产生原因不同,但有一个共同特点:
信号不是由 CPU 执行错误指令直接产生的,而是由软件状态或软件设置的条件触发的。
例如:

再例如:

这就是软件条件产生信号的基本含义。
八、系统闹钟的底层原理
前面我们通过 alarm() 实现了定时功能。
但还有一个值得思考的问题:
操作系统是怎么记住每个进程设置的闹钟,并且在时间到达时准确发送信号的?
这就涉及操作系统对定时器的管理。
8.1 操作系统为什么需要管理定时器?
假设系统中有三个进程:
进程 A:设置 5 秒后发送信号
进程 B:设置 10 秒后发送信号
进程 C:设置 3 秒后发送信号
操作系统必须知道:
- 哪个进程设置了闹钟?
- 闹钟什么时候到期?
- 到期之后应该执行什么操作?
如果只记录时间,却不知道对应的进程,就无法准确地发送信号。
因此,操作系统需要对定时器进行统一管理。
这也符合操作系统中一个非常重要的思想:
先描述,再组织。
8.2 定时器的数据结构
Linux 内核中曾使用过类似下面的定时器结构:
struct timer_list
{
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_t_base_s *base;
};
这是较早期 Linux 内核版本中的结构示意,现代内核的具体实现已经有所变化。
其中比较重要的是两个成员:
expires:表示定时器的到期时间。
例如:
expires = 100
可以理解为定时器在指定的时间点到期。
function:表示定时器到期后需要执行的回调函数。
例如,闹钟到期后,相关处理逻辑就会向对应进程发送 SIGALRM。
8.3 多个定时器如何组织?
假设操作系统中同时存在很多定时器:
定时器 A:5 秒
定时器 B:10 秒
定时器 C:3 秒
定时器 D:1 秒
操作系统需要快速找到最先到期的定时器。
从数据结构的角度,可以使用最小堆来帮助理解:
1
/ \\
3 5
/
10
最小堆有一个特点:
堆顶元素永远是当前最小的元素。
因此,想知道哪个定时器最先到期,只需要查看堆顶即可。
- 获取最小值:O(1)
- 插入元素:O(log n)
- 删除堆顶:O(log n)
九、硬件异常产生信号
前面介绍的几种信号,大多来自用户操作或者软件条件。
接下来,我们看看另一种比较特殊的产生方式:
硬件异常。
例如:
- 整数除零。
- 访问非法内存地址。
- 向只读内存写入数据。
这些操作都可能导致程序出现异常。
那么,操作系统是怎么知道程序发生了错误的?
9.1 除零错误:SIGFPE
先来看一个最简单的例子:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
printf("catch a sig : %d\\n", sig);
}
int main()
{
sleep(1);
int a = 10;
a /= 0;
while (1);
return 0;
}
运行程序:
Floating point exception
这里产生的是:
SIGFPE(信号编号 8)
SIGFPE 的全称是 Floating Point Exception,表示算术运算异常。
虽然名字中有 Floating Point,但它并不只针对浮点运算,整数除零也可能产生 SIGFPE。
我们可以尝试捕捉它:
signal(SIGFPE, handler);
发生异常时,处理函数就会收到对应的信号编号。
9.2 野指针:SIGSEGV
再来看另一个非常常见的错误:
非法内存访问。
例如:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
printf("catch a sig : %d\\n", sig);
}
int main()
{
sleep(1);
int* p = NULL;
*p = 100;
while (1);
return 0;
}
运行:
Segmentation fault (core dumped)
这是因为我们尝试通过空指针访问内存。
系统产生的信号是:
SIGSEGV(信号编号 11)
它表示非法内存访问。
常见情况包括:
- 解引用空指针。
- 访问没有权限的内存。
- 向只读内存区域写入数据。
- 访问已经失效的内存地址。
9.3 为什么操作系统能够发现这些错误?
这是一个非常重要的问题。
我们先回忆一下之前学习的虚拟内存和地址转换。
在现代操作系统中,进程使用的是虚拟地址。
当程序访问一个地址时,CPU 中的 MMU 会负责将虚拟地址转换为物理地址。
整个过程可以简单理解为:

这里涉及两个重要的硬件:
CPU:
负责执行指令,当执行到非法操作时,可能触发处理器异常。
例如整数除零,就可能触发处理器的除法异常。
MMU:
负责内存地址转换和访问权限检查。
当程序访问非法地址或者违反内存访问权限时,MMU 可以触发页故障等异常。
随后,CPU 将控制权交给操作系统的异常处理程序。
操作系统根据异常类型和发生位置,判断应该采取什么处理方式,并向对应进程产生相应信号。
因此,需要记住:
硬件负责检测异常并通知内核,真正向进程发送信号的是操作系统。
9.4 为什么捕捉 SIGSEGV 后,程序可能一直打印?
这里有一个比较容易让人困惑的现象。
假设我们捕捉了 SIGSEGV:
void handler(int sig)
{
printf("catch a sig : %d\\n", sig);
}
然后执行:
int* p = NULL;
*p = 100;
可能出现:
catch a sig : 11
catch a sig : 11
catch a sig : 11
…
为什么同一个信号会不断产生?
原因在于:
程序发生非法内存访问后,处理函数虽然执行了,但原来的错误指令并没有得到修复。
当处理函数返回后,程序可能重新回到发生异常的指令位置继续执行。
再次执行时,又会发生相同的非法访问,于是再次产生 SIGSEGV。
如此反复,就可能形成连续的异常处理。
因此,捕捉信号并不意味着修复了程序错误。
对于 SIGSEGV、SIGFPE 这类硬件异常,通常不能简单地捕捉后继续执行原程序。
十、Core Dump:进程异常终止后的现场保存
前面在学习 Ctrl+\\ 时,我们见过:
Quit (core dumped)
这里就正式认识一下 Core Dump。
10.1 什么是 Core Dump?
当进程因为某些信号异常终止时,操作系统可以将进程当时的部分内存状态保存到磁盘文件中。
这个文件通常称为 Core 文件。
它的作用是:
保存进程异常终止时的现场,方便开发人员事后分析程序为什么崩溃。
例如:

这种调试方式叫作:
Post-mortem Debug,也就是事后调试。
10.2 为什么有时候没有 Core 文件?
因为 Core 文件可能比较大,而且可能包含进程中的敏感数据。
所以 Linux 通常会通过资源限制控制 Core 文件的生成。
我们可以使用:
ulimit -a
查看当前 Shell 的资源限制。
其中:
core file size (blocks, -c) 0
表示当前 Core 文件大小限制为 0,也就是不允许生成 Core 文件。
如果希望在开发调试阶段生成 Core 文件,可以执行:
ulimit -c unlimited
表示取消当前 Shell 的 Core 文件大小限制。
然后再运行程序:
./test
当程序因为支持 Core Dump 的信号异常终止,并且其他生成条件满足时,就有机会生成 Core 文件。
需要注意:
ulimit 修改的是当前 Shell 及其后续创建的子进程所继承的资源限制,不会自动影响已经运行的其他进程。
另外,Core 文件的实际生成位置和名称还可能受到系统配置影响。
10.3 如何查看子进程是否产生 Core Dump?
我们可以通过 waitpid() 获取子进程的退出状态。
例如:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(1);
int a = 10;
a /= 0;
return 0;
}
int status = 0;
waitpid(id, &status, 0);
if (WIFSIGNALED(status))
{
std::cout << "exit signal: "
<< WTERMSIG(status) << std::endl;
std::cout << "core dump: "
<< WCOREDUMP(status) << std::endl;
}
return 0;
}
这里有几个值得认识的宏:
| WIFEXITED(status) | 判断子进程是否正常退出 |
| WEXITSTATUS(status) | 获取正常退出时的退出码 |
| WIFSIGNALED(status) | 判断子进程是否被信号终止 |
| WTERMSIG(status) | 获取终止子进程的信号编号 |
| WCOREDUMP(status) | 判断是否产生了 Core Dump |
例如:
exit signal: 8
core dump: 1
表示子进程因为 SIGFPE(8)异常终止,并且产生了 Core Dump。
十一、总结
到这里,我们已经学习了 Linux 信号的五种产生方式。
可以将它们统一整理成下面这张图:
#mermaid-svg-VkostN5TsB5jPUPw{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VkostN5TsB5jPUPw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VkostN5TsB5jPUPw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VkostN5TsB5jPUPw .error-icon{fill:#552222;}#mermaid-svg-VkostN5TsB5jPUPw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VkostN5TsB5jPUPw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VkostN5TsB5jPUPw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VkostN5TsB5jPUPw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VkostN5TsB5jPUPw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VkostN5TsB5jPUPw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VkostN5TsB5jPUPw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VkostN5TsB5jPUPw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VkostN5TsB5jPUPw .marker.cross{stroke:#333333;}#mermaid-svg-VkostN5TsB5jPUPw svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VkostN5TsB5jPUPw p{margin:0;}#mermaid-svg-VkostN5TsB5jPUPw .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-VkostN5TsB5jPUPw .cluster-label text{fill:#333;}#mermaid-svg-VkostN5TsB5jPUPw .cluster-label span{color:#333;}#mermaid-svg-VkostN5TsB5jPUPw .cluster-label span p{background-color:transparent;}#mermaid-svg-VkostN5TsB5jPUPw .label text,#mermaid-svg-VkostN5TsB5jPUPw span{fill:#333;color:#333;}#mermaid-svg-VkostN5TsB5jPUPw .node rect,#mermaid-svg-VkostN5TsB5jPUPw .node circle,#mermaid-svg-VkostN5TsB5jPUPw .node ellipse,#mermaid-svg-VkostN5TsB5jPUPw .node polygon,#mermaid-svg-VkostN5TsB5jPUPw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VkostN5TsB5jPUPw .rough-node .label text,#mermaid-svg-VkostN5TsB5jPUPw .node .label text,#mermaid-svg-VkostN5TsB5jPUPw .image-shape .label,#mermaid-svg-VkostN5TsB5jPUPw .icon-shape .label{text-anchor:middle;}#mermaid-svg-VkostN5TsB5jPUPw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VkostN5TsB5jPUPw .rough-node .label,#mermaid-svg-VkostN5TsB5jPUPw .node .label,#mermaid-svg-VkostN5TsB5jPUPw .image-shape .label,#mermaid-svg-VkostN5TsB5jPUPw .icon-shape .label{text-align:center;}#mermaid-svg-VkostN5TsB5jPUPw .node.clickable{cursor:pointer;}#mermaid-svg-VkostN5TsB5jPUPw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VkostN5TsB5jPUPw .arrowheadPath{fill:#333333;}#mermaid-svg-VkostN5TsB5jPUPw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VkostN5TsB5jPUPw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VkostN5TsB5jPUPw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VkostN5TsB5jPUPw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VkostN5TsB5jPUPw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VkostN5TsB5jPUPw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VkostN5TsB5jPUPw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VkostN5TsB5jPUPw .cluster text{fill:#333;}#mermaid-svg-VkostN5TsB5jPUPw .cluster span{color:#333;}#mermaid-svg-VkostN5TsB5jPUPw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VkostN5TsB5jPUPw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VkostN5TsB5jPUPw rect.text{fill:none;stroke-width:0;}#mermaid-svg-VkostN5TsB5jPUPw .icon-shape,#mermaid-svg-VkostN5TsB5jPUPw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VkostN5TsB5jPUPw .icon-shape p,#mermaid-svg-VkostN5TsB5jPUPw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VkostN5TsB5jPUPw .icon-shape .label rect,#mermaid-svg-VkostN5TsB5jPUPw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VkostN5TsB5jPUPw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VkostN5TsB5jPUPw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VkostN5TsB5jPUPw :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-VkostN5TsB5jPUPw .startNode>*{fill:#F2F4F7!important;stroke:#9CA3AF!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .startNode span{fill:#F2F4F7!important;stroke:#9CA3AF!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .startNode tspan{fill:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .sourceNode>*{fill:#E0F2FE!important;stroke:#7DD3FC!important;stroke-width:1px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .sourceNode span{fill:#E0F2FE!important;stroke:#7DD3FC!important;stroke-width:1px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .sourceNode tspan{fill:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .detailNode>*{fill:#DCFCE7!important;stroke:#86EFAC!important;stroke-width:1px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .detailNode span{fill:#DCFCE7!important;stroke:#86EFAC!important;stroke-width:1px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .detailNode tspan{fill:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .osNode>*{fill:#F3E8FF!important;stroke:#D8B4FE!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .osNode span{fill:#F3E8FF!important;stroke:#D8B4FE!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .osNode tspan{fill:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .pendingNode>*{fill:#FFE4E6!important;stroke:#FDA4AF!important;stroke-width:1px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .pendingNode span{fill:#FFE4E6!important;stroke:#FDA4AF!important;stroke-width:1px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .pendingNode tspan{fill:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .handleNode>*{fill:#ECFCCB!important;stroke:#BEF264!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .handleNode span{fill:#ECFCCB!important;stroke:#BEF264!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-VkostN5TsB5jPUPw .handleNode tspan{fill:#333!important;}
信号产生
键盘输入
系统调用
系统命令
硬件异常
软件条件
Ctrl+C / Ctrl+\\ / Ctrl+Z
kill() / raise() / abort()
kill -9 等
除零 / 野指针
alarm() / SIGPIPE
操作系统处理
保存信号 Pending
选择合适的时机
执行信号处理
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _
网硕互联帮助中心






评论前必须登录!
注册