
🔥小叶-duck:个人主页
❄️个人专栏 :《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一. 信号的快速认知:从生活场景到技术本质](#一. 信号的快速认知:从生活场景到技术本质)
[1.1 生活角度理解信号](#1.1 生活角度理解信号)
[1.1.1 信号的本质](#1.1.1 信号的本质)
[1.1.2 生活中有哪些信号?](#1.1.2 生活中有哪些信号?)
[1.1.3 收到一个信号会立即处理它吗?](#1.1.3 收到一个信号会立即处理它吗?)
[1.1.4 信号怎么处理?](#1.1.4 信号怎么处理?)
[1.1.5 总结生活角度理解信号](#1.1.5 总结生活角度理解信号)
[1.2 技术视角的信号定义](#1.2 技术视角的信号定义)
[1.3 查看系统信号:kill -l 命令](#1.3 查看系统信号:kill -l 命令)
[二. 信号的产生:5 种核心方式](#二. 信号的产生:5 种核心方式)
[2.1 系统命令产生信号(kill 命令)](#2.1 系统命令产生信号(kill 命令))
[2.2 终端按键产生信号(键盘,最常用)](#2.2 终端按键产生信号(键盘,最常用))
[2.2.1 Ctrl+C:SIGINT(2 号信号)](#2.2.1 Ctrl+C:SIGINT(2 号信号))
[2.2.2 Ctrl+\:SIGQUIT(3 号信号)](#2.2.2 Ctrl+\:SIGQUIT(3 号信号))
[2.1.3 Ctrl+Z:SIGTSTP(20 号信号)](#2.1.3 Ctrl+Z:SIGTSTP(20 号信号))
[2.3 函数调用产生信号(编程触发)](#2.3 函数调用产生信号(编程触发))
[2.3.1 kill 函数:向指定进程发送信号](#2.3.1 kill 函数:向指定进程发送信号)
[2.3.2 raise 函数:向当前进程发送信号](#2.3.2 raise 函数:向当前进程发送信号)
[2.3.3 abort 函数:异常终止进程](#2.3.3 abort 函数:异常终止进程)
[2.3.4 三个系统调用层层递进](#2.3.4 三个系统调用层层递进)
[2.4 硬件异常产生信号(CPU / 内存错误)](#2.4 硬件异常产生信号(CPU / 内存错误))
[2.4.1 除零错误:SIGFPE(8 号信号)](#2.4.1 除零错误:SIGFPE(8 号信号))
[2.4.2 非法内存访问:SIGSEGV(11 号信号)](#2.4.2 非法内存访问:SIGSEGV(11 号信号))
[2.4.3 关键说明](#2.4.3 关键说明)
[2.5 软件条件产生信号(定时 / 管道异常)](#2.5 软件条件产生信号(定时 / 管道异常))
[2.5.1 管道破裂信号(SIGPIPE)](#2.5.1 管道破裂信号(SIGPIPE))
[2.5.2 alarm 函数:定时器信号(SIGALRM)](#2.5.2 alarm 函数:定时器信号(SIGALRM))
[2.5.3 关于 alarm 的应用场景和管理(先描述,再组织)](#2.5.3 关于 alarm 的应用场景和管理(先描述,再组织))
前言
在 Linux 系统中,信号是一种经典的进程间异步通信机制,也是操作系统响应异常、处理中断的核心手段。从用户在终端按下 Ctrl+C 终止前台进程,到程序因访问非法内存触发段错误崩溃,再到定时器超时向进程发送通知,这些常见场景的背后都是信号在发挥作用。简单来说,信号就是内核写给进程的一张"紧急便条",告知其某个事件已经发生。由于信号的到来是异步的,进程无法预知其具体时机,只能在收到通知后暂停当前工作并予以响应。本文将从生活类比、核心概念与实战用法三个维度,由浅入深地带你理解 Linux 信号的底层逻辑与具体操作。
一. 信号的快速认知:从生活场景到技术本质
1.1 生活角度理解信号
用 "快递收发" 的场景类比信号处理流程,瞬间理清核心逻辑:
- 信号识别:你知道快递来了要取(进程能识别系统预设的信号,如SIGINT);
- 信号产生:快递员打电话通知(信号由内核或其他进程发送);
- 信号未决:你正在打游戏,5 分钟后才去取(信号产生后未立即处理,处于 "未决" 状态);
- 信号递达:游戏结束后取快递(进程在合适时机执行信号处理动作);
- 处理方式 :
- 默认动作(打开快递进行使用);
- 自定义动作(送给朋友);
- 忽略(拿到快递但扔在一边)。
核心结论 :信号是异步通知,进程无法预知信号何时到来,但提前知道如何处理。
1.1.1 信号的本质

什么是通知?什么是异步?
- 通知:这个通知需要我们理解一下------事件通知。
- 异步:通知的到来,跟我不同步。
举个例子,我点了个外卖,然后就打游戏,外卖小哥送货上门,敲门是一个通知,说明外卖到了,我和外卖小哥是不同步的。
异步关系就是没关系(你做你的,我做我的);同步关系就是有关系(我得等你做完再做)。

- 再比如,比如我是一个老师,我讲课,突然快递电话打过来了,我叫张三去帮我取快递,但是我等张三找完快递回来再继续讲,这就是个同步的关系,张三不回来,我就不往下讲。
- 如果我继续讲课,张三也在找快递,我讲课、他帮我办事,这个就是个异步的关系------我讲我的课,张三找他要帮我找的快递。
1.1.2 生活中有哪些信号?

上面这些所有的信号产生的几乎都是异步的。
- 人能够识别对应的、甚至还没有发生的这些信号
人为什么能够识别这些信号?
人是经过教育的,早就知道信号对应处理动作的对应关系。
操作系统给目标进程发送信号,目标进程能不能识别呢?
- 答案是:进程天然能够识别------进程和信号都是程序员写的代码 (进程相当于我们人,已经被程序员教育 过了,程序员设计好了 )
进程能够识别信号,并且已经知道怎么处理信号了。
1.1.3 收到一个信号会立即处理它吗?
当我们收到一个信号,准备处理这个信号,这里的我们即进程,会立即处理这个信号吗?有时候中断不了呢!处理不了信号。
- 对于信号的处理------信号可能会立即处理 ,也可能在合适的时候会处理。
既然有一定概率不会立即处理,那么得要有把信号临时保存 起来的能力------不会立即处理,就得有临时保存 的能力。
比如像生活中外卖小哥打电话,答应去取外卖之后如果没有把这件事记下来(保存信号) ,是不是就打游戏打着打着就忘了取外卖了。
1.1.4 信号怎么处理?
- 默认:我后面打完游戏去拿外卖回来吃
- 忽略:我直接就不想拿了(但是并不是忘了,只是不想)
- 自定义:比如我拿完外卖直接丢了或者干别的事,自定义行为
以红绿信号灯为例:
- 红灯------自觉遵守交通规则停下来------默认信号。
- 收到信号,也处理了,但是处理的方式是忽略,也就是闯红灯------忽略信号。
- 红灯亮了,别人要么忽略要么停下,而你在莫名其妙地跳舞------自定义信号。
1.1.5 总结生活角度理解信号
- 设别信号是内置的,进程能自己识别信号,是内核程序员写的内置特性。
- 信号的处理方法,在信号产生之前就已经准备好了。
- 处理信号不是立即处理的,因为我可能正在做优先级更高 的事情,会选择合适的时候进行处理。比如我在打游戏,外卖员给我打电话叫拿外卖,那我肯定是先忙完手头的事再去拿。
- 信号不会被立即处理 ,所以就注定了进程要有临时保存信号的能力!
- 信号处理的动作有三种:默认、忽略、自定义 ,后续都叫信号捕捉。


1.2 技术视角的信号定义
信号是 Linux 中进程间事件异步通知 的一种方式,属于**"软中断"** ------ 模拟硬件中断的行为(硬件中断发给 CPU,信号发给进程),用于处理突发事件(如用户中断 、程序异常 、定时触发等)。
关键特性:
- 异步性:信号的产生与进程的控制流程无关,进程执行到任意位置都可能收到信号;
- 预定义动作:每个信号都有默认处理动作(终止、忽略、Core Dump 等),进程可自定义处理逻辑;
- 内核转发:信号的产生、发送、递达均由内核管理,进程仅需关注处理动作;
- 前台进程专属 :Ctrl+C 等终端按键 产生的信号,仅发送给前台进程 (后台进程 需用kill命令发送)。
1.3 查看系统信号:kill -l 命令
Linux 系统支持 64 种信号(34 以下为常规信号 ,34 以上为实时信号 ),通过kill -l可查看所有信号的编号和名称:
bash
kill -l
# 输出示例(核心信号):
1) SIGHUP 2) SIGINT 3) SIGQUIT 9) SIGKILL 11) SIGSEGV
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD 20) SIGTSTP
核心信号说明(后续都会进行讲解):
| 信号编号 | 信号名称 | 产生场景 | 默认动作 |
|---|---|---|---|
| 2 | SIGINT | 按下 Ctrl+C |
终止进程 |
| 3 | SIGQUIT | 按下 Ctrl+\ |
终止进程 + Core Dump |
| 6 | SIGABRT | abort 函数调用 |
终止进程 + Core Dump |
| 8 | SIGFPE | 算术异常(如除零) | 终止进程 + Core Dump |
| 9 | SIGKILL | kill -9 PID 命令 |
强制终止进程(不可捕捉 / 忽略) |
| 11 | SIGSEGV | 非法内存访问(野指针) | 终止进程 + Core Dump |
| 14 | SIGALRM | alarm 函数超时 |
终止进程 |
| 15 | SIGTERM | kill PID 命令默认信号 |
终止进程(可捕捉) |
| 19 | SIGSTOP | kill -STOP PID 等 |
暂停进程(不可捕捉 / 忽略) |
| 20 | SIGTSTP | 按下 Ctrl+Z |
暂停进程 |
二. 信号的产生:5 种核心方式
前置储备:

2.1 系统命令产生信号(kill 命令)
kill命令是发送信号的常用工具,本质是调用kill系统函数,语法:
bash
kill -信号编号 进程PID
kill -信号名称 进程PID

实战案例:用 kill 命令发送 SIGSEGV 信号
cpp
// 程序:死循环运行,等待外部信号
#include <iostream>
#include <unistd.h>
int main()
{
std::cout << "进程PID:" << getpid() << ",等待信号(用kill命令测试)..." << std::endl;
while (true)
{
sleep(1);
}
return 0;
}
操作步骤:
- 编译运行程序,记录 PID(如 213784);
- 打开新终端,发送 SIGSEGV(11 号信号,段错误):
bash
kill -11 213784
# 或 kill -SIGSEGV 213784
原终端输出:Segmentation fault (core dumped),进程终止。
2.2 终端按键产生信号(键盘,最常用)
终端通过组合键产生预设信号,用于控制前台进程,核心组合键及对应信号:


2.2.1 Ctrl+C:SIGINT(2 号信号)
默认动作 :终止前台进程,可通过signal 函数自定义捕捉。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
// 自定义信号处理函数
void sigint_handler(int signum)
{
std::cout << "\n进程[" << getpid() << "]捕获到SIGINT信号(编号:" << signum << "),未终止!" << std::endl;
}
int main()
{
std::cout << "进程PID:" << getpid() << ",等待信号(按Ctrl+C测试)..." << std::endl;
// 注册SIGINT信号的处理函数
signal(SIGINT, sigint_handler);
// 死循环等待信号
while (true)
{
sleep(1);
std::cout << "运行中..." << std::endl;
}
return 0;
}
编译运行:
bash
g++ sigint_demo.cc -o sigint_demo
./sigint_demo
效果 :按下Ctrl+C 后,进程不会终止 ,而是执行自定义处理函数也就是打印出一段文字并继续运行。

补充知识:前台进程/后台进程及两者如何切换?


什么叫做给进程发送信号?发送信号的本质是什么?

2.2.2 Ctrl+\:SIGQUIT(3 号信号)
默认动作:终止进程 + 生成 Core Dump 文件(用于事后调试),同样可自定义捕捉。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void sigquit_handler(int signum)
{
std::cout << "\n进程[" << getpid() << "]捕获到SIGQUIT信号(编号:" << signum << ")" << std::endl;
}
int main()
{
std::cout << "进程PID:" << getpid() << ",等待信号(按Ctrl+\\测试)..." << std::endl;
signal(SIGQUIT, sigquit_handler);
while (true)
{
sleep(1);
}
return 0;
}
关键说明:Core Dump 文件默认关闭,可通过ulimit -c 1024开启(允许最大 1024KB 的 Core 文件),调试时用gdb ./程序名 core.进程号分析。
2.1.3 Ctrl+Z:SIGTSTP(20 号信号)
默认动作:暂停前台进程 ,将其挂入后台 ,可通过 fg 命令 恢复前台运行 ,也可以通过bg 把这个后台进程运行起来。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void sigtstp_handler(int signum)
{
std::cout << "\n进程[" << getpid() << "]捕获到SIGTSTP信号(编号:" << signum << ")" << std::endl;
}
int main()
{
std::cout << "进程PID:" << getpid() << ",等待信号(按Ctrl+Z测试)..." << std::endl;
signal(SIGTSTP, sigtstp_handler);
while (true)
{
sleep(1);
std::cout << "运行中..." << std::endl;
}
return 0;
}
后台操作示例:
bash
# 运行程序后按Ctrl+Z,进程暂停
[1]+ Stopped ./sigtstp_demo
# 查看后台进程
jobs
# 将后台进程恢复到前台
fg 1
# 将后台进程运行起来
# bg 1
2.3 函数调用产生信号(编程触发)
通过系统函数主动发送信号,核心函数包括kill、raise、abort,适用于编程场景下的信号触发。
2.3.1 kill 函数:向指定进程发送信号
函数原型:
cpp
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- **pid:**目标进程 PID(正数);进程组 ID(负数,发送给组内所有进程);0(发送给当前进程组);-1(发送给所有有权限的进程);
- sig: 信号编号(0 表示检测进程是否存在,不发送信号);
- **返回值:**成功返回 0,失败返回 - 1。
实战:实现简易版 kill 命令(我最开始给的那个mykill也是可以的)
cpp
#include <iostream>
#include <signal.h>
#include <string>
//执行进程进行kill指令
//int kill(pid_t pid, int sig);
//./mykill signumber pid
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cout << "./mykill signumber pid" << std::endl;
return 1;
}
int signum = std::stoi(argv[1]);
pid_t target = std::stoi(argv[2]);
int n = kill(target, signum);
if(n == 0)
{
std::cout << "send " << signum << "to " << "pid:" << target << " success" << std::endl;
}
else
{
std::cout << "kill error" << std::endl;
return 2;
}
return 0;
}

2.3.2 raise 函数:向当前进程发送信号
函数原型:
cpp
#include <signal.h>
int raise(int sig);
- 作用 :自己给自己发送信号,等价于kill(getpid(), sig);
- 返回值:成功返回 0,失败返回非 0。
实战案例 :利用raise函数查找1-31号信号有哪些信号无法被自定义捕捉
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
sleep(1);
}
int main()
{
// signal(SIGINT, handlerSig);
// 将1 - 31所有常规信号全部进行自定义处理操作
for (int i = 1; i < 32; i++)
{
signal(i, handlerSig);
} // 判断一个进程通过这个操作是否将无法被杀死
// 通过上面代码我们碰巧发现9信号是无法被自定义捕捉的
// 那我们能不能将所有不能自定义捕捉的信号找到呢?可以的->raise函数
for(int i = 1; i < 32; i++)
{
sleep(1);
if(i == 9) continue; //跳过不可自定义捕捉的9信号
raise(i); //自己给自己指定发送对应信号
}
return 0;
}

2.3.3 abort 函数:异常终止进程
函数原型:
cpp
#include <stdlib.h>
void abort(void);
- 作用 :向当前进程发送 SIGABRT(6 号信号 ),强制异常终止 ,不可被忽略或自定义捕捉;但是他能执行自定义的操作 只不过最后依旧会终止
- 无返回值 (必然终止进程)。
实战案例:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
sleep(1);
}
int main()
{
// signal(SIGINT, handlerSig);
// 将1 - 31所有常规信号全部进行自定义处理操作
for (int i = 1; i < 32; i++)
{
signal(i, handlerSig);
} // 判断一个进程通过这个操作是否将无法被杀死
int cnt = 0;
while (true)
{
std::cout << "hello world, " << cnt++ << ", pid:" << getpid() << std::endl;
abort();
sleep(3);
}
return 0;
}

2.3.4 三个系统调用层层递进
前面的kill、raise、abort 三个系统调用我们发现是层层递进的!

OS先收到键盘输入,再由操作系统转换成信号发送给目标进程
接下来,先不往后面看,先复盘一个问题:

我们已经说过啦,必须只能是OS!
只有操作系统 可以在内核操作系统里面对PCB内部的位图进行修改。
换句话说,通过键盘产生信号也并不是键盘直接产生的信号,必然是OS先收到键盘的输入的,再由操作系统转换成信号发送给目标进程!
2.4 硬件异常产生信号(CPU / 内存错误)
由硬件检测到的异常触发,内核将其解释为对应信号发送给进程,所有硬件异常信号基本上均触发 Core Dump(后续有补充) ,核心场景包括除零错误(SIGFPE) 、非法内存访问(SIGSEGV),也就是8号和11号信号


2.4.1 除零错误:SIGFPE(8 号信号)
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void sigfpe_handler(int signum)
{
std::cout << "捕获到SIGFPE信号(编号:" << signum << "),除零错误!" << std::endl;
// 注意:除零后CPU状态未清理,信号会持续触发,需退出进程
exit(1);
}
int main()
{
signal(SIGFPE, sigfpe_handler);
std::cout << "模拟除零错误..." << std::endl;
sleep(1);
int a = 10;
a /= 0; // 除零,触发SIGFPE
return 0;
}


保留问题:进程怎么知道硬件出错的?
2.4.2 非法内存访问:SIGSEGV(11 号信号)
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void sigsegv_handler(int signum)
{
std::cout << "捕获到SIGSEGV信号(编号:" << signum << "),非法内存访问!" << std::endl;
exit(1);
}
int main()
{
signal(SIGSEGV, sigsegv_handler);
std::cout << "模拟野指针访问..." << std::endl;
sleep(1);
int* p = nullptr;
*p = 100; // 访问空指针,触发SIGSEGV
return 0;
}

2.4.3 关键说明
硬件异常产生的信号,本质是 CPU 或硬件检测到错误后,通知内核,再由内核转化为信号发送给进程:
-
除零错误:CPU 运算单元检测到异常,状态寄存器标记错误,内核解释为 SIGFPE;
-
非法内存访问:MMU(内存管理单元)检测到地址无效,内核解释为 SIGSEGV;
-
若不退出进程,异常状态会持续存在,导致信号反复触发而死循环。


2.5 软件条件产生信号(定时 / 管道异常)
由软件内部 状态触发的信号,核心场景包括定时器超时(alarm函数) 、**管道破裂(SIGPIPE)**等。

2.5.1 管道破裂信号(SIGPIPE)
当向无读端 的管道写入数据 时,内核会向写进程发送SIGPIPE(13 号信号),默认动作是终止进程。
cpp
#include <unistd.h>
#include <signal.h>
#include <iostream>
void sigpipe_handler(int signum)
{
std::cout << "捕获到SIGPIPE信号(编号:" << signum << "),管道破裂!" << std::endl;
exit(1);
}
int main()
{
int pipefd[2];
pipe(pipefd); // 创建管道
close(pipefd[0]); // 关闭读端
signal(SIGPIPE, sigpipe_handler);
char buf[1024] = "hello";
write(pipefd[1], buf, sizeof(buf)); // 向无读端的管道写数据,触发SIGPIPE
return 0;
}
2.5.2 alarm 函数:定时器信号(SIGALRM)
函数原型:
cpp
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 作用 :设置定时器 ,seconds秒后向当前进程发送 SIGALRM(14 号信号);
- 返回值:0(无之前的定时器)或之前定时器的剩余秒数;
- 特性:一个进程同时只能有一个活跃的alarm定时器,重复调用会覆盖之前的设置。
基本alarm验证-体会IO效率问题:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int cnt = 0;
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << " cnt: " << cnt << std::endl;
exit(13);
}
//IO效率问题
int main()
{
signal(SIGALRM, handlerSig);
alarm(1); //设定1s闹钟,1s之后,当前进程就会收到一个信号
//int cnt = 0;
// //循环中有IO操作:std::cout、std::endl
// while (true)
// {
// std::cout << "hello world, " << cnt++ << std::endl;
// }
//循环中没有IO操作
while (true)
{
cnt++;
}
return 0;
}

设置重复闹钟:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional>
#include <vector>
using func_t = std::function<void()>; // 包装器
std::vector<func_t> funcs;
int gcount = 0;
void handlerSig(int sig)
{
// std::cout << "获得了一个信号: " << sig << " , pid: " << getpid() << std::endl;
std::cout << "###############################" << std::endl;
for (auto &f : funcs)
{
f();
}
std::cout << "gcount : " << gcount << std::endl;
int n = alarm(1); // 重设闹钟,会返回上⼀次闹钟的剩余时间
std::cout << "剩余时间 : " << n << std::endl;
std::cout << "###############################" << std::endl;
}
int main()
{
// lambda表达式插入任务
funcs.push_back([]()
{ std::cout << "我是一个进程调度" << std::endl; });
funcs.push_back([]()
{ std::cout << "我是⼀个检测进程时间⽚的操作,如果时间⽚到了,我会切换进程" << std::endl; });
funcs.push_back([]()
{ std::cout << "我是⼀个内存管理操作,定期清理操作系统内部的内存碎⽚" << std::endl; });
signal(SIGALRM, handlerSig);
alarm(1); // ⼀次性的闹钟,超时alarm会自动被取消,后续不再被触发
while (true)
{
pause();
std::cout << "我醒来了..." << std::endl;
gcount++;
}
// 这就是操作系统的运行原理
return 0;
}

2.5.3 关于 alarm 的应用场景和管理(先描述,再组织)

结束语
本章节我们从生活场景切入,逐层讲解了信号的概念、系统指令、五大产生方式以及各类典型信号的特性与使用场景。信号是 Linux 进程间交互、异常处理的核心机制,也是后端开发与系统编程的必备基础。希望大家结合理论多加动手实践,熟练掌握信号的收发逻辑与使用规则,为后续学习筑牢根基。
下一篇我们将继续讲解信号的保存 与递达,深入剖析未决信号集、阻塞信号集以及信号捕捉流程等底层逻辑。