目录
[1.1 信号的核心定位](#1.1 信号的核心定位)
[1.2 信号与中断的深层关联](#1.2 信号与中断的深层关联)
二、信号基础认知:从生活场景到技术定义
[2.1 生活视角理解信号生命周期](#2.1 生活视角理解信号生命周期)
[2.2 信号的技术定义与核心属性](#2.2 信号的技术定义与核心属性)
[2.3 信号的三种处理动作](#2.3 信号的三种处理动作)
三、信号的产生:五大触发场景全解析
[3.1 终端按键触发(硬件中断衍生)](#3.1 终端按键触发(硬件中断衍生))
[3.2 系统命令/函数触发](#3.2 系统命令/函数触发)
[3.3 软件条件触发](#3.3 软件条件触发)
[3.4 硬件异常触发](#3.4 硬件异常触发)
四、信号的保存:未决与阻塞机制
[4.1 核心概念:未决、阻塞、递达](#4.1 核心概念:未决、阻塞、递达)
[4.2 内核中的信号存储结构](#4.2 内核中的信号存储结构)
[4.3 信号集操作函数实战](#4.3 信号集操作函数实战)
五、中断本质深度解析
[5.1 中断的核心作用:OS的"驱动引擎"](#5.1 中断的核心作用:OS的“驱动引擎”)
[5.2 硬件中断:外设与CPU的通信桥梁](#5.2 硬件中断:外设与CPU的通信桥梁)
[5.2.1 硬件中断的完整流程](#5.2.1 硬件中断的完整流程)
[5.2.2 中断向量表(IDT)与中断服务程序](#5.2.2 中断向量表(IDT)与中断服务程序)
[5.3 时钟中断:OS调度的"心跳"](#5.3 时钟中断:OS调度的“心跳”)
[5.3.1 时钟中断如何推动进程调度](#5.3.1 时钟中断如何推动进程调度)
[5.3.2 内核源码中的时钟中断设置](#5.3.2 内核源码中的时钟中断设置)
[5.4 软中断:系统调用与信号的底层实现](#5.4 软中断:系统调用与信号的底层实现)
[5.4.1 软中断的触发方式(int 0x80/syscall)](#5.4.1 软中断的触发方式(int 0x80/syscall))
[5.4.2 系统调用的软中断流程拆解](#5.4.2 系统调用的软中断流程拆解)
[5.5 异常:软件错误的中断化处理](#5.5 异常:软件错误的中断化处理)
[5.5.1 异常与信号的关联(除零/野指针)](#5.5.1 异常与信号的关联(除零/野指针))
[5.5.2 缺页中断:虚拟内存的核心异常机制](#5.5.2 缺页中断:虚拟内存的核心异常机制)
六、信号捕捉:从内核态到用户态的完整流程
[6.1 信号捕捉的核心流程拆解](#6.1 信号捕捉的核心流程拆解)
[6.2 sigaction函数:更强大的信号捕捉接口](#6.2 sigaction函数:更强大的信号捕捉接口)
[6.3 内核态与用户态切换的细节](#6.3 内核态与用户态切换的细节)
七、信号使用的避坑指南
[7.1 可重入函数与不可重入函数](#7.1 可重入函数与不可重入函数)
[7.2 volatile关键字的信号场景应用](#7.2 volatile关键字的信号场景应用)
[7.3 SIGCHLD信号:僵尸进程的优雅回收](#7.3 SIGCHLD信号:僵尸进程的优雅回收)
八、总结
一、引言:信号的本质是"软中断"
在Linux系统中,信号是进程间异步通信的核心机制,但其底层本质是对"中断"机制的软件模拟------硬件中断处理外设事件,而信号则处理进程间的异步事件。理解中断的工作原理,是掌握信号底层逻辑的关键;反过来,通过信号的使用,也能更深刻地理解操作系统如何通过中断驱动运行。
本文我将从信号的基础认知出发,逐步深入到中断的本质,详细拆解硬件中断、时钟中断、软中断、异常的底层实现,最终串联起信号"产生-保存-捕捉"的全流程,让你彻底搞懂信号与中断的深层关联。
1.1 信号的核心定位
信号是Linux中进程间事件异步通知的一种方式,属于"软中断"。它的核心特点是:
- 异步性:信号的产生时机不可预知,进程执行到任意指令时都可能收到信号;
- 内核管理:信号的产生、传递、处理均由内核统一管理,进程无法直接操作;
- 中断特性:信号的处理流程与硬件中断类似,会打断进程当前执行流程,处理完成后恢复。
1.2 信号与中断的深层关联
中断是CPU处理外部事件的核心机制,分为硬件中断和软件中断(软中断/异常) 。信号本质是一种软中断,其处理流程完全借鉴了硬件中断的设计:
- 硬件中断:外设(键盘、磁盘)→ 触发CPU中断 → 内核执行中断服务程序;
- 信号:进程/内核 → 产生信号(软中断)→ 内核记录信号 → 合适时机执行信号处理函数。
可以说,信号是"用户态的中断",而中断是"内核态的信号",二者共享相同的底层执行逻辑。
二、信号基础认知:从生活场景到技术定义
在深入底层之前,先通过生活场景和技术定义,建立对信号的基础认知。
2.1 生活视角理解信号生命周期
用"收快递"的场景类比信号的完整生命周期,直观易懂:
- 信号产生:快递员到达(信号触发);
- 信号保存:你收到快递通知,但正在打游戏,暂时不处理(信号未决);
- 信号处理:游戏结束后,你去取快递(信号递达),处理方式有三种------自己拆(默认动作)、送给室友(自定义动作)、扔掉(忽略)。
对应到技术场景:
- 信号产生:通过终端按键、函数调用等方式触发;
- 信号保存:内核在进程PCB中记录信号(未决状态);
- 信号处理:进程在合适时机(从内核态返回用户态前)执行处理动作。
2.2 信号的技术定义与核心属性
Linux系统中共有64种信号(34以下为常规信号,34以上为实时信号),每种信号都有明确的编号和宏定义(如SIGINT对应2号信号)。通过kill -l命令可查看所有信号:

信号的核心属性存储在进程的task_struct(PCB)中,关键结构体包括:
blocked:信号屏蔽字(阻塞信号集),标记哪些信号被阻塞;pending:未决信号集,标记哪些信号已产生但未处理;sighand:信号处理动作集合,存储每种信号的处理方式(默认/忽略/自定义)。
2.3 信号的三种处理动作
信号的处理动作由进程预先设定,共三种可选:
- 默认动作(SIG_DFL) :内核预定义的动作,如
SIGINT(Ctrl+C)默认终止进程; - 忽略动作(SIG_IGN):进程收到信号后不做任何处理;
- 自定义动作(信号捕捉):进程注册自定义函数,信号递达时执行该函数。
示例:通过signal函数设置SIGINT的自定义处理动作:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signo) {
std::cout << "进程[" << getpid() << "]捕获到信号:" << signo << std::endl;
}
int main() {
std::cout << "进程PID:" << getpid() << std::endl;
signal(SIGINT, handler); // 注册自定义处理函数
while (true) {
sleep(1);
std::cout << "等待信号..." << std::endl;
}
return 0;
}
运行后按下Ctrl+C,进程不会终止,而是执行handler函数,证明信号被成功捕捉。若想强制终止得按Ctrl+\,发送3号信号SIGQUIT。

三、信号的产生:五大触发场景全解析
信号的产生本质是"中断事件的触发",不同场景对应不同的中断来源,主要分为五大类:
3.1 终端按键触发(硬件中断衍生)
终端按键触发的信号,本质是"键盘硬件中断"的软件转化:
- 键盘按下(如
Ctrl+C)→ 触发键盘硬件中断; - 内核中断服务程序解析按键,转化为对应信号(
SIGINT); - 内核将信号发送给前台进程。
常见终端按键与对应信号:
Ctrl+C:SIGINT(2号信号),默认终止进程;Ctrl+\:SIGQUIT(3号信号),默认终止进程并生成core dump文件;Ctrl+Z:SIGTSTP(20号信号),默认暂停进程。
3.2 系统命令/函数触发
通过系统命令或函数主动向进程发送信号,属于"软中断触发":
- 命令 :
kill -信号编号 PID(如kill -11 1234发送SIGSEGV信号); - 函数 :
kill(向指定进程发信号)、raise(向当前进程发信号)、abort(向当前进程发SIGABRT信号)。
示例:用kill函数实现自定义"kill命令":
cpp
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "用法:" << argv[0] << " -信号编号 PID" << std::endl;
return 1;
}
int signo = std::stoi(argv[1]+1); // 去掉前缀"-"
pid_t pid = std::stoi(argv[2]);
return kill(pid, signo); // 发送信号
}

3.3 软件条件触发
由软件内部状态变化触发信号,典型场景:
- 定时器超时:
alarm函数设置定时器,超时后内核发送SIGALRM信号,该信号的默认处理动作是终止当前进程; - 管道破裂:向无读端的管道写数据,内核发送
SIGPIPE信号。
示例:alarm函数实现定时信号:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
int main() {
alarm(1); // 1秒后发送SIGALRM信号
while (true) {
std::cout << "count = " << count << std::endl;
count++;
}
return 0;
}

3.4 硬件异常触发
硬件异常被CPU检测到后,内核转化为信号发送给当前进程,属于"异常中断触发":
- 除零错误:CPU运算单元检测到除零,内核发送
SIGFPE(8号信号); - 非法内存访问:MMU检测到无效地址,内核发送
SIGSEGV(11号信号)。
示例:模拟非法内存访问触发SIGSEGV信号:
cpp
#include <iostream>
#include <signal.h>
void handler(int signo) {
std::cout << "捕获到信号:" << signo << "(非法内存访问)" << std::endl;
exit(1);
}
int main() {
signal(SIGSEGV, handler);
int* p = NULL;
*p = 100; // 非法内存访问,触发SIGSEGV
return 0;
}

四、信号的保存:未决与阻塞机制
信号产生后并非立即处理,而是先被内核保存,等待合适的处理时机。这一过程涉及"未决"和"阻塞"两个核心概念。
4.1 核心概念:未决、阻塞、递达
- 未决(Pending):信号从产生到递达之间的状态,内核已记录但未处理;
- 阻塞(Block):进程可选择阻塞某个信号,被阻塞的信号会保持未决状态,直到阻塞解除;
- 递达(Delivery):信号的处理动作被实际执行(默认/忽略/自定义)。
关键区别:阻塞是"不让信号递达",忽略是"信号递达后不处理",二者是不同层面的概念。
4.2 内核中的信号存储结构
内核在task_struct中通过三个核心结构管理信号:
sigset_t blocked:信号屏蔽字,用位图表示(1位对应一个信号,1表示阻塞);struct sigpending pending:未决信号集,同样用位图表示(1位对应一个信号,1表示未决);struct sighand_struct *sighand:存储每种信号的处理动作(sa_handler指向处理函数)。

4.3 信号集操作函数实战
Linux提供专门的函数操作信号集(sigset_t),核心函数包括:
sigemptyset:初始化信号集为空;sigfillset:初始化信号集包含所有信号;sigaddset:向信号集添加指定信号;sigdelset:从信号集删除指定信号;sigismember:判断信号是否在信号集中;sigprocmask:修改进程的信号屏蔽字(阻塞/解除阻塞);sigpending:获取当前进程的未决信号集。
示例:阻塞SIGINT信号,观察未决状态变化:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void PrintPending(sigset_t& pending) {
std::cout << "当前未决信号集:";
for (int i = 31; i >= 1; --i) {
if (sigismember(&pending, i)) {
std::cout << "1";
} else {
std::cout << "0";
}
}
std::cout << std::endl;
}
void handler(int signo) {
std::cout << "捕获到信号:" << signo << std::endl;
return;
}
int main() {
signal(SIGINT, handler); // 注册自定义处理函数
// 1. 初始化信号集,添加SIGINT(2号信号)
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, SIGINT);
// 2. 设置信号屏蔽字,阻塞SIGINT
sigprocmask(SIG_BLOCK, &block_set, &old_set);
int cnt = 10;
while (cnt--) {
// 3. 获取并打印未决信号集
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
// 4. 解除阻塞
std::cout << "解除SIGINT阻塞!" << std::endl;
sigprocmask(SIG_SETMASK, &old_set, nullptr);
return 0;
}
运行后按下Ctrl+C,未决信号集会出现"1"(SIGINT未决),10秒后解除阻塞,信号被递达并执行handler函数。

五、中断本质深度解析
中断是操作系统的"心脏",所有异步事件(包括信号)都依赖中断机制处理。所以在讲解信号捕捉之前,我将详细拆解中断的分类、流程和底层实现。
5.1 中断的核心作用:OS的"驱动引擎"
中断的核心作用是"打破进程的连续执行",处理异步事件。操作系统的所有核心功能(进程调度、IO处理、信号传递)都依赖中断驱动:
- 没有中断,OS会陷入死循环,无法响应任何外部事件;
- 中断让OS具备"并发"能力,通过中断切换进程、处理外设请求。
中断的分类:
| 类型 | 触发源 | 示例 |
|---|---|---|
| 硬件中断 | 外部设备(键盘、磁盘) | 键盘按下、磁盘IO完成 |
| 软中断 | 软件主动触发 | 系统调用(int 0x80)、信号 |
| 异常 | 软件错误(CPU检测) | 除零、非法内存访问、缺页 |
5.2 硬件中断:外设与CPU的通信桥梁
硬件中断是外部设备与CPU的通信方式,流程完全由硬件和内核协同完成。
5.2.1 硬件中断的完整流程
- 外设就绪:外部设备完成操作(如键盘按下、磁盘IO完成);
- 触发中断:设备向CPU发送中断信号(通过中断控制器8259A);
- CPU响应:CPU暂停当前执行的指令,保存上下文(寄存器状态);
- 查找中断服务程序 :CPU通过"中断向量号"查找中断向量表(IDT),获取中断服务程序(ISR)的地址;
- 执行中断服务程序:内核执行对应设备的中断服务程序(如键盘中断处理函数);
- 恢复上下文:中断处理完成后,CPU恢复之前的上下文,继续执行原程序。
5.2.2 中断向量表(IDT)与中断服务程序
中断向量表(Interrupt Descriptor Table, IDT)是内核在内存中维护的一张表,存储所有中断的服务程序地址 。每个中断对应一个"中断向量号"(0~255),作为IDT的下标。下图展示了OS通过IDT进行中断处理的完整流程。

5.3 时钟中断:OS调度的"心跳"
时钟中断是最特殊的硬件中断,由系统时钟(如8253定时器)周期性触发,是OS调度的"心跳"。
5.3.1 时钟中断如何推动进程调度
- 系统时钟每10ms(可配置)触发一次时钟中断;
- 中断服务程序
timer_interrupt被调用,内核更新系统时间和进程时间片; - 若当前进程时间片耗尽,内核调用
schedule函数进行进程切换; - 切换完成后,恢复新进程的上下文,继续执行。
没有时钟中断,OS无法进行进程调度,所有进程会一直执行直到结束,无法实现"并发"。
5.3.2 内核源码中的时钟中断设置
Linux内核通过schedule_init函数(kernel/sched.c)注册时钟中断:
c
void sched_init(void) {
// 注册时钟中断服务程序
set_intr_gate(0x20, &timer_interrupt);
// 允许时钟中断(修改中断控制器屏蔽码)
outb(inb_p(0x21) & ~0x01, 0x21);
// ... 其他初始化
}
// 调度⼊⼝
void do_timer(long cpl)
{
// ...
schedule();
}
void schedule(void)
{
// ...
switch_to(next); // 切换到任务号为next 的任务,并运⾏之。
}
// system_call.s
_timer_interrupt:
call _do_timer // 调用C语言处理函数
iret // 恢复上下文,返回用户态
do_timer函数是时钟中断的核心处理逻辑,最终调用schedule函数完成进程切换。
5.4 软中断:系统调用与信号的底层实现
软中断是软件主动触发的中断,核心用于系统调用和信号传递。
5.4.1 软中断的触发方式
- 32位系统:通过
int 0x80指令触发软中断,0x80是系统调用的中断向量号; - 64位系统:通过
syscall指令触发,效率更高。
软中断的本质是"软件模拟硬件中断",流程与硬件中断一致,但触发源是软件指令。
5.4.2 系统调用的软中断流程拆解
系统调用(如open、read、fork)是用户态进程请求内核服务的唯一方式,底层通过软中断实现:
- 用户态准备 :进程将系统调用号存入
eax寄存器,参数存入ebx、ecx等寄存器; - 触发软中断 :执行
int 0x80指令,CPU切换到内核态; - 查找系统调用表 :内核通过
eax中的系统调用号,查找sys_call_table,获取对应系统调用的实现函数; - 执行系统调用 :内核执行系统调用函数(如
sys_open),完成后将返回值存入eax; - 恢复用户态 :执行
iret指令,恢复用户态上下文,进程继续执行。
内核源码中的系统调用表(include/linux/sys.h):
c
// 系统调用函数指针表
fn_ptr sys_call_table[] = {
sys_setup, // 0: 系统初始化
sys_exit, // 1: 进程退出
sys_fork, // 2: 创建进程
sys_read, // 3: 读文件
sys_write, // 4: 写文件
// ... 其他系统调用
};
信号的传递也依赖软中断:内核在合适时机(如中断处理完成后)检查进程的未决信号,若有则触发软中断,执行信号处理函数。
5.5 异常:软件错误的中断化处理
异常是CPU检测到的软件错误,属于"被动触发的软中断",流程与硬件中断类似,但触发源是软件执行错误。
5.5.1 异常与信号的关联
异常发生后,内核会将其转化为对应信号,发送给当前进程:
- 除零错误 → 内核触发
SIGFPE信号; - 非法内存访问 → 内核触发
SIGSEGV信号; - 缺页错误 → 内核先处理缺页(申请内存、映射页表),处理失败则触发
SIGSEGV信号。
示例:除零错误的异常处理流程:
- 进程执行
a = 10 / 0,CPU检测到除零异常,触发中断向量号为0的异常; - 内核执行
divide_error中断服务程序; - 服务程序将异常转化为
SIGFPE信号,设置进程的未决信号集; - 进程从内核态返回用户态前,检查到未决信号,执行信号处理动作(默认终止进程)。
5.5.2 缺页中断:虚拟内存的核心异常机制
缺页中断(缺页异常)作为虚拟内存的核心支撑,根据"数据位置"和"触发原因"分为三类:硬缺页(Major Fault) 、软缺页(Minor Fault) 、无效缺页(Invalid Fault),下面逐一拆解:
1. 硬缺页
- 定义 :当程序访问的页面有效 (属于其地址空间),但尚未被加载到物理内存中时发生的缺页。这是最普遍的缺页类型。
- 触发场景 :
- 程序第一次访问一个代码或数据页。
- 之前被交换到磁盘交换分区/文件的页面被再次访问。
- 处理过程 :
- 操作系统需要找到一个空闲的物理页帧。
- 从磁盘(可能是可执行文件本身,也可能是交换空间)中将所需页面读入该页帧。
- 更新页表,建立虚拟页到物理页帧的映射。
- 重新执行引发缺页的指令。
2. 软缺页
- 定义 :所需页面已经在物理内存中 ,但当前进程的页表中没有建立有效的映射关系。
- 触发场景 :
- 进程创建(写时拷贝) :在
fork()系统调用后,父子进程最初共享所有物理页面,但页面被标记为只读。当任一进程尝试写入该页面时,会触发缺页。此时操作系统会为写入的进程分配一个新的物理页,复制原页面内容,并建立新的可写映射。 - 内存映射文件:一个文件被映射到进程内存空间,当首次访问某个页面时,虽然该页面可能已被其他进程缓存到内存中,但当前进程的页表还没有指向它。
- 页面在内存,但未分配:有时操作系统可能已经为进程预加载了页面到内存,但还没来得及建立页表项。
- 进程创建(写时拷贝) :在
- 处理过程 :
- 操作系统无需进行磁盘I/O。
- 只需在页表中建立或修改一个映射,指向已存在于内存中的页帧。
- 重新执行引发缺页的指令。
3. 无效缺页 / segfault
- 定义 :程序访问了一个非法的地址,该地址不属于其合法的地址空间。
- 触发场景 :
- 访问了
NULL指针或随机的未分配地址。 - 访问了已释放的内存。
- 试图向代码段等只读区域执行写操作。
- 访问了
- 处理过程 :
- 操作系统无法解决这个错误。
- 它会向当前进程发送一个信号 (在Unix/Linux中是
SIGSEGV,即段错误)。 - 如果进程没有自定义的信号处理程序,默认行为是终止进程并可能产生核心转储文件。
总结与对比
| 类型 | 原因 | 页面是否在内存? | 处理成本 | 最终结果 |
|---|---|---|---|---|
| 硬缺页 | 页面在磁盘上 | 否 | 非常高(需磁盘I/O) | 加载页面,建立映射,继续执行 |
| 软缺页 | 页面在内存,但映射缺失 | 是 | 非常低(仅修改页表) | 建立映射,继续执行 |
| 无效缺页 | 访问非法地址 | 不适用 | 无法修复(程序崩溃) | 发送SIGSEGV信号,通常终止进程 |
六、信号捕捉:从内核态到用户态的完整流程
信号捕捉是信号处理的核心环节,当信号的处理动作是自定义函数时,内核会切换到用户态执行该函数,流程与中断处理紧密关联。
6.1 信号捕捉的核心流程拆解
假设进程注册了SIGINT的自定义处理函数handler,信号捕捉的完整流程:
- 信号产生 :用户按下
Ctrl+C,内核产生SIGINT信号,设置进程的未决信号集; - 中断触发:进程执行到任意指令时,发生时钟中断(或其他中断),切换到内核态;
- 中断处理:内核处理完中断(如更新时间片),准备返回用户态;
- 检查信号 :内核检查进程的未决信号集,发现
SIGINT未决且未被阻塞; - 切换处理函数 :内核不直接返回原程序,而是切换到用户态的
handler函数; - 执行处理函数 :
handler函数执行完成后,调用sigreturn系统调用,再次进入内核态; - 恢复原程序 :内核清除
SIGINT的未决标志,恢复原程序的上下文,返回用户态继续执行。
关键特点:handler函数与原程序是两个独立的控制流程,不存在调用关系,通过内核态切换连接。
6.2 sigaction函数:更强大的信号捕捉接口
signal函数功能简单,sigaction函数是更强大的信号捕捉接口,支持设置信号屏蔽字、保存原处理动作等。
函数原型:
c
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
signo:要设置的信号编号;act:新的信号处理配置(若非空);oact:保存原有的信号处理配置(若非空)。
struct sigaction结构体:
c
struct sigaction {
void (*sa_handler)(int); // 处理函数(与signal一致)
sigset_t sa_mask; // 信号处理期间的屏蔽字(自动屏蔽当前信号)
int sa_flags; // 标志位(如SA_RESTART重启被中断的系统调用)
void (*sa_sigaction)(int, siginfo_t *, void *); // 实时信号处理函数
};
示例:用sigaction设置SIGINT的处理函数:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signo) {
std::cout << "捕获到信号:" << signo << std::endl;
}
int main() {
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask); // 初始化屏蔽字
act.sa_flags = 0; // 默认标志
// 设置SIGINT的处理动作
if (sigaction(SIGINT, &act, &oact) < 0) {
perror("sigaction");
return 1;
}
while (true) {
sleep(1);
std::cout << "等待信号..." << std::endl;
}
return 0;
}

6.3 内核态与用户态切换的细节
信号捕捉过程中,内核态与用户态会发生三次切换:
- 进程执行 → 中断触发 → 内核态(处理中断);
- 内核态 → 信号处理 → 用户态(执行
handler); - 用户态 →
sigreturn→ 内核态(清除未决信号); - 内核态 → 恢复上下文 → 用户态(执行原程序)。
切换的核心是"上下文保存与恢复":
- 内核态切换时,保存用户态的寄存器、程序计数器(PC);
- 恢复时,从保存的上下文恢复寄存器和PC,继续执行。

七、信号使用的避坑指南
在实际使用信号时,需要注意可重入函数、volatile关键字等细节,避免出现隐藏bug。
7.1 可重入函数与不可重入函数
信号处理函数可能在任意时刻执行,若处理函数与原程序访问同一全局资源,可能导致数据错乱,这涉及"可重入性"的概念:可重入性描述的是一个函数能否在尚未返回的情况下,被再次安全地调用(重入),导致再次调用的原因可能是:
- 多线程并发执行。
- 函数被中断,在中断处理程序中又被调用。
- 函数直接或间接地调用自身(递归)。
与之对应的函数分为:
- 可重入函数 :仅访问局部变量或参数,每次调用都在自己的栈帧中拥有独立的数据副本,互不干扰。(如
strcpy、memset); - 不可重入函数 :访问全局资源(全局变量、堆内存、标准IO),多个调用上下文共享了同一个状态(静态/全局数据),导致一个上下文的数据被另一个上下文覆盖。(如
malloc、printf、fwrite)。
示例:不可重入函数导致的问题:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int global = 0;
void insert(int val) {
// 全局变量操作(不可重入)
global = val;
sleep(1); // 模拟耗时操作
std::cout << "global = " << global << std::endl;
}
void handler(int signo) {
insert(100); // 信号处理函数调用insert
}
int main() {
signal(SIGINT, handler);
insert(10); // 主程序调用insert
return 0;
}
运行后按下Ctrl+C,global会被信号处理函数修改,导致输出结果错乱。

7.2 volatile关键字的信号场景应用
编译器优化可能导致信号处理函数修改的变量不被主程序感知,volatile关键字可禁止编译器优化,确保变量的内存可见性。
示例:未使用volatile的问题:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int flag = 0; // 未加volatile
void handler(int signo) {
flag = 1;
std::cout << "flag = " << flag << std::endl;
}
int main() {
signal(SIGINT, handler);
while (!flag); // 编译器优化后,可能一直循环
std::cout << "进程退出" << std::endl;
return 0;
}

编译时添加-O2优化,while (!flag)会被优化为死循环(编译器认为flag不会变化)。添加volatile关键字后,编译器会每次从内存读取flag,解决问题:
cpp
volatile int flag = 0; // 禁止优化

7.3 SIGCHLD信号:僵尸进程的优雅回收
子进程终止时会向父进程发送SIGCHLD信号,父进程可通过捕捉该信号,调用waitpid回收僵尸进程,避免轮询。
示例:SIGCHLD信号回收僵尸进程:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void handler(int signo) {
pid_t id;
// 非阻塞回收所有终止的子进程
while ((id = waitpid(-1, nullptr, WNOHANG)) > 0) {
std::cout << "回收子进程:" << id << std::endl;
}
}
int main() {
signal(SIGCHLD, handler); // 注册SIGCHLD处理函数
pid_t pid = fork();
if (pid == 0) {
std::cout << "子进程PID:" << getpid() << std::endl;
sleep(3);
exit(0);
}
while (true) {
sleep(1);
std::cout << "父进程工作中..." << std::endl;
}
return 0;
}
子进程终止后,父进程收到SIGCHLD信号,在handler中回收子进程,避免僵尸进程产生。

八、总结
- 信号的本质是软中断:信号的处理流程完全借鉴硬件中断,通过内核态与用户态的切换实现异步处理;
- 中断是OS的核心驱动:硬件中断处理外设事件,时钟中断推动进程调度,软中断实现系统调用,异常处理软件错误;
- 信号捕捉的核心是上下文切换:信号处理函数与原程序是独立控制流程,通过内核态切换连接;
- 实际使用需避坑 :避免在信号处理函数中使用不可重入函数,关键变量需加
volatile关键字。