目录
[一 快速认识信号](#一 快速认识信号)
[1 理解信号是什么?为什么要有?](#1 理解信号是什么?为什么要有?)
[2 自定义捕捉信号](#2 自定义捕捉信号)
[3 信号产生的方式](#3 信号产生的方式)
[(3) 系统调用](#(3) 系统调用)
(5)软件条件软件条件)
[4 键盘怎么向目标进程发送信号](#4 键盘怎么向目标进程发送信号)
一 快速认识信号
1 理解信号是什么?为什么要有?
1-31号信号是普通信号

信号:用户或系统,异步的给进程发送就绪事件方式,异步通知机制
我们来解释一下什么是异步:
例如:同步:骑手打电话让你下楼取餐,必须等你拿到才离开,双方互相等待、步调一致。
异步:骑手把外卖放门口发个消息就走,不用等你处理,你有空再去拿,双方互不等待、各做各的
所以:
同步:发通知的人要等你回应、处理完才走,双方步调绑定、互相等待。
异步:发通知的人发完就走,不等你处理,你有空再处理,双方互不等待、各自执行
人是能识别信号的:1 认识它 2 触发事件,即使信号没有发生,也知道该怎么做。例如:遇到红绿灯,闹钟想起来的时候....
结论:进程能识别信号
基本结论:
你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性 。
信号产生之后,你知道怎么处理吗?知道 。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。
处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。
信号到来 | 信号保存 | 信号处理
怎么进行信号处理啊?a. 默认 b. 忽略 c. 自定义,后续都叫做信号捕捉。
因为有时信号不会被立即处理(操作系统可能有更重要的事做),所以就要求进程有临时保存信号的能力
信号处理的方式有三种:默认,忽略,自定义
在进程中要有特定的数据类型保存对应信号:位图!!!
保存什么信号? → 记录信号编号
信号的状态? → 记录这个信号是否已经产生 / 被收到
为了高效实现这两点,内核采用了 位图(bit array) 的设计。
位图的原理:
底层实现:用一个整数(比如 long int,32 位 / 64 位)来存储
每一个比特位对应一个信号编号(比如第 3 位对应信号 3)
比特位的值为 1,表示收到了该信号;值为 0,表示未收到
例如:
二进制: 0000 0000 0000 0000 0000 0000 0000 1001
第 0 位为 1:收到了信号 0(保留信号)
第 3 位为 1:收到了信号 3(SIGQUIT)
其余位为 0:未收到对应信号
信号具体化:就是一个数字,可以指导操作系统修改进程中的PCB信息
发送一个信号的操作,归根结底就是一次内存数据的修改:
目标:找到目标进程的 PCB(进程控制块)。
操作:修改 PCB 中信号位图的指定位。
结果:将该信号的状态置为 1(未决 / 待处理),表示 "我给你发了个信号"
位图在哪?进程PCB中,PCB是内核数据结构;能够给进程写入信号的,只有操作系统
信号的产生:让操作系统给目标进程写信号
2 自定义捕捉信号
signal 系统调用:收到某个信号时,用哪种方式处理
cpp
signal(int signum, void (*handler)(int));
参数含义:
signum:要处理的信号编号(如 SIGINT、SIGQUIT)
handler:信号的处理方式,有 3 种选择:
SIG_DFL:使用系统默认动作(通常终止进程)
SIG_IGN:忽略该信号
自定义函数名:收到信号时自动调用这个函数(调用signal时,可以把自己写的函数【返回值void,参数int】,把函数的入口地址设置进来)
向这种给一个函数,把另一个函数以参数的形式设置进来,叫做回调
signal设置好了,不是立即调用,而是在未来处理信号的时候使用
signal的本质是对知道信号来进行自定义处理的一种延时处理
信号的处理工作是进程做的
在Linux中,不同的信号的自定义捕捉方法一样
cpp
signal(信号, SIG_IGN); // 忽略
signal(信号, SIG_DFL); // 默认
SIG_IGN:Ignore → 忽略
收到信号,不做任何处理,直接丢掉。
SIG_DFL:Default → 默认动作
收到信号,执行系统原本的行为(大部分是终止进程)

3 信号产生的方式
(1)通过系统命令,kill产生
(2)通过键盘产生信号
Ctrl + C:发送 2 号信号 SIGINT
Ctrl + \:发送 3 号信号 SIGQUIT
Ctrl + Z:发送 20 号信号 SIGTSTP
| 信号名 | 编号 | 按键 | 默认动作 | 作用说明 |
|---|---|---|---|---|
SIGINT |
2 | Ctrl + C |
Terminate(终止) | 终端中断信号,请求进程正常退出 |
SIGQUIT |
3 | Ctrl + \ |
Core Dump(退出 + 生成 core 文件) | 终端退出信号,进程会崩溃并生成调试用的 core 文件 |
SIGTSTP |
20 | Ctrl + Z |
Stop(暂停) | 终端停止信号,让进程暂停,放入后台 |
Ctrl + C 能终止进程,本质是发送了 2 号信号,而 2 号信号的默认处理动作就是终止进程
我们来看一下信号的动作:Action那一列

Term和Core都表示进程退出
信号的动作的具体使用动作,我们在后面讲解
保留问题:键盘怎么能向目标进程发送信号
(3) 系统调用
kill:Linux/Unix 系统中用于向进程发送信号的核心系统调用
cpp
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
pid表示发送给哪一个进程,sig表示要发送几号信号
rasie:进程给自己发送一个指定信号
cpp
#include <signal.h>
int raise(int sig);
abort:作用是让进程立刻异常终止
cpp
#include <stdlib.h>
void abort(void);
abort向调用进程,发送指定信号:6号 SIGABRT(动作是core)-->通常是用来进行异常终止,可以设置捕捉动作,但是最后还是会终止进程
上面三个系统调用是层层递进的:
| 函数 | 作用 | 本质 |
|---|---|---|
kill |
给任意进程发任意信号 | 系统调用 |
raise |
给自己发任意信号 | kill(getpid(), sig) |
abort |
强制异常终止当前进程 | 最终发 SIGABRT 信号 |
不管发送信号的方式有多少种,最终,必须是谁完成真正的信号发送动作呢?
只能是操作系统
通过键盘产生信号不是直接键盘产生的,那么必然是操作系统先收到键盘的输入
(4)异常(产生信号方式)
信号捕捉函数一直被触发
软件层面,如果代码出现异常,进场就会自己收到对应的进程
进程为什么会把异常当作信号?进程异常如何被操作系统识别?
信号 11 (SIGSEGV) 是 Linux/Unix 系统上的 段错误(Segmentation Fault),表示程序访问了非法内存地址,被操作系统强制终止
两个问题:
a.进程为什么异常了之后,会自动退出----因为收到了信号,对进程的默认处理动作是终止进程;信号只能是操作系统发送的
b.为什么是这样?
1. 硬件层面:CPU 触发异常
CPU 是程序执行的核心硬件,当进程执行非法操作时,会直接触发 CPU 内部的硬件异常:
除 0 错误:执行a/0这类非法运算时,CPU 的运算单元会检测到错误,在 x86 架构下,会通过EFLAGS/RFLAGS寄存器(记录进程硬件上下文的状态寄存器)的相关标志位(如溢出标志位 OF)标记异常 。
野指针 / 非法内存访问:进程访问未分配、无权限的内存地址时,CPU 的内存管理单元(MMU)会触发页错误异常,同样会通知内核。
2. 内核层面:操作系统捕获异常并发送信号
操作系统是软硬件资源的管理者,必须感知并处理 CPU 的硬件异常:
内核会通过中断 / 异常处理机制,捕获 CPU 上报的硬件错误;
内核通过全局指针task_struct *current,精准定位到当前正在执行的、触发异常的进程;
内核根据异常类型,向该进程发送对应的致命信号(如除 0 对应SIGFPE,野指针对应SIGSEGV)。
3. 进程层面:信号的默认处理与自定义
进程收到信号后,会执行对应的处理动作:
默认行为:绝大多数致命信号的默认处理就是终止进程,这就是我们看到的「进程异常自动退出」;
自定义行为:进程可以通过signal()/sigaction()等系统调用,自定义信号的捕捉处理逻辑,替换默认的终止动作(比如打印崩溃日志、现场保存后再退出)
为什么进程开始死循环了?
因为溢出标志位没有恢复到0,CPU硬件上一直报错,导致进程一直在处理信号
结论:C/C++程序一旦出现异常(一般都是引起了硬件异常,操作系统识别硬件异常,给目标进程发送特定对应信号,所以进程收到和进程处理本质是让系统恢复正常),程序会崩溃的原因是进程退出了
终止方式:
我们来学习一下前面提到的进程的动作
term:正常终止(外部原因进程被杀掉,进程内部无错误)
core:进程内部有问题,引起异常,需要自己debug;操作系统会将进程的退出时的相关内存数据,转存到磁盘上,这个过程叫做核心转储(形成core文件)。在云服务器上,核心转储这个配置是默认关闭的 为什么默认关闭? 因为AI云服务器中有大量的core文件,会出现未定义的问题,有风险。 那我们如何打开?
bash
ulimit -c xxx
ulimt -c o
core.pid:在当前目录下,形成一个二进制文件
我们之前学到的,Linux 中进程退出状态用一个 32 位整数存储,如下图:

核心标志位:第 8 位为 1 时,代表进程崩溃时生成了 core dump,可通过waitpid()等系统调用获取该状态;若为 0:进程被信号杀死,但未生成 core dump
核心转储的核心作用是 "留存进程崩溃现场,支撑事后精准调试"。 它是 Linux 系统调试内存错误、生产级故障的 "终极手段",没有 core dump,很多底层、偶发的崩溃类问题几乎无法定位。结合 GDB 等工具,core 文件能将抽象的崩溃问题转化为具体的代码行、变量状态,大幅提升调试效率。
(5)软件条件
管道通信的一种场景:读端关闭,写端一直写,操作系统就会把写进程杀掉--->操作系统会给子进程发送一个信号:13号SIGPIPE(收到 SIGPIPE 后进程直接退出,无 core dump(默认动作是 Term,非 Core))
管道是一种基于文件系统的通信方式,所以属于软件范畴;当软件条件不满足是,操作系统会给目标发送信号,杀掉进程
系统调用:alarm 设置一个若干秒之后的闹钟;若干秒之后,操作系统就会发送14号信号SIGALRM信号
bash
unsigned int alarm(unsigned int seconds);
功能:在 seconds 秒后,操作系统向调用进程发送 14 号信号 SIGALRM。
返回值:返回上一个闹钟的剩余时间(若未设置过则返回 0),多次调用会重置闹钟时间
| 信号名 | 编号 | 触发条件 | 信号来源 | 默认动作 | 核心用途 |
|---|---|---|---|---|---|
SIGPIPE |
13 | 管道读端关闭,写端持续写入 | 操作系统(软件条件不满足) | 终止进程 | 管道 / 套接字通信异常处理 |
SIGALRM |
14 | alarm() 定时时间到 |
操作系统(系统调用触发) | 终止进程 | 超时控制、定时任务 |
alarm是一个一次性闹钟,对你这个进程,只会有一个闹钟
bash
int n = alarm(0);
这句代码表示取消闹钟,返回剩余时间
理解闹钟:背后机理是定时器
应用场景:操作系统具有设定延时触发的能力
操作系统内部可不可以同时存在多种闹钟(定时器)?操作系统要不要管理这些闹钟?
可以存在;要管理!如何管理:先描述,再组织
bash
struct timer {
int timeout; // 未来的超时时间(绝对时间,如jiffies)
pid_t who; // 目标进程PID(标识该定时器属于哪个进程)
void (*callback)(); // 超时触发的回调函数(如发送SIGALRM信号)
// 其他链表/堆节点指针、优先级等字段
};
当操作系统管理了闹钟之后:就能实现
快速插入新定时器(进程调用 alarm() 时)
快速查找 / 删除到期定时器(时钟中断时)
高效遍历所有定时器,检查是否到期
我们可以把描述的这个结构,理解为最小堆 注意:这里是为例便于理解,操作系统实际并没有用堆结构管理定时器
堆顶元素永远是最早到期的定时器:每次时钟中断只需检查堆顶,若堆顶未到期则所有定时器都未到期,无需遍历全部;如果堆顶超时了,那就查看下一个

4 键盘怎么向目标进程发送信号
我们先构建概念

操作系统怎么知道键盘上有数据了?
硬件中断!!(后面有一个专题讲)
外设向CPU发送中断,CPU暂停手中工作,转而执行操作系统中代码
简单理解硬件中断:假设你现在 在外面,不知道你的舍友在不在宿舍,想知道的话,最直接的方法就是打电话--->这里的打电话就相当于硬件中断、
(1)如何理解键盘输入:键盘就是基于硬件中断进行工作
(2)操作系统如何解释快捷键:把ctrl+c,ctrl+v.....可能直接解释成信号
(3)操作系统怎么知道信号应该发送给哪一个进程
我们先补充一部分知识:软件方面的进程组和作业

会话(Session)
- 对应图中的
session,是用户登录后创建的最高级别的进程集合。- 一个会话关联一个控制终端 (图中的
终端文件),用户通过这个终端和会话交互。- 会话里可以有多个进程组,同一时间只有一个前台进程组可以直接和终端交互(图中写了 "前台进程 (组) 只有一个!!!")。
进程组(Process Group)
- 对应图里的
group,是一组关联的进程集合 ,通常由管道命令创建(比如ls | grep txt,两个进程属于同一个进程组)。- 每个进程组有一个组长(进程组 ID = 组长 PID),shell 会把前台的进程组设为当前会话的前台进程组。
终端(Terminal)
图中的终端文件,是用户输入输出的接口 (比如你用 xshell 登录后,那个窗口对应的设备文件)。
所有来自键盘的信号(Ctrl+C、Ctrl+Z)和输入数据,都会发送给前台进程组,而不是后台进程
Shell(bash)
- 图里的bash,是会话的创建者和管理者。
- 用户登录时,shell 会创建会话,成为会话首进程(session leader)。
- 每次执行命令时,shell 会创建新的进程组,前台运行的进程组会抢占终端,后台运行的进程组无法直接和终端交互。
作业(Job)
- 作业就是 shell 对进程组的管理抽象:
- 前台作业:就是当前的前台进程组,独占终端。
- 后台作业:后台运行的进程组,用
&启动,或者用Ctrl+Z挂起后放到后台
图示过程解析:
用户通过 xshell 建立连接,系统为用户创建会话,打开对应的终端设备文件。bash 成为会话首进程,绑定控制终端。用户在 bash 里输入命令(比如sleep、管道命令),bash 会创建新的进程组(作业)
前台命令:进程组成为会话的前台进程组,接收终端的输入和信号。
后台命令:进程组作为后台作业运行,无法直接接收终端信号,也无法读取终端输入。
终端信号路由:Ctrl+C、Ctrl+Z 这些信号,只会发送给前台进程组里的所有进程,后台进程组收不到
ctrl+c只能用来终止前台进程:因为终端的信号只会发送给前台进程组,后台进程组和终端的交互被 shell 屏蔽了
进程组允许是一个进程,作业是由进程组完成的
会话内部至少包含一个进程组,是进程组的继承,通常有属于自己的终端文件
在一个会话中,任何时刻,只允许一个进程(组)在前台--->那么其他命令就无法执行了吗?
前台进程只是暂时占用当前终端的交互权,你可以通过后台运行、挂起、多终端等方式,在不终止前台进程的情况下继续执行其他命令,完全不会 "无法操作"
什么叫做前后台:可以直接获取用户输入的叫前台进程,否则叫后台进程
谁拥有终端文件(主要是键盘),谁就是前台
后台可以向显示器打印,无法从键盘获取数据
ctrl+C是以进程组为点位杀死进程的,所以有时候会杀死多个进程