上篇热榜文章:【Linux进程间通信】硬核剖析:消息队列、信号量、内核IPC资源统一管理与mmap加餐
导语
在 Linux 学习的漫漫长路中,进程信号(Signal) 绝对是一个让人又爱又恨的知识点。它是操作系统和进程沟通的重要桥梁。为什么程序出现野指针直接就崩溃了?为什么 Ctrl + C 能杀掉一个死循环的程序?后台进程和前台进程到底有啥区别?
本篇文章,我们将结合生活隐喻与底层源码,把你零散的信号知识全部串联起来。不仅会教你如何使用信号 API,更会向下击穿到底层硬件,带你硬核剖析 CPU、操作系统与进程之间是如何协同处理异常的。准备好发车了吗?我们开始!
目录
[1.3 内核是如何保存信号的?(核心原理)](#1.3 内核是如何保存信号的?(核心原理))
[2. 信号的处理机制与 API 实战](#2. 信号的处理机制与 API 实战)
[2.1 信号捕捉 API:signal 函数](#2.1 信号捕捉 API:signal 函数)
[2.2 面试必问:能把所有信号都忽略,造一个杀不死的进程吗?](#2.2 面试必问:能把所有信号都忽略,造一个杀不死的进程吗?)
[3.1 终端按键与前后台进程机制](#3.1 终端按键与前后台进程机制)
[3.2 系统命令与系统调用产生](#3.2 系统命令与系统调用产生)
[1. kill 函数(发给指定进程)](#1. kill 函数(发给指定进程))
[2. raise 函数(自己杀自己)](#2. raise 函数(自己杀自己))
[3.3 软件条件产生(系统闹钟与 IO 效率)](#3.3 软件条件产生(系统闹钟与 IO 效率))
[4. 硬核剖析:硬件异常是如何终结进程的?](#4. 硬核剖析:硬件异常是如何终结进程的?)
[补充概念:什么是 Core Dump?](#补充概念:什么是 Core Dump?)
[5. 本章总结](#5. 本章总结)
1.理解信号
1.1什么是信号
信号是由用户或系统通过异步通知机制,异步的给进程发送就绪事件的方式,属于软中断。
怎么理解"异步"?(结合快递员的例子) 假设你在网上买了东西,快递员随时可能送来。但在他来之前,你可以打游戏、敲代码。当快递员到了楼下给你打电话(发信号 ),你因为正在打团战,不能立刻下楼,于是你跟他说"放外卖柜"(信号被临时保存 )。等你打完游戏,你再去取快递并拆开(在合适的时候处理信号)。
这就是信号的核心特征:
-
识别能力是内置的:就像你知道红绿灯红灯停绿灯行,进程本身在被内核程序员编写时,就已经"认识"了所有的信号。
-
异步发生:进程不知道信号什么时候来,信号的产生和进程的执行是并发的。
-
延迟处理 :进程如果正在执行更高优先级的内核代码,并不会立即处理信号,这就要求进程必须有保存信号的能力。
1.2查看系统中的信号
通过命令 kill -l 用于查看所有信号:

Linux 中共有 64 个信号:
-
1 ~ 31 号:普通信号(我们日常开发和本章研究的重点)。
-
34 ~ 64 号:实时信号(底层用队列实现,必须立即处理,暂不深究)。
每个信号都有一个宏定义名称(定义在 signal.h 中),比如 SIGINT 其实就是 2。你可以通过 man 7 signal 查看每个信号的来源、默认行为和用途。

通过以下命令,可以查看每个信号的来源,默认行为和用途:
man 7 signal

1.3 内核是如何保存信号的?(核心原理)
既然进程需要临时保存信号,它保存在哪里呢?答案是:进程控制块(PCB,即 task_struct)中。
因为普通信号只有 31 个,所以 Linux 内核巧妙地采用了一个 32 位的无符号整型(uint32_t / long int)变量,把它当成位图(Bitmap)来使用!
-
比特位的位置:代表信号的编号(如第 2 个比特位代表 2 号信号)。
-
比特位的内容(0或1):代表是否收到了该信号。
突破点理解:什么是"发信号"? 由于位图是保存在内核空间的 PCB 中的,普通用户根本没有权限修改。所以,发信号的本质,其实是操作系统去修改目标进程 PCB 中特定位图的比特位! 用户只是通过系统调用(如 kill)去"请求"操作系统办事而已。
2. 信号的处理机制与 API 实战
当进程在"合适的时候"去处理信号时,通常有以下三种处理动作(统称为信号捕捉):
-
执行默认动作(Default):大多数信号的默认动作是直接干掉当前进程。
-
忽略信号(Ignore):收到信号了,但在位图里清零,假装没看见,什么都不做。
-
自定义捕捉(Catch):让内核暂停默认行为,转而去执行我们程序员自己写的处理函数。
2.1 信号捕捉 API:signal 函数
我们可以使用 signal 函数来更改信号的处理动作,通过命令 man signal 用于查看信号类型:
cpp
NAME
signal - ANSI C signal handling
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
其中signum:你要捕捉的信号编号。(*sighandler_t)为函数指针类型,sighandler_t handler是对指定信号未来进行自定义处理的一种延时设定,返回值sighandler_t代表处理信号通过旧的处理方法进行处理。
实战代码:测试多种处理情况
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
// sig:表示的是收到的信号编号
void handlersig(int sig)
{
std::cout << "正在处理一个信号, pid: " << getpid() << "sig number: " << sig << std::endl;
}
int main()
{
signal(2, handlersig);
while(true)
{
sleep(1);
std::cout << "进程pid: " << getpid() << std::endl;
}
return 0;
}
运行结果(通过kill -9结束进程):

信号处理的多种情况:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
// sig:表示的是收到的信号编号
void handlersig(int sig)
{
// switch(sig)
// {
// case 1:
// break;
// case 2:
// break;
// // ...
// }
std::cout << "正在处理一个信号, pid: " << getpid() << "sig number: " << sig << std::endl;
}
int main()
{
signal(2, handlersig); // 自定义方法
// signal(3, handlersig);
// signal(4, handlersig);
// signal(2, SIG_IGN); // 忽略信号
signal(2, SIG_DFL); // 系统默认
while(true)
{
sleep(1);
std::cout << "进程pid: " << getpid() << std::endl;
}
return 0;
}
注意:signal 函数仅仅是对信号处理方式的一种"延时注册",如果没有收到对应信号,handlersig 永远不会被调用。
2.2 面试必问:能把所有信号都忽略,造一个杀不死的进程吗?
如果在上述代码中加入一个循环:for(int i=1; i<=31; i++) signal(i, SIG_IGN);,是不是这个进程就无敌了,再也关不掉了?
cpp
int main()
{
for(int signo = 1; signo <= 31; signo++)
signal(signo, SIG_IGN);
while(true)
{
sleep(1);
std::cout << "进程pid: " << getpid() << std::endl;
}
return 0;
}
答案是:否! 操作系统的设计者早就想到了这一点。在 Linux 中,9号信号(SIGKILL)和 19号信号(SIGSTOP)是绝对的霸主 。它们不允许被自定义捕捉,也不允许被忽略 。无论你代码怎么写,只要操作系统发出 kill -9,该进程必死无疑。
3.信号的产生方式

3.1 终端按键与前后台进程机制
我们在终端运行程序时,最常用的杀进程方式就是按下快捷键。
-
Ctrl + C:发送 2 号信号SIGINT(中断)。 -
Ctrl + \:发送 3 号信号SIGQUIT(退出并产生 core dump)。 -
Ctrl + Z:发送 20 号信号SIGTSTP(暂停进程)。
信号图如下:

作用如下:

键盘是怎么给进程发信号的?
-
键盘敲击产生硬件中断。
-
CPU 感知到中断,去中断向量表里找到操作系统内置的键盘处理驱动。
-
操作系统读取键盘数据,发现是
Ctrl+C的组合键,于是将其解释为 2 号信号,并把信号写给当前前台进程的 PCB 位图中。
重点科普:前台进程 vs 后台进程
-
前台进程 :拥有终端控制权,能直接获取键盘输入的进程(任何时刻终端只有一个前台进程/进程组)。
-
后台进程 :在命令后加
&运行(如./myproc &)。它失去了对键盘输入的响应权,所以你对后台进程按Ctrl+C是没用的! 必须用kill命令才能杀掉它。 -
可以使用
jobs查看后台任务,使用fg 任务编号将其提至前台。
这里有一个极其硬核的问题:操作系统如何知道这个 Ctrl + C 的信号应该发送给哪一个进程呢? 结论是:键盘产生的信号只能发送给"前台进程(组)",而且前台进程只有一个!
我们通过一组管道命令来验证:输入一串命令:sleep 10000 | sleep 20900 | sleep 30000
-
PID:进程ID。
-
PGID:进程组(Process Group)。多个通过管道协同工作的进程属于同一个进程组,完成同一个"作业"。
-
SID:会话组(Session)。

上图中PGID指进程组,SID指会话组,而其中相同的sleep的进程组是同一个,SID和bash进程号相同,而bash的SID就是他自己,我们通过下图解释,也就是说用户登录操作系统,都要建立会话,打开终端文件,通过命令行操作启动进程,本质是在会话中启动进程组。

通过上述截图,我们也发现当一个任务在运行时,其显示S+,那其中的 加号 是指前台运行,如果在执行命令后加 & ,就表示后台运行,示例如下:

通过fg + 任务号的命令可以将后台任务提到前台。
那么显示的 [1] 是什么含义?[1]是指当前进程的任务号。
对于后台进程,我们可以通过jobs这个命令进行查看:

但是对于后台进程,我们不能通过ctrl + c的方式将其结束运行。
前后台含义:能够直接获取用户输入的进程叫做前台进程,否则叫做后台进程。也就是说,谁拥有终端文件(主要是键盘),谁就是前台。
结论:
作业是由进程组来完成的。
会话中至少包含一个进程组(作业)。
在一个会话中,任何时刻只允许一个进程组在前台。(当一个进程组在前台时,bash会自动变为后台,不会接收到命令)
注意:
signal函数仅仅是设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作。如果后续特定信号没有产⽣,设置的捕捉函数永远也不会被调用
Ctrl + C 产⽣的信号只能发给前台进程。⼀个命令后⾯加个&可以放到后台运行,这样 Shell不必等待进程结束就可以接受新的命令,启动新的进程。
Shell可以同时运行⼀个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产⽣的信号。
前台进程在运⾏过程中用户随时可能按下 Ctrl C 而产⽣⼀个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
3.2 系统命令与系统调用产生
除了键盘,我们也可以主动调用系统 API 来发信号。
1. kill 函数(发给指定进程)
通过命令 man 2 kill查看:
cpp
NAME
kill - send signal to a process
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <signal.h>
int kill(pid_t pid, int sig);
根据以上条件,我们也可以自己实现kill命令:
cpp
#include <iostream>
#include <string>
#include <signal.h>
#include <unistd.h>
void Usage(std::string proc)
{
std::cerr << "Usage:\n" << proc << "signmber\n";
}
// ./mykill signumber pid
// ./mykill -9 1232144
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
int signumber = std::stoi(argv[1]);
pid_t pid = std::stoi(argv[2]);
std::cout << "send" << signumber << "to " << pid << std::endl;
int n = kill(pid, signumber);
(void)n;
return 0;
}
2. raise 函数(自己杀自己)
相当于 kill(getpid(), sig)。
cpp
NAME
raise - send a signal to the caller
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <signal.h>
int raise(int sig);
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
// sig:表示的是收到的信号编号
void handlersig(int sig)
{
std::cout << "正在处理一个信号, pid: " << getpid() << "sig number: " << sig << std::endl;
}
int main()
{
for(int signo = 1; signo <= 31; signo++)
signal(signo, handlersig);
while(true)
{
sleep(1);
std::cout << "进程pid: " << getpid() << std::endl;
raise(SIGINT);
}
return 0;
}
运行结果:

3. abort 函数(异常终止自己)
给自己发送 6号信号 SIGABRT。注意:即使你通过 signal 捕捉了 6号信号,abort 执行完自定义函数后,依然会强制终止进程!
使当前进程接收到信号而异常终止。
cpp
NAME
abort - cause abnormal process termination
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <stdlib.h>
[[noreturn]] void abort(void);
代码:
cpp
int main()
{
for(int signo = 1; signo <= 31; signo++)
signal(signo, handlersig);
while(true)
{
sleep(1);
std::cout << "进程pid: " << getpid() << std::endl;
// raise(SIGINT);
abort();
}
return 0;
}
运行结果:

可以看出,对于abort函数可以设置捕捉动作,但是最后还是会终止进程。
3.3 软件条件产生(系统闹钟与 IO 效率)
所谓的"软件条件",指的是由软件内部状态触发的机制。典型的例子是向已关闭的管道写数据产生的 SIGPIPE,以及定时器超时 产生的 SIGALRM。
alarm 函数就是系统闹钟:
cpp
NAME
alarm - set an alarm clock for delivery of a signal
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
RETURN VALUE
alarm() returns the number of seconds remaining until any previously scheduled alarm was due to be delivered, or zero if there was no
previously scheduled alarm.
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当期进程。
基本alarm验证-体会IO效率问题
程序 A(纯内存运算):
cpp
// IO 少
#include <iostream>
#include <unistd.h>
#include <signal.h>
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;
}
结果:
cpp
$ g++ alarm.cc -o alarm
$ ./alarm
count : 492333713
程序 B(高频 I/O):
cpp
// 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;
}
结果:
cpp
... ...
count : 107148
count : 107149
Alarm clock
结论: 纯内存储备运算的速度,是读写外设(I/O)速度的几千倍!这也是我们在开发高性能服务端时,为什么要极力避免频繁同步 I/O 的原因。
闹钟返回值验证
闹钟会响一次,默认终止进程,
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
举例一:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
int cnt = 0;
// sig:表示的是收到的信号编号
void handlersig(int sig)
{
std::cout << "正在处理一个信号, pid: " << getpid() << "sig number: " << sig << "cnt: " << cnt << std::endl;
exit(1); // 没有这个exit就会死循环
}
int main()
{
alarm(10); // 一秒的闹钟
sleep(3);
int n = alarm(5); // 闹钟返回时间是上一闹钟剩余时间
printf("remain: %d\n", n);
signal(SIGALRM, handlersig);
while(true)
{
// std::cout << "counter: " << cnt << std::endl;
cnt++;
}
return 0;
}
结果:

举例二:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
int cnt = 0;
// sig:表示的是收到的信号编号
void handlersig(int sig)
{
std::cout << "正在处理一个信号, pid: " << getpid() << "sig number: " << sig << "cnt: " << cnt << std::endl;
exit(1); // 没有这个exit就会死循环
}
int main()
{
alarm(10); // 十秒的闹钟
int n = alarm(0); // 取消闹钟,返回剩余时间
printf("remain: %d\n", n);
sleep(100);
signal(SIGALRM, handlersig);
while(true)
{
std::cout << "counter: " << cnt << std::endl;
cnt++;
}
return 0;
}
结果:

举例三:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
int cnt = 0;
// sig:表示的是收到的信号编号
void handlersig(int sig)
{
std::cout << "正在处理一个信号, pid: " << getpid() << "sig number: " << sig << "cnt: " << cnt << std::endl;
exit(1); // 没有这个exit就会死循环
}
int main()
{
alarm(2); // 一次性闹钟 && 对你这个进程,只会有一个闹钟
signal(SIGALRM, handlersig);
while (true)
{
std::cout << "counter: " << cnt << std::endl;
cnt++;
sleep(1);
}
return 0;
}
结果:

创建周期性闹钟
代码:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
int cnt = 0;
// sig:表示的是收到的信号编号
void handlersig(int sig)
{
std::cout << "正在处理一个信号, pid: " << getpid() << "sig number: " << sig << "cnt: " << cnt << std::endl;
alarm(2);
}
int main()
{
alarm(2); // 一次性闹钟 && 对你这个进程,只会有一个闹钟
signal(SIGALRM, handlersig);
while (true)
{
std::cout << "counter: " << cnt << std::endl;
cnt++;
sleep(1);
}
return 0;
}
结果:

理解软件条件
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因为操作系统内部或外部软件操作而触发的信号产生。
理解系统闹钟
系统闹钟,本质就是操作系统必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术。现代Llinux是提供定时功能的,定时器也要被管理:先描述,再组织。内核中的定时器数据结构是:
cpp
struct timer_list
{
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_t_base_s *base;
};
操作系统管理定时器,采用的是时间轮的做法,我们为了简单理解,可以把它在组织成为"堆结构"
4. 硬核剖析:硬件异常是如何终结进程的?
平时写 C/C++,如果写出野指针或者除零错误,程序会立刻崩溃。
-
野指针:引发 11 号信号(
SIGSEGV,段错误) -
除零错误:引发 8 号信号(
SIGFPE,浮点异常)
野指针代码:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
// sig:表示的是收到的信号编号
void handlersig(int sig)
{
std::cout << "正在处理一个信号, pid: " << getpid() << "sig number: " << sig << std::endl;
}
int main()
{
for(int signo = 1; signo <= 31; signo++)
signal(signo, handlersig);
while(true)
{
std::cout << "进程pid: " << getpid() << std::endl;
sleep(1);
int *p = nullptr;
*p = 100; // 野指针报错
}
return 0;
}
结果发现信号捕捉一直被触发,信号码为11号(段错误):

除零错误代码,加入exit(1),确保避免死循环:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
// sig:表示的是收到的信号编号
void handlersig(int sig)
{
std::cout << "正在处理一个信号, pid: " << getpid() << "sig number: " << sig << std::endl;
exit(1); // 没有这个exit就会死循环
}
int main()
{
for(int signo = 1; signo <= 31; signo++)
signal(signo, handlersig);
while(true)
{
std::cout << "进程pid: " << getpid() << std::endl;
sleep(1);
// int *p = nullptr;
// *p = 100; // 野指针报错
int a = 10;
a /= 0;
}
return 0;
}
结果信号为8号(SIGFPE):

那么问题来了,进程异常是如何被 OS 识别并解释为信号的?为什么程序会崩溃?
请牢记这句终极结论:
C/C++程序的崩溃,本质是硬件异常触发了操作系统介入,最终操作系统以发送"软件信号"的形式,强制终结了目标进程。
我们以除零或野指针为例,详细剖析这其中的崩溃四部曲逻辑:

本质是硬件异常触发操作系统介入,最终以信号的形式发送给进程。
崩溃逻辑:
CPU运算单元报错。结合指令执行,解释除零导致 ALU 运算异常,或野指针导致 MMU 寻址失败。重点提及状态寄存器(如 RFLAGS 标志位)的改变。
-
动作: CPU 的运算单元(ALU)在执行除法指令
10 / 0时,发现除数为 0,无法计算。或者在执行野指针引发的内存访问时,MMU(内存管理单元)发现虚拟地址无法映射到物理地址。 -
结果: 硬件出错了!CPU 内部的状态寄存器(图中画的
RFLAGS)中的特定标志位会被设置,比如溢出标志OF (Overflow Flag),或者触发缺页中断异常。
操作系统(OS)接管。 CPU 一旦发生硬件级别的计算异常或寻址异常,会立刻触发一个硬件中断/异常 (Exception)。OS 提前在系统中注册了中断处理程序,所以 OS 会立刻接管控制权。
**current指针锁定目标。**OS 被唤醒后,需要查明是谁搞出了这个硬件错误。
- 结果: OS 通过内核中的一个宏/全局指针
current(它指向当前正在 CPU 上运行的进程的 PCB,即task_struct),立刻锁定了正在执行10 / 0的"肇事进程"。
硬件异常转换为软件信号。
-
动作: OS 将底层的硬件异常翻译成上层的软件信号。除零错误被翻译成
SIGFPE(8号信号,Floating-point exception);野指针被翻译成SIGSEGV(11号信号,段错误)。 -
结果: OS 将这个信号发送给
current指向的进程。该进程在收到SIGFPE或SIGSEGV后,执行默认处理动作------终止进程并生成 Core Dump 文件 (即板书右侧的Core和终止!)。
**最终结论:**C/C++程序一旦出现异常,程序崩溃是由硬件异常,操作系统识别硬件异常,给目标进程发送特定的信号,从而让进程收到信号,处理信号使其崩溃(崩溃本质是让系统恢复正常)
追问:为什么自定义捕捉异常会陷入"死循环"?
如果我们在代码中把除零错误产生的 8 号信号捕捉了,并且在捕捉函数里不写 exit(1) 退出:
void handlersig(int sig){
std::cout << "正在处理异常信号: " << sig << std::endl;
// 没有 exit(1);
}
int main(){
signal(SIGFPE, handlersig);
int a = 10; a /= 0;
}
结果是:屏幕会疯狂无限地刷屏打印这句话,陷入死循环!为什么?
原理解析:
-
异常触发 -> OS 发信号 -> 执行捕捉函数。因为没有退出,函数执行完毕后,操作系统会将执行流返回到用户态原来的上下文中。
-
但是!引起错误的 CPU 汇编指令(即那句除法)并没有执行成功,更要命的是,CPU 内部状态寄存器的异常标志位也没有被清空还原!
-
程序计数器(PC 指针)依然指着那条出错的指令。CPU 会再次尝试执行,立刻又发生硬件异常 -> OS 再次发信号 -> 再次捕捉...... 这就造成了永无休止的死循环陷阱!因此,硬件异常引发的信号,捕捉后如果无法从根本上修复硬件级错误,就必须通过
exit()让进程老老实实退出!
补充概念:什么是 Core Dump?
在执行异常崩溃时,我们常看到终端输出 Segmentation fault (core dumped)。

-
Term (正常终止):直接干掉进程。
-
Core (核心转储) :由于进程内部自己引起的异常,操作系统不仅会干掉进程,还会在它死亡的瞬间,将其内存中的核心数据、寄存器状态统统打包保存到磁盘上的
core文件中 。 这方便了程序员事后通过 GDB 等调试工具直接加载core文件(Post-mortem Debug),瞬间定位到崩溃代码在哪一行!

5. 本章总结
今天这篇文章很长,但理顺之后你会觉得酣畅淋漓。我们总结一下最核心的要点:
-
信号的本质机制 :信号是被保存在进程 PCB 的一个
uint32_t位图中的。发信号,本质就是 OS 去修改了目标进程的这个位图。 -
前后台控制机理 :谁拥有键盘输入权谁就是前台进程。
Ctrl+C产生的中断信号只能发送给前台进程(组)。 -
软硬件联动之美 :C/C++程序的异常崩溃,本质是底层硬件异常(如 ALU、MMU 报错)触发操作系统介入,最终操作系统以发送"软件信号"的形式终结了进程。 如果你用
signal捕捉了这些异常却不退出,由于硬件寄存器错误没被抹除,CPU 将陷入死循环重试。
理解了这些,以后再面对 Segmentation fault 时,你的脑海里浮现的将不再是苍白的"代码写错了",而是整个 CPU、内核与用户态进程之间华丽的联动画面!
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!下一篇,我们将继续深入内核,揭秘信号在"保存"与"捕捉"之间的那些不为人知的故事!