036 进程信号 —— 信号的产生

进程信号 —— 信号的产生

首先,本节的信号和上一节的信号量没有任何关系!它们的关系就像老婆饼和老婆,没有任何关系!后面的内容主要是根据 信号的产生 → 信号的保存 → 信号的处理 来进行讲解。

Linux 中的 31 个普通信号

1. 信号的概念

1. 生活中的信号

生活中常见的信号,比如:

  • 闹钟:闹钟响 = 通知你该起床了。
  • 红绿灯:红灯亮 = 告诉你该停下来了。
  • 电话:响铃 = 通知你有人呼叫你,需要你接听。
  • 警报器:火警 = 紧急中断,要求人立刻撤离。

可见,信号本质就是“通知 + 响应”,核心思想是:不需要你一直盯着,只要有事就异步提醒你。

2. 信号的定义与理解

信号是一种 异步通知机制。用来 通知进程 发生了某种 异步事件本质:操作系统向一个进程发送一个整数编号(信号编号),告诉它“发生了某件事”,要求它“采取行动”或“做出响应”。

Q:你怎么认识这些信号?
A:有人教我 → 我记住了。认识:① 识别信号 ② 知道信号的处理方法。即便现在没有信号产生,我们也知道信号产生后该干什么。那么我们是谁?站在 OS 层面,我们自然是进程啦~

一个进程必须具备识别和处理信号的能力,这种能力是其自身功能的一部分,即使在没有收到任何信号的情况下,也应事先明确知道每种信号该如何处理。当某个信号实际产生时,进程可能不会立即响应,而是在合适的时机进行处理。因此,在信号产生与信号被真正处理之间,必然存在一个时间窗口。为了保证信号不会丢失,进程需要具备临时记录哪些信号已经发生的能力。

3. 信号的处理方式(3 种)

信号产生了,我们可能并不立即处理这个信号,在合适的时候,因为我们可能正在做更重要的事情,所以,信号产生后会被保存到当前的时间窗口,等待信号处理。可以类比为生活中的“来电提醒”:你正在开会(执行重要任务),手机静音(屏蔽某些信号),有人打电话给你(信号产生),但你不接(不立即处理),手机记录未接来电(未决信号集),会议结束后,你查看未接来电并回电(处理信号)。

每个信号都有默认动作,但你可以选择以下 三种方式 之一来处理它:

处理方式 含义 举例
默认动作 使用系统定义的默认行为 红灯亮,人们 默认动作 是等待绿灯
忽略 显式地忽略该信号(就像没发生一样),但某些信号无法忽略(如 SIGKILL, SIGSTOP 红灯亮,不妨存在闯红灯的人,他们完全 忽视 了红灯的存在
自定义动作 注册一个信号处理函数(signal handler),当信号到达时调用这个函数处理 红灯亮,你可能会看到“大小姐驾到统统闪开”“随我出发”这些社牛人群的 自定义动作

4. 深入理解 ctrl + c

认识“作业”(Job)和前后台进程

作业(Job) 这个词听起来抽象,其实就是:我们在一个 shell 会话里启动的一个程序或命令运行单元。关键点:

  • 在 bash 里,每个“作业”可以是 一个进程,也可以是 一组进程(进程组)
  • bash 会帮我们记录:当前有多少个作业?哪个是前台作业?哪些是后台作业?
名称 解释
前台作业 当前直接和终端(键盘输入、输出)交互的那组进程
后台作业 在终端后面跑,不占用终端输入,不能直接交互的那组进程
前台进程组 前台作业内部的那组进程,组成一个进程组,shell 管它
后台进程组 后台作业内部的那组进程
比较点 前台作业 后台作业
是否抢占终端输入 是(独占) 否(输入留给 shell)
是否能直接交互 可以 不能
能否直接接收 Ctrl+C 可以 不会收到
能否被 shell 追踪 会被追踪 会被追踪
  • “前台进程” ≈ “前台作业对应的进程(或进程组)”
  • “后台进程” ≈ “后台作业对应的进程(或进程组)”

因为:bash 管理的单位是 作业(Job)把前后台“作业”映射成前后台“进程组”。但底层作业就是由一个或多个 进程(进程组)组成,所以大家常常口头直接说“前台进程 / 后台进程”,指的其实就是“前台作业 / 后台作业里的进程”。我们只需要记住:“前台作业 / 前台进程 / 前台进程组” 在作业控制场景下是一个概念的不同层面,本质指的就是:当前跟终端交互、能直接被 Ctrl+C 打断的那一组进程。

相关命令:

1
2
3
4
5
>sleep 100 &      # 后台
>jobs # 查看
>fg %1 # 拉回前台
>Ctrl+Z # 挂起
>bg %1 # 后台继续

1. Ctrl+C 为什么能杀掉前台进程?

Ctrl+C 的本质:当我们在终端(如 bash)按下 Ctrl+C,它会向 前台进程组 发送一个 SIGINT 信号。SIGINT 是信号编号 2,所以也叫 2 号信号。

谁发的?

  • 不是按键直接杀死进程,而是 终端驱动检测到我们按下了 Ctrl+C
  • 然后内核就会让这个终端把 SIGINT 发给 前台进程组

为什么是前台进程?

  • Linux 下,每个终端只能有一个前台进程组
  • 只有前台进程能接收由终端产生的 终端控制信号(比如 Ctrl+C → SIGINT,Ctrl+Z → SIGTSTP)。

我没能展示一个完美的 Ctrl+C 的硬件到软件底层示意图,只能大致说一下流程了:键盘 → 控制器缓冲区 → 触发中断 → CPU 查向量表 → 键盘驱动读码 → 驱动识别特殊字符 → 内核发 SIGINT → 前台进程结束。

敲击键盘到屏幕上打印字符计算机都做了什么 | CSDN

当你在终端中按 Ctrl-c 时会发生什么 | Medium

了解 Linux 中的信号,例如程序上的 kill -9、CTRL + C | Medium

超越 Ctrl-C:Unix 信号处理的黑暗角落 | sunshowers

2. signal 函数原型

1. 作用

用于 设置某个信号的处理方式(3 种):

  • 使用自定义的 信号处理函数(handler,自己写函数)
  • 或者设置为 忽略SIG_IGN)。
  • 或者恢复为 默认处理SIG_DFL)。
2. 函数原型
1
2
3
#include <signal.h>
typedef void (*sighandler_t)(int); // typedef 定义了一个函数指针类型:sighandler_t 表示一个参数是 int、返回值是 void 的函数指针。
sighandler_t signal(int signum, sighandler_t handler);
3. 参数详解
  • int signum 要处理信号编号:如 SIGINT(2),SIGTERM(15),SIGKILL(9)等。这个参数可以直接写信号对应的编号,也能直接写信号(信号本身就是宏)。
  • sighandler_t handler 信号处理的三种方式:① 指向你写的自定义处理函数 ② SIG_IGN(忽略)③ SIG_DFL(默认处理)。

注意点:SIGKILL(9)和 SIGSTOP(19)不能被捕获、不能忽略,只能默认由内核强制处理!

4. 返回值
  • 成功:返回 之前注册的处理函数指针(方便保存旧处理方式)。
  • 失败:返回 SIG_ERR
5. 放置位置(通常只设置一次!)

signal 是用来 告诉系统:如果收到了这个信号,要怎么处理,所以——一定要在信号有可能到来之前就设置好!

常见位置 说明
main 一开始就注册 最常见、最稳妥:程序一启动就告诉内核“收到某个信号就这么处理”。
启动前先注册 如果有多线程 / fork 子进程,通常会在主线程或父进程里先注册好,再去干别的活
循环前注册 千万别把 signal 放在循环里反复调!只需要设置一次就行。

3. 验证 Ctrl+C 是 2 号信号

源码一览:证明信号的本质是宏。

image-20250715231031692

列出当前系统支持的所有信号名称:

image-20250715231713804

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void myhandler(int signum)
{
cout << "收到信号编号: " << signum << endl;
// 当 signal 使用了自定义“捕获”函数,使用 CTRL+C 退出程序时会无法正常退出,此时在此处调用 exit 函数即可正常退出程序。
// 不使用 exit 函数,想要退出可以使用 CTRL+\ 组合键。
exit(0);
}

int main()
{
signal(2, myhandler);
// 也可以写成 signal(SIGINT, myhandler);
cout << "运行中,按 Ctrl+C..." << endl;

while (true)
{
sleep(1);
}

return 0;
}

运行结果示例:程序成功打印出“收到信号编号: 2”的字样,证明 Ctrl+C 其实是 2 号信号 SIGINT。

image-20250715232026079

2. 信号的产生

1. 键盘组合键(终端控制信号)

  • Ctrl+C → 产生 SIGINT(2 号)
  • Ctrl+\ → 产生 SIGQUIT(3 号)
  • Ctrl+Z → 产生 SIGTSTP(20 号)

2. kill 命令

  • kill -9 PID → 给指定进程发 SIGKILL(9 号)
  • kill -15 PID → 给指定进程发 SIGTERM(15 号)
  • kill -l → 查看所有信号

kill 并不一定“杀”,它本质是“发信号”,什么信号自己选

3. 系统调用

1. kill 函数

1. 功能

kill 函数用于向指定 进程进程组 发送一个信号。常用于:

  • 结束某个进程(如发送 SIGTERMSIGKILL)。
  • 给自己或其他进程发送自定义信号。
  • 配合信号处理函数(signal / sigaction)实现 IPC。

注意:kill 并不一定会“杀死”进程,它只是发送信号,是否终止要看目标进程是否处理或屏蔽该信号。

2. 函数原型
1
2
#include <signal.h>
int kill(pid_t pid, int sig);
3. 参数详解
  1. pid_t pid:目标进程 ID 或进程组 ID。特殊值含义如下:
    • pid > 0:发送给 PID = pid 的单个进程。
    • pid = 0:发送给当前进程所在进程组的所有进程。
    • pid = -1:发送给调用者有权发送信号的所有进程(不推荐危险!),在生产中,不要轻易使用 pid = -1,可能会把整个系统都影响了。
    • pid < -1:发送给 PGID = -pid 的进程组中所有进程。
  2. int sig:要发送的信号编号,比如 SIGKILL(9)、SIGTERM(15)、SIGSTOP(19)等,也可以是自定义信号(如 SIGUSR1)。
4. 返回值
  • 成功:返回 0
  • 失败:返回 -1,并设置 errno 说明错误原因(如 ESRCH:无此 PID,EPERM:权限不足等)。
5. 代码示例

下面给你一个小示例,演示父进程使用 kill 给子进程发送 SIGTERM 信号,子进程注册信号处理函数后捕获该信号:

1
2
3
4
5
6
7
8
9
10
>pid_t pid = fork();

>if (pid == 0)
>{
// 这里是子进程,pid=0
>}
>else if (pid > 0)
>{
// 这里是父进程,pid=子进程PID
>}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <unistd.h> // fork(), sleep()
#include <signal.h> // signal(), kill()
#include <sys/wait.h> // wait()
using namespace std;

void myhandler(int signum)
{
cout << "子进程收到信号:" << signum << endl;
}

int main()
{
pid_t pid = fork();

if(pid == 0) // 子进程
{
signal(2, myhandler);
cout << "子进程运行中,按 Ctrl+C 退出..." << endl;

while(true)
{
sleep(1); // 保持运行。子进程休眠,等待父进程发送信号
}
}
else if(pid > 0) // 父进程
{
sleep(3); // 父进程休眠,等待子进程运行
kill(pid, 2); // 向子进程发送信号
cout << "父进程向子进程发送信号 2" << endl;

wait(NULL); // 等待子进程结束
cout << "子进程已结束,父进程退出!" << endl;
}
else
{
perror("fork"); // fork() 失败
}

return 0;
}

2. raise 函数

1. 功能

raise 用来让当前程序自己给自己发一个信号。

2. 函数原型
1
2
#include <csignal>  // 或 <signal.h>
int raise(int sig);
3. 参数详解

int sig 要发出的信号编号(如 SIGTERMSIGINT)。

4. 返回值
  • 成功:0。
  • 失败:非 0。
5. 代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <csignal>
using namespace std;

void myhandler(int sig)
{
cout << "收到信号: " << sig << endl;
}

int main()
{
signal(2, myhandler); // 注册自定义处理

cout << "程序准备即将被终止!" << endl;
raise(2); // 自己给自己发 SIGTERM

return 0;
}

3. abort 函数

1. 功能

abort 用来让程序立刻异常终止,发出 SIGABRT(6 号信号)。 它是固定发 SIGABRT不能换别的

2. 函数原型
1
2
#include <cstdlib>  // C++ 中用 <cstdlib>,C 中用 <stdlib.h>
void abort(void);

注意:无参数、无返回值,调用后程序就要终止(如果没捕获 SIGABRT)。

3. 代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <csignal>
#include <cstdlib>
using namespace std;

void myhandler(int sig)
{
cout << "收到 SIGABRT: " << sig << endl;
exit(0);
}

int main()
{
signal(SIGABRT, myhandler); // 注册自定义处理

cout << "程序即将收到 abort 信号立刻异常终止..." << endl;
abort(); // 固定发 6号信号 SIGABRT

return 0;
}

4. killraiseabort 的区别

函数 作用 典型使用场景 信号目标 头文件 特点
kill 指定进程/进程组 发送信号(谁都能杀 父进程给子进程发信号、脚本杀进程 任意指定的进程 PID(或进程组) <signal.h> 最通用,支持跨进程发信号
raise 自己(当前进程) 发送信号(自己吓自己 程序内部触发某个信号(模拟外部 kill) 调用它的当前进程 <signal.h> 相当于 kill(getpid(), signum)
abort 自己 发送 SIGABRT 信号(自己崩溃 程序发现不可恢复错误时触发异常中止 调用它的当前进程 <cstdlib>(C++),<stdlib.h>(C) 只能发 SIGABRT,直接终止程序并产生 core dump(若开启)

4. 硬件异常

1. 除以 0 导致异常

  • CPU 有除法指令,如果除数是 0,CPU 硬件级别检测到非法操作,硬件立刻触发,这个异常会立刻 中断当前指令执行,由 CPU 抛给操作系统。操作系统收到异常中断后,根据异常类型找对应的 异常处理程序
  • 如果是用户态进程除 0,OS 默认是:给该进程发送 SIGFPE (Floating Point Exception)信号。如果进程没处理这个信号,就会被 OS 杀掉并回收资源。

所以:除以 0 = 硬件发现错误 → 触发 CPU 异常 → OS 发 SIGFPE → 进程崩溃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void myhandler(int sig)
{
cout << "收到的信号是:" << sig << endl;
exit(0);
}

int main()
{
// signal(SIGFPE, myhandler);

cout << "这是除以 0 之前!" << endl;
sleep(1);

int a = 10;
a /= 0; // 触发 SIGFPE 信号

cout << "这是除以 0 之后!" << endl;
sleep(1);

return 0;
}

运行结果示例:

image-20250716145736498

2. 野指针导致异常

每个进程有自己的 虚拟地址空间,由操作系统 + CPU 的 MMU(内存管理单元,现代计算机会将 MMU 放入 CPU)一起管理。CPU 用 MMU 把“虚拟地址”翻译成“物理地址”;访问未映射或权限不符的地址 → MMU 触发 Page Fault → 内核一看是“合法缺页”就帮你补页,是“非法野指针”就发 SIGSEGV,默认把进程干掉。

所以:非法访问 = MMU 拦截 → Page Fault → OS 判定非法 → 发 SIGSEGV → 进程被杀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void myhandler(int sig)
{
cout << "收到的信号是:" << sig << endl;
exit(0); // 注释掉会导致程序无限循环,可以尝试注释掉观察现象
}

int main()
{
signal(SIGSEGV, myhandler);

cout << "这是程序开始之前!" << endl;
sleep(1);

int *p = nullptr;
*p = 10; // 野指针,会触发信号 SIGSEGV

cout << "这是程序结束之后!" << endl;
sleep(1);

return 0;
}

运行结果示例:

image-20250716150703445

5. 软件条件

SIGPIPE 就是典型的软件条件信号:管道通信:① 写端:进程 A ② 读端:进程 B。正常流程:A 写 → B 读 → 数据传递。

异常场景:

  • B 提前关闭读端(close(fd)),或者 B 异常退出。
  • A 还在不停 write(fd, ...)

SIGPIPE = “没人接水龙头却还在放水” → OS 发现了 → 给你发信号 → 不处理就直接 kill。

软件条件的信号本质上是 内核或程序自己产生 的,常用来定时器、子进程管理、超时处理。注意:不是硬件 CPU 抛出的硬件异常,而是 OS 根据进程/资源状态来调度。alarm 是其中最典型的。

1. alarm 函数

1. 功能

alarm 用来在指定的若干秒后,自动向当前进程发送 SIGALRM 信号(14 号信号)。常用于实现简单的超时控制,比如:

  • 程序运行超过 N 秒就触发中断。
  • 防止阻塞的 IO 卡死。
  • signal(SIGALRM, handler) 配合使用。
  • alarm 通常只能设置一个闹钟,再次调用会覆盖上一次设置。
2. 函数原型
1
2
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
3. 参数详解

seconds 多少秒后发出 SIGALRM,设为 0 则取消之前设置的闹钟。

4. 返回值
  • 返回之前设置的剩余秒数(若没有则返回 0)。
  • 如果要设置新的闹钟,返回值表示原闹钟还有多久触发。
5. 代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <unistd.h> // alarm
#include <signal.h> // signal
using namespace std;

void myhandler(int sig)
{
cout << "收到 SIGALRM 信号,编号:" << sig << endl;
exit(0); // 如果注释掉这里,也必须要注释掉 signal 函数(或者单独注释掉 alarm 函数),运行才会只显示前5秒的输出
}

int main()
{
signal(SIGALRM, myhandler);

cout << "设置闹钟,5 秒后触发 SIGALRM..." << endl;

alarm(5); // 5 秒后产生 SIGALRM

for (int i = 1; i <= 10; ++i)
{
cout << "程序运行中: " << i << " 秒" << endl; // 程序只会运行到 i=5 秒,然后被 SIGALRM 信号中断
sleep(1);
}

cout << "程序结束。" << endl;

return 0;
}

运行结果示例:

1
2
3
4
5
6
7
8
[hcc@hcss-ecs-be68 Signal Generation]$ ./alarm 
设置闹钟,5 秒后触发 SIGALRM...
程序运行中: 1 秒
程序运行中: 2 秒
程序运行中: 3 秒
程序运行中: 4 秒
程序运行中: 5 秒
收到 SIGALRM 信号,编号:14

父子进程 + alarm + signal 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <unistd.h> // fork(), alarm(), sleep()
#include <signal.h> // signal()
#include <sys/wait.h> // wait()
using namespace std;

void alarm_handler(int sig)
{
cout << "子进程收到 SIGALRM 信号,编号:" << sig << endl;
exit(0);
}

int main()
{
pid_t pid = fork();

if (pid == 0)
{
cout << "子进程启动,PID: " << getpid() << endl;
signal(SIGALRM, alarm_handler);

alarm(5); // 设置 5 秒后触发 SIGALRM

for (int i = 1; i <= 10; ++i)
{
cout << "子进程运行中: " << i << " 秒" << endl;
sleep(1);
}

cout << "子进程正常结束。" << endl;
}
else if (pid > 0)
{
cout << "父进程启动,PID: " << getpid() << ",等待子进程..." << endl;

int status;
waitpid(pid, &status, 0); // 阻塞等待子进程结束

cout << "父进程检测到子进程结束,父进程退出。" << endl;
}
else
{
perror("fork");
}

return 0;
}

运行结果示例:

1
2
3
4
5
6
7
8
9
10
[hcc@hcss-ecs-be68 Signal Generation]$ ./alarm2
父进程启动,PID: 1754,等待子进程...
子进程启动,PID: 1755
子进程运行中: 1 秒
子进程运行中: 2 秒
子进程运行中: 3 秒
子进程运行中: 4 秒
子进程运行中: 5 秒
子进程收到 SIGALRM 信号,编号:14
父进程检测到子进程结束,父进程退出。

2. 常用信号

使用 man 7 signal 命令(偏底部)查看:

image-20250716232959815

做一个简单的小翻译:

信号名 数值 默认动作 中文含义
SIGHUP 1 Term 挂起:控制终端关闭或父进程终止
SIGINT 2 Term 键盘中断(Ctrl+C)
SIGQUIT 3 Core 键盘退出(Ctrl+\),并生成 core dump
SIGILL 4 Core 非法指令(CPU 执行了未定义或错误的指令)
SIGABRT 6 Core 程序调用 abort(),异常终止并生成 core dump
SIGFPE 8 Core 浮点异常(除以 0、溢出)并生成 core dump
SIGKILL 9 Term 强制终止(不可捕获,不可忽略)
SIGSEGV 11 Core 无效内存引用(段错误,野指针)并生成 core dump
SIGPIPE 13 Term 管道破裂:写端写但没人读
SIGALRM 14 Term 定时器到期(由 alarm() 触发)
SIGTERM 15 Term 终止信号(程序优雅退出)
SIGUSR1 30,10,16 Term 用户自定义信号 1
SIGUSR2 31,12,17 Term 用户自定义信号 2
SIGCHLD 20,17,18 Ign 子进程停止或结束
SIGCONT 19,18,25 Cont 恢复已停止的进程
SIGSTOP 17,19,23 Stop 停止进程(不可捕获,不可忽略)
SIGTSTP 18,20,24 Stop 从终端暂停(Ctrl+Z)
SIGTTIN 21,21,26 Stop 后台进程请求终端输入
SIGTTOU 22,22,27 Stop 后台进程请求终端输出

默认动作关键说明:

  • 终止(Term) :终止进程。
  • 核心转储(Core) :终止进程并生成 core dump 文件(用于调试)。注意:一般虚拟机上是启用的,而云服务器上是关闭的!
  • 忽略(Ign) :忽略该信号。
  • 继续(Cont) :继续已暂停的进程。
  • 暂停(Stop) :暂停进程(进程挂起,状态变成 T)。
1. Core dump 是什么?

Core Dump 是程序崩溃时的“事故现场记录仪”,通过它开发者可以精准还原崩溃时的程序状态,是调试复杂问题的关键工具(事后调试)。合理配置 Core Dump 生成规则,并结合 GDB 等工具分析,能显著提升问题排查效率。打开 OS 的 Core Dump 功能后,一旦进程出现异常,OS 会将进程在内存中的运行信息给 Dump(转存)到进程当前目录(磁盘)上形成 core.pid 文件!

相关命令:

1
2
3
ulimit -c				# 查看当前进程最大 Core 文件大小(单位:KB)
ulimit -c unlimited # 临时对当前 shell 启用,关闭 shell 后失效(0 表示禁用,unlimited 表示 无限制,即允许生成)
ulimit -c 0 # 临时禁用

Core Dump 还有其他相关命令,这里就不过多赘述了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <unistd.h> // fork(), sleep(), getpid(), exit()
#include <sys/wait.h> // waitpid()
#include <cstdlib> // exit()
using namespace std;

int main()
{
pid_t id = fork();

if (id == 0)
{
int cnt = 5;

while (cnt)
{
cout << "我是子进程, 我的 pid 是: " << getpid() << ", 剩余次数: " << cnt << endl;

sleep(1); // 每秒打印一次
cnt--;
}

// 注释掉下面这2行,子进程会正常退出,core dump(核心转储)也会显示 0,不注释则显示1
int *p = nullptr;
*p = 123; // 访问空指针,必触发 SIGSEGV,从而显示 core dump(核心转储)

exit(0); // 子进程正常退出,返回码 0(永远走不到这里)
}

int status = 0;
pid_t rid = waitpid(id, &status, 0);

if (rid == id)
{
cout << "子进程结束,进程 id:" << rid << " "<< "退出码:" << ((status >> 8) & 0xFF) << endl;
cout << "退出信号: " << (status & 0x7F) << " " << "核心转储: " << ((status >> 7) & 1) << endl;
}

return 0;
}

在进程等待的章节中讲过 status 的位布局,这里深入理解一下:

image-20250414221815934

1
2
3
4
31                              16 15              8 7     7 6         0
+--------------------------------+------------------+-------+----------+
| 其他保留 | exit code(8位) | core? | signal(7位)|
+--------------------------------+------------------+-------+----------+

内核会把 子进程的退出信息 打包成一个 int 型整数,放到 status 里,这个整数就像一个 位图,把多个信息整理到了一块。这也是为什么没有 32、33 号信号的原因。

这个位图里面放了什么?

位区 意义 举例
高 8 位(位 8~15) 如果子进程是调用 exit 正常退出,这里是返回码 exit(42) → 高 8 位 = 42
低 7 位(位 0~6) 如果子进程是被信号杀死,这里放 信号编号 SIGSEGV → 11
第 7 位(位 7) 如果是信号杀死,且生成了 core dump,这里是 1 有 core dump
其他 还有一些特定状态(暂停/继续)可用宏判断 WIFSTOPPEDWIFCONTINUED

用位运算怎么拆?

表达式 意义
(status >> 8) & 0xFF 右移 8 位,取高 8 位 = exit code
status & 0x7F 低 7 位,表示终止信号编号
(status >> 7) & 1 第 7 位,表示是否生成 core dump

运行结果示例:

image-20250717002737659

Core Dump 的调试: 一般会显示信号信息、调用堆栈、寄存器状态、错误行号、变量值等一系列非常详细的信息!

image-20250717005041894

更多调试命令请自行百度,这里点到为止了。

安装调试包命令:

1
>sudo yum install debuginfo-install glibc-2.17-326.el7_9.3.x86_64 libgcc-4.8.5-44.el7.x86_64

2. 为什么 Core Dump 一般虚拟机上是启用的,而云服务器上是关闭的?
  1. 在虚拟机上:

    • 调试优先:在本地虚拟机(开发机、测试机),程序挂掉后要调试,最有用的证据就是 core 文件(它保存了崩溃时的内存、堆栈、寄存器)。

    • 单机单人可控:一般是自己用或者小团队用,没有什么安全风险,也不担心磁盘写 core。

    • 调试习惯:开发环境崩溃了,生成 core → gdb ./a.out core.xxx → 定位 bug,常规套路。

  2. 在云服务器上:

    • 生产环境以安全和稳定为主,不是以调试为主!

    • 安全隐私风险大: core dump 会把 整个进程内存快照 写到文件里,里面可能包含:明文账号密码、秘钥、用户数据(比如客户表单、交易信息)等,一旦留在磁盘或者被误传出去,就是安全事故。

  3. 影响稳定性: 仔细观察会发现:几十行的代码就会生成体积非常庞大的 core 文件。如果线上进程瞬间崩溃大量生成 core(一个 core 可以几个 GB),会把服务器磁盘打满,后果很严重(尤其是在大型服务器集群中非常显著!):

    • 其他服务写文件失败。

    • 数据库宕机。

    • 整个实例卡死。

  4. 运维风控: 生产环境一般靠 日志监控告警 和最小可复现 Demo 来定位问题,不直接看 core dump。有些还存在自动化的专业 debug 工具/脚本等。

3. 普通信号 VS 实时信号(了解)

早期 UNIX

  • 信号是最简单的 IPC,设计得很“轻”:只要告诉你“有事情发生了”,具体什么事情、来了几次都不重要。
  • 所以,重复来的同一个信号只保留一个,省开销。

后来的实时场景

  • 有些实时系统(工业控制、航空、机器人)需要精确传递多次事件(每次都重要)。
  • 所以 Linux 引入了 实时信号,内核会把多个相同实时信号放到队列里,保证每一个都能送达。
维度 普通信号 实时信号
范围 1 ~ 31 32 ~ 64
是否排队 不排队,同类型只保留 1 个 排队,同类型多个也可以同时排队
发送顺序 不保证顺序 保证顺序 FIFO
优先级 没有严格优先级 实时信号优先级可自定义
用途 kill、Ctrl+C、kill -9、常见系统信号 高实时性要求的 IPC,比如实时控制、进程间发送大量通知
标准来源 早期 UNIX POSIX.1b