Linux ——— 信号机制深度详解,从生命周期到工程实践
目录
process.cc9(信号阻塞 / 未决队列 / 解除阻塞演示)
process.cc10(sigaction 实现信号处理 - 替代 signal 函数)
process.cc11(sigaction 的 sa_mask 信号屏蔽集演示)
一、struct sigaction 结构体的.sa_handler 字段
process.cc15(基于信号捕捉对单个子进程进行回收)
process.cc16(基于信号捕捉对多个子进程进行回收)
一、process.cc14:子进程退出时主动发送 SIGCHLD 信号
二、process.cc15:基于信号捕捉对单个子进程进行回收
信号的概念
一、信号的核心概念(生活类比)
1. 信号的本质定义信号是操作系统向进程发送的异步通知机制,用于告知进程发生了某个特定事件(如用户中断、硬件错误、进程间通信指令等),进程需根据预设规则响应该事件。2. 生活场景类比
- 类比红绿灯:红绿灯无需司机主动询问状态,会通过灯光变化向所有车辆发送 “停止 / 通行” 的信号,司机看到后按规则响应,对应信号的异步通知特性;
- 类比闹钟:闹钟到预设时间后主动发出声响(产生信号),即使使用者正在忙其他事(如工作),也可先完成手头事务,等合适时机再关闭闹钟(处理信号),对应信号 “不立即处理” 的特性;
- 类比门铃:门外按铃(产生信号),屋内人可能正在做饭(执行主程序),不会立刻开门,等关火后再回应(处理信号),体现信号的 “时间窗口” 特性。
二、信号的完整生命周期:产生→接收→处理
1. 信号产生信号的触发来源分为三类,是信号生命周期的起点:
- 硬件触发:如按下
Ctrl+C组合键(产生SIGINT中断信号)、硬件错误(如内存访问越界产生SIGSEGV信号); - 软件触发:如操作系统通过
kill函数向进程发送信号、定时器到期产生SIGALRM信号; - 进程间触发:一个进程通过系统调用向另一个进程发送自定义信号(如
SIGUSR1/SIGUSR2)。类比场景:红绿灯由交通控制系统切换为红灯(信号产生)、闹钟到达设定时间开始响铃(信号产生)。
2. 信号接收进程通过内核感知到信号的到达,但不会立即暂停当前执行的程序去处理信号,仍会继续执行原有逻辑。此时内核会将信号标记为 “待处理” 状态,记录在进程的内核数据结构中。类比场景:司机看到路口红灯(接收信号),但因车辆正处于路口中间,不会立即停车,而是继续开出路口后再处理;闹钟响铃(接收信号),但使用者正在通话,需等通话结束后再处理。
3. 信号处理进程在合适的时机(如从内核态切换回用户态时),读取内核中记录的 “待处理信号”,并按预设规则响应:
- 默认处理:遵循操作系统内置规则(如
SIGINT默认终止进程、SIGSEGV默认终止并生成核心转储文件); - 忽略处理:进程明确告知内核不响应该信号(如忽略
SIGPIPE信号); - 自定义处理:进程通过
signal/sigaction函数注册自定义处理函数,信号到达时执行该函数。类比场景:司机开出路口后停车(默认处理红灯信号)、闹钟响后直接关掉(自定义处理)、忽略邻居敲门的无关提醒(忽略处理)。
三、信号的时间窗口与进程的内置处理能力
1. 时间窗口的定义从进程 “接收信号”(内核标记信号为待处理)到 “处理信号”(进程执行响应逻辑)的这段时间,称为信号的时间窗口。在此期间,进程仍正常执行原有程序逻辑,内核会持续记录 “待处理信号” 的状态,确保进程不会遗漏。类比场景:从看到红灯(接收)到停车(处理)的间隔、从闹钟响铃(接收)到关闭闹钟(处理)的间隔,均为时间窗口。
2. 进程的内置识别处理能力进程的内核数据结构中,天生维护着一张 “信号处理表”,表中记录了所有系统定义信号的默认处理规则(如SIGKILL默认终止进程、SIGSTOP默认暂停进程)。这种能力是进程与生俱来的,属于操作系统为进程初始化时内置的核心功能:
- 即使进程从未收到过任何信号,也能明确知道每一种信号的处理规则;
- 进程可通过系统调用修改 “信号处理表”(如自定义
SIGINT的处理逻辑),但无法删除表本身,确保任何信号到来时都有对应的处理依据。类比场景:司机即使未看到红绿灯,也天生知道 “红灯停、绿灯行” 的规则;手机即使未收到闹钟提醒,也内置了 “到点响铃、响铃后可关闭” 的处理逻辑,规则不会因未触发而消失。
总结
- 信号是操作系统的异步通知机制,可类比红绿灯、闹钟等生活中的提醒信号,核心特性是 “异步、可延迟处理”;
- 信号的生命周期分为产生、接收、处理三个阶段,接收至处理的间隔为时间窗口,期间内核会记录待处理信号;
- 进程天生具备识别和处理所有信号的能力,内置的 “信号处理表” 是该能力的核心,无论是否收到信号,规则始终存在。
前台进程和后台进程
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "I am a process, my pid is: " << getpid() << endl;
sleep(1);
}
return 0;
}
一、前台进程与后台进程的核心概念
1. 前台进程(Foreground Process)前台进程是与终端会话直接关联的进程,占据终端的输入输出控制权:
- 终端的所有输入(如键盘敲击的字符、快捷键)都会被前台进程接收或处理;
- 终端的输出(如进程打印的内容)会直接显示在终端界面;
- 前台进程运行期间,终端无法响应其他命令输入(除非进程退出或暂停)。
2. 后台进程(Background Process)后台进程与终端会话间接关联,不占用终端的输入控制权:
- 后台进程不会接收终端的键盘输入(包括快捷键),仅会将输出信息打印到终端;
- 终端的输入控制权仍保留给 bash,可正常输入并执行新命令;
- 后台进程运行期间,终端可同时执行其他操作,不影响进程本身运行。
二、Linux 终端、bash 与前后台进程的关联规则
1. 终端与 bash 的基础关系Linux 系统中,一次登录会话对应一个终端实例,每个终端默认启动一个 bash 进程(命令解释器):
- bash 进程的核心作用是解析用户输入的命令、创建子进程执行命令、返回执行结果;
- 未执行其他命令时,bash 进程默认作为终端的前台进程,持续等待并接收用户输入的命令。
2. 前后台进程的数量规则单个终端会话中,前后台进程的数量遵循固定规则:
- 同一时间仅能有一个前台进程:终端的输入输出控制权只能归属一个进程;
- 同一时间可存在多个后台进程:多个进程可在后台并行运行,不抢占终端控制权。
三、结合 process.cc 代码的执行场景分析
process.cc 的核心逻辑是创建一个无限循环的进程,每秒打印自身 PID,基础编译命令为g++ process.cc -o process,生成可执行文件 process,以下是不同执行方式的具体分析:
1. 执行./process:process 成为前台进程
- 执行该命令后,bash 进程会创建子进程(process 进程),并将终端的前台进程身份切换给 process:
- bash 进程转为后台进程,失去终端输入控制权;
- process 进程占据前台,终端所有输入(如键盘敲击字符)都会被 process 接收,但 process 代码中未处理输入,因此输入内容不会被响应;
- 终端仅显示 process 的循环输出(每秒打印 PID),无法输入新命令(输入的字符不会被 bash 解析)。
2. 执行./process &:process 成为后台进程
- 命令后的
&是终端特殊标记,告知 bash 将创建的 process 进程置于后台运行:- bash 进程保留前台进程身份,仍拥有终端输入控制权;
- process 进程在后台运行,其输出(PID 打印)仍会显示在终端,但不会抢占输入控制权;
- 用户可正常输入命令(如 ls、pwd),bash 会解析并执行,结果回显到终端。
3. Ctrl+C 的处理差异原因Ctrl+C 是 Linux 终端的特殊快捷键,作用是发送SIGINT中断信号,其处理逻辑与进程的前后台身份直接相关:
- 执行./process(process 为前台):Ctrl+C 发送的
SIGINT信号被终端传递给前台的 process 进程,process 未自定义SIGINT处理逻辑,会遵循默认规则终止运行,随后 bash 重新恢复为前台进程; - 执行./process &(process 为后台):Ctrl+C 发送的
SIGINT信号被终端传递给前台的 bash 进程,bash 内置了SIGINT的处理规则(不会终止自身),因此 process 进程无法收到该信号,无法通过 Ctrl+C 终止; - 后台 process 进程的终止方式:需使用 kill 系列命令(如
kill <PID>、kill -9 <PID>),直接向 process 进程发送信号(默认SIGTERM或强制SIGKILL),实现进程终止。
总结
- 前台进程占据终端输入输出控制权,后台进程仅输出不占输入,单个终端仅能有一个前台进程、多个后台进程;
- 终端默认由 bash 作为前台进程,执行./process 会让 process 抢占前台,bash 退居后台;加 & 则 process 后台运行,bash 保留前台;
- Ctrl+C 仅向前台进程发送 SIGINT 信号,后台进程需通过 kill 命令终止。
信号的产生与调用接口
proces.cc1(捕捉信号并且执行自定义动作)
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
// 捕捉2号信号,并且改变2号信号的默认动作,改为执行myhandler函数
// 并且只需要在main函数开始捕捉即可,只要process进程捕捉到用户在发送2好信号时,
// 就会执行myhandler函数的动作,所以这是异步操作
signal(2, myhandler);
while(true)
{
cout << "I am a process, my pid is: " << getpid() << endl;
sleep(1);
}
return 0;
}
process2.cc(自定义一个kill指令)
#include<iostream>
#include<string>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
using namespace std;
void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " signum pid\n\n";
}
int main(int argc, char* argv[0])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signum = stoi(argv[1]);
pid_t pid = stoi(argv[2]);
int n = kill(pid, signum);
if(n == -1)
{
perror("kill");
exit(2);
}
return 0;
}
一、Ctrl+C 终止前台进程的底层逻辑
1. 键盘输入的接收规则Linux 终端中,键盘输入(包括组合键)的接收权归属当前终端的前台进程,后台进程无法接收键盘输入。这是终端会话的核心规则,确保输入指令仅被当前交互的进程响应。
2. Ctrl+C 的本质:发送 2 号信号(SIGINT)Ctrl+C 并非直接终止进程,而是终端将该组合键解析为向前台进程发送 2 号信号(SIGINT,中断信号)。2 号信号属于普通信号,其默认处理动作是终止进程运行,因此前台进程收到该信号后,会执行默认动作退出,这是 Ctrl+C 能杀掉前台进程的核心原因。
二、普通信号(1-31 号)的基础特性
1. 普通信号的范围与查看方式Linux 系统中信号分为普通信号(1-31 号)和实时信号(34-64 号),其中 1-31 号为普通信号,是最常用的信号类型。可通过终端执行kill -l命令查看所有信号的编号与名称,例如:
- 2 号:SIGINT(中断信号,对应 Ctrl+C);
- 3 号:SIGQUIT(退出信号,对应 Ctrl+/);
- 9 号:SIGKILL(强制终止信号);
- 19 号:SIGTSTP(暂停信号,对应 Ctrl+z)。
三、signal 接口修改信号处理动作的原理
1. signal 函数的参数与功能signal 函数是 Linux 中修改进程信号处理动作的基础接口,函数原型为:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 参数 1(signum):需要修改处理动作的信号编号(如 2 代表 SIGINT);
- 参数 2(handler):信号的处理方式,支持三种取值:
- SIG_DFL:执行信号的默认动作(如 2 号信号默认终止进程);
- SIG_IGN:忽略该信号(进程收到后无任何响应);
- 自定义函数指针(如 process1.cc 中的 myhandler):执行自定义处理动作。
2. process1.cc 的执行效果process1.cc 中通过signal(2, myhandler)将 2 号信号的处理动作修改为执行自定义函数 myhandler:
- 运行该进程后,其成为终端前台进程,此时按下 Ctrl+C,终端仍向其发送 2 号信号;
- 进程收到信号后,不再执行默认的终止动作,而是调用 myhandler 函数,打印信号编号,进程继续运行,不会退出。
四、信号的处理方式:三选一规则
进程对每个信号的处理动作仅存在三种可选方式,且只能选择其一:1. 默认动作(SIG_DFL)操作系统为每个信号预设的处理逻辑,是进程未修改信号处理动作时的默认行为,例如 2 号信号终止进程、9 号信号强制终止进程。
2. 自定义动作(自定义函数)通过 signal 等接口注册自定义函数,信号到达时执行该函数逻辑,如 process1.cc 中打印信号编号的行为,属于异步操作(信号可在进程任意执行阶段到达,无需进程主动轮询)。
3. 忽略信号(SIG_IGN)进程明确告知内核忽略该信号,收到后不执行任何动作,信号直接被丢弃。
五、信号的本质:软中断(对比硬件中断)
1. 硬件中断的基础概念硬件中断是硬件设备(如键盘、鼠标、硬盘)向 CPU 发送的异步请求,CPU 收到后会暂停当前执行的指令,转去执行中断处理程序,完成后再回到原指令继续执行。例如键盘按下按键时,键盘控制器向 CPU 发送硬件中断,CPU 响应并读取按键数据。
2. 信号的本质:软中断信号是操作系统模拟硬件中断实现的 “软中断” 机制:
- 异步性:信号可在进程任意运行阶段到达,无需进程主动等待(类似硬件中断无需 CPU 主动轮询);
- 中断处理逻辑:进程收到信号后,会在合适时机(如从内核态切换回用户态时)暂停当前执行的代码,执行信号处理函数,完成后回到原代码继续运行;
- 软件层面实现:信号的产生、传递、处理均由操作系统内核在软件层面完成,无硬件参与,因此称为软中断。
六、键盘常见组合键对应的信号及特殊规则
1. 常见组合键与对应信号终端中常用键盘组合键会被解析为特定普通信号,发送给前台进程:
- Ctrl+C:发送 2 号信号(SIGINT),默认终止进程;
- Ctrl+/:发送 3 号信号(SIGQUIT),默认终止进程并生成核心转储文件;
- Ctrl+z:发送 19 号信号(SIGTSTP),默认暂停进程(进程进入停止状态,可通过 fg 命令恢复)。
2. 不可捕捉的特殊信号9 号信号(SIGKILL)和 19 号信号(SIGTSTP)属于系统保护信号,无法通过 signal 等接口修改处理动作:
- 9 号信号:强制终止进程,无论进程是否注册自定义处理动作,收到后必然终止;
- 19 号信号:强制暂停进程,同样无法被捕捉或忽略,确保终端能强制暂停前台进程。
七、process2.cc 中 kill 函数的解析与使用
1. kill 函数的功能与原型kill 函数是 Linux 系统调用,用于向指定进程发送指定信号,函数原型为:int kill(pid_t pid, int sig);
2. kill 函数的参数解析
- 参数 1(pid):目标进程的 PID(进程标识符),指定需要接收信号的进程;
- 参数 2(sig):需要发送的信号编号(如 2 代表 SIGINT,9 代表 SIGKILL)。
3. kill 函数的返回值
- 返回值为 0:表示信号成功发送给目标进程;
- 返回值为 - 1:表示发送失败,可通过 perror 函数查看失败原因(如目标 PID 不存在、无权限发送信号)。
4. process2.cc 的使用方式process2.cc 封装了 kill 函数的调用逻辑,使用时需传入两个参数:信号编号和目标进程 PID,执行格式为:./process2 信号编号 目标PID
- 执行
./process2 9 12345,等价于终端命令kill -9 12345,向 PID 为 12345 的进程发送 9 号信号,强制终止该进程; - 执行
./process2 2 12345,等价于kill -2 12345,向目标进程发送 2 号信号,触发其 2 号信号的处理动作。
总结
- Ctrl+C 通过向前台进程发送 2 号信号(SIGINT)触发默认终止动作,signal 接口可将该信号的处理方式修改为自定义或忽略;
- 1-31 号为普通信号(kill -l 可查看),信号处理仅支持默认、自定义、忽略三种方式,信号本质是模拟硬件中断的软中断;
- 9/19 号信号不可被捕捉,kill 函数可向指定进程发送信号,process2.cc 封装了该函数,使用方式等价于 kill 命令。
进程的异常
process.cc3(捕捉除0错误的信号)
#include<iostream> // 标准输入输出流(cout)
#include<string> // C++字符串(本例未使用,预留)
#include<sys/types.h>// 系统类型定义(如pid_t,本例未直接使用)
#include<unistd.h> // Linux系统调用(如sleep、fork,本例未直接使用)
#include<signal.h> // 信号处理核心头文件(signal、SIGFPE等)
using namespace std;
// ==================== 信号处理函数 ====================
// 参数signo:触发的信号编号(如SIGFPE对应8)
void handler(int signo)
{
// 打印捕获到的信号编号(用于验证信号处理逻辑)
cout << "...get a sig, number: " << signo << endl;
}
int main()
{
// ==================== 注册信号处理函数 ====================
// signal函数:为指定信号绑定自定义处理函数
// 参数1:要处理的信号(SIGFPE:浮点异常信号,除零错误会触发)
// 参数2:信号处理函数指针(handler)
// 作用:默认情况下SIGFPE会终止进程,此处自定义处理逻辑
signal(SIGFPE, handler);
cout << "begin" << endl;
// ==================== 触发除零错误(核心测试逻辑) ====================
int a = 10;
a /= 0; // 除零运算:CPU触发浮点异常,内核向当前进程发送SIGFPE信号
// ==================== 信号处理后的代码 ====================
// 注意:SIGFPE属于“不可恢复信号”,处理完信号后进程行为未定义,此代码大概率不会执行
cout << "end" << endl;
return 0;
}
process.cc4(捕捉野指针的信号)
#include <iostream> // 标准输入输出流(cout)
#include <string> // C++字符串(本例未使用,预留)
#include <sys/types.h>// 系统类型定义(如pid_t,本例未直接使用)
#include <unistd.h> // Linux系统调用(如sleep、fork,本例未直接使用)
#include <signal.h> // 信号处理核心头文件(signal、SIGSEGV等)
using namespace std;
// ==================== 自定义信号处理函数 ====================
// 参数signo:触发的信号编号(SIGSEGV对应11)
// 作用:捕获信号后打印信号编号,替代默认的进程终止行为
void handler(int signo)
{
cout << "...get a sig, number: " << signo << endl;
}
int main()
{
// ==================== 注册SIGSEGV信号的处理函数 ====================
// signal函数:修改指定信号的默认处理逻辑
// 参数1:SIGSEGV(段错误信号,编号11),由非法内存访问触发
// 参数2:自定义处理函数handler
// 默认行为:SIGSEGV会直接终止进程并生成core dump,此处改为先执行handler再终止
signal(SIGSEGV, handler);
cout << "begin" << endl; // 程序启动标识
// ==================== 触发非法内存访问(核心测试逻辑) ====================
int* ptr = nullptr; // 空指针(指向内存地址0,属于内核空间,用户进程无访问权限)
*ptr = 10; // 解引用空指针:向非法内存地址写入数据,触发段错误,内核发送SIGSEGV
// ==================== 信号处理后的代码(不会执行) ====================
// 说明:SIGSEGV是“不可恢复信号”,即使捕获并处理,进程也无法回到原执行流
cout << "end" << endl;
return 0;
}
一、process.cc3 核心逻辑与循环打印的底层原因
1. process.cc3 的基础执行流程process.cc3 的核心逻辑是注册 8 号信号(SIGFPE,浮点异常信号)的自定义处理函数,随后执行除零运算触发该信号,尝试修改信号的默认终止行为。代码中a /= 0是触发信号的关键操作,signal(SIGFPE, handler)则将 8 号信号的处理动作改为执行handler函数。
2. 除零错误触发信号的硬件 + 系统联动机制除零错误并非由软件直接检测,而是 CPU 硬件层面的异常触发:
- CPU 内置状态寄存器(Status Register),其中包含 “溢出标志位”(Overflow Flag)等关键标识位;
- 当 CPU 执行
a /= 0的除零运算时,因数学上无意义,CPU 会将状态寄存器的溢出标志位从 0(正常)置为 1(异常); - 操作系统内核会持续检测 CPU 的状态寄存器,发现溢出标志位为 1 后,立即向当前运行的 process.cc3 进程发送 8 号信号(SIGFPE)。
3. 循环打印 handler 的核心原因process.cc3 中handler函数仅打印信号编号,未修复 CPU 状态寄存器的异常标志位,导致以下循环逻辑:
- 第一步:CPU 执行
a /= 0,溢出标志位置 1 → 内核发送 SIGFPE 信号 → 进程执行自定义 handler 函数(打印信号编号); - 第二步:handler 执行完成后,进程回到原执行流,继续尝试执行
a /= 0(进程上下文未改变,仍停留在除零指令处); - 第三步:CPU 再次执行除零指令,溢出标志位仍为 1 → 内核再次发送 SIGFPE 信号 → 进程再次执行 handler 函数;
- 循环:上述步骤无限重复,表现为终端持续打印 handler 中的内容,进程不会终止,也不会执行后续的
cout << "end" << endl;语句。
4. 为何 "end" 语句永远不会执行进程收到 SIGFPE 信号后,会暂停当前执行的代码(停在a /= 0处),转去执行 handler 函数。由于 handler 未修复 CPU 的异常状态,执行完成后进程会回到a /= 0指令处重新执行,而非跳过该指令,因此a /= 0后的所有代码(包括 "end" 打印)永远无法被执行。
二、process.cc4 的核心逻辑(对比补充)
1. SIGSEGV 信号的触发机制process.cc4 中int* ptr = nullptr; *ptr = 10;触发 11 号信号(SIGSEGV,段错误信号),其底层逻辑与 SIGFPE 类似但触发源不同:
- 空指针
ptr指向内存地址 0(内核空间,用户进程无读写权限); - 解引用空指针(
*ptr = 10)属于非法内存访问,MMU(内存管理单元)检测到权限异常后,向 CPU 发送硬件中断 → 内核捕获该中断,向进程发送 SIGSEGV 信号。
2. SIGSEGV 与 SIGFPE 的共性:不可恢复信号SIGFPE(除零)和 SIGSEGV(段错误)均属于 “不可恢复信号”:
- 即使通过 signal 注册了自定义处理函数,进程也无法修复导致信号触发的底层异常(CPU 状态寄存器异常 / 内存访问权限异常);
- 处理函数执行完成后,进程会回到触发异常的指令处,导致信号被重复触发(或直接终止进程,取决于系统实现),因此
cout << "end" << endl;同样不会执行。
总结
- process.cc3 循环打印 handler 的核心是:除零错误导致 CPU 状态寄存器溢出标志位置 1,自定义 handler 未修复该标志位,进程回到除零指令处重复触发 SIGFPE 信号;
- CPU 状态寄存器是硬件层面的关键,其异常标志位是操作系统发送 SIGFPE 信号的依据,未修复则异常持续;
- SIGFPE 和 SIGSEGV 均为不可恢复信号,触发后进程无法回到正常执行流,因此异常指令后的代码永远不会执行。
软件的异常
process.cc5(alarm闹钟函数)
#include <iostream> // 标准输入输出流(cout)
#include <unistd.h> // Linux系统调用(alarm、sleep)
#include <signal.h> // 信号相关头文件(SIGALRM默认处理逻辑依赖)
using namespace std;
int main()
{
// ==================== 设置闹钟信号 ====================
// alarm函数:设置一个定时器,指定秒数后内核向当前进程发送SIGALRM信号
// 参数:5 → 5秒后触发SIGALRM
// 返回值:n → 之前未到期的闹钟剩余秒数(首次调用返回0)
int n = alarm(5);
// ==================== 死循环模拟进程运行 ====================
// 每秒打印一次运行状态,模拟进程持续执行
while(true)
{
cout << "process is running..." << endl;
sleep(1); // 休眠1秒,降低CPU占用,同时让输出有时间间隔
}
// 死循环不会退出,以下代码永远不会执行
return 0;
}
process.cc6(捕捉14号信号)
#include <iostream> // 标准输入输出流(cout)
#include <unistd.h> // Linux系统调用(alarm、sleep)
#include <signal.h> // 信号处理核心头文件(signal、SIGALRM等)
using namespace std;
// ==================== 自定义SIGALRM信号处理函数 ====================
// 参数signo:触发的信号编号(SIGALRM对应14)
// 作用:替代SIGALRM的默认终止行为,仅打印信号编号
void handler(int signo)
{
cout << "...get a sig, number: " << signo << endl;
}
int main()
{
// ==================== 注册SIGALRM信号的处理函数 ====================
// signal函数:修改SIGALRM的默认处理逻辑
// 参数1:SIGALRM(闹钟信号,编号14)
// 参数2:自定义处理函数handler
// 核心:默认SIGALRM会终止进程,此处改为执行handler后继续运行
signal(SIGALRM, handler);
// ==================== 设置5秒后触发SIGALRM信号 ====================
// alarm(5):启动一次性定时器,5秒后内核向当前进程发送SIGALRM
// 返回值n:首次调用返回0(无之前未到期的闹钟)
int n = alarm(5);
// ==================== 死循环模拟进程持续运行 ====================
// 每秒打印运行状态,验证信号处理后进程是否继续执行
while(true)
{
cout << "process is running..." << endl;
sleep(1); // 休眠1秒,控制打印频率,降低CPU占用
}
// 死循环不会退出,以下代码永远不会执行
return 0;
}
process.cc7(利用闹钟重复执行特定任务)
#include <iostream> // 标准输入输出流(cout)
#include <unistd.h> // Linux系统调用(alarm、sleep)
#include <signal.h> // 信号处理核心头文件(signal、SIGALRM等)
using namespace std;
// ==================== 业务任务函数 ====================
// 模拟实际业务逻辑:如打印日志、检测状态、数据备份等
void work()
{
cout << "printf log..." << endl; // 示例:打印日志
}
// ==================== SIGALRM信号处理函数 ====================
// 参数signo:触发的信号编号(SIGALRM对应14)
// 核心逻辑:执行业务任务 + 重新设置闹钟,实现周期性触发
void handler(int signo)
{
work(); // 执行自定义业务任务(打印日志)
alarm(5); // 重新设置5秒后触发SIGALRM,形成“周期性闹钟”
}
int main()
{
// ==================== 注册SIGALRM信号处理函数 ====================
// 作用:将SIGALRM的默认终止行为改为执行handler函数
signal(SIGALRM, handler);
// ==================== 启动首次闹钟 ====================
// alarm(5):5秒后触发第一次SIGALRM信号
// 返回值n:首次调用返回0(无之前未到期的闹钟)
int n = alarm(5);
// ==================== 主循环:模拟进程持续运行 ====================
// 死循环表示进程一直在运行(如后台服务),每秒打印运行状态
while(true)
{
cout << "process is running..." << endl;
sleep(1); // 休眠1秒,控制打印频率,降低CPU占用
}
// 死循环不会退出,以下代码永远不会执行
return 0;
}
一、软件异常的核心概念
软件异常是程序运行过程中因逻辑错误、资源访问违规或环境异常导致的非预期行为,区别于硬件故障(如 CPU 损坏),完全由软件层面的操作触发,且会被操作系统捕捉并通过信号机制通知进程。常见的软件异常包括:
- 算术异常:如 process.cc3 中的除零错误(触发 8 号 SIGFPE 信号);
- 内存异常:如 process.cc4 中的空指针解引用(触发 11 号 SIGSEGV 信号);
- 通信异常:如管道通信中读端关闭后写端持续写入(触发 13 号 SIGPIPE 信号)。
管道通信中读端关闭导致写端被终止的逻辑:管道是半双工的进程间通信方式,依赖 “读端 - 写端” 的双向关联:
- 当读端进程关闭管道描述符后,管道失去数据接收方,此时写端进程继续写入数据属于 “无意义操作”(数据写入后无进程读取,只会浪费内核缓冲区和 CPU 资源);
- 操作系统为避免低效率、无意义的资源消耗,会向写端进程发送 13 号信号(SIGPIPE,管道破裂信号);
- SIGPIPE 信号的默认处理动作是终止进程,因此写端进程会被操作系统终止,这是操作系统对资源的 “高效管理策略”,避免无效操作占用系统资源。
二、alarm 函数的参数与返回值解析
alarm 函数是 Linux 系统中设置 “软件定时器” 的核心接口,用于让内核在指定秒数后向当前进程发送 14 号信号(SIGALRM,闹钟信号),函数原型为:
unsigned int alarm(unsigned int seconds);
1. 参数解析(seconds)
- 类型:无符号整数(unsigned int),表示定时器的超时秒数;
- 取值规则:
- 传入
5:设置 5 秒后内核向当前进程发送 SIGALRM 信号; - 传入
0:取消当前进程中所有未到期的闹钟(若存在),内核不再发送 SIGALRM 信号; - 传入非 0 值:覆盖当前进程中已存在的未到期闹钟(新的超时时间替代旧值)。
- 传入
2. 返回值解析
- 类型:无符号整数(unsigned int),表示 “上一个未到期闹钟的剩余秒数”;
- 取值场景:
- 首次调用 alarm(进程无未到期闹钟):返回
0(如 process.cc5/6/7 中int n = alarm(5);的 n 值为 0); - 非首次调用(进程已有未到期闹钟):返回上一个闹钟的剩余秒数。例如:
alarm(10); // 设置10秒后触发闹钟 sleep(3); // 休眠3秒,剩余7秒 int ret = alarm(5); // 覆盖旧闹钟,ret返回7(旧闹钟剩余7秒)
- 首次调用 alarm(进程无未到期闹钟):返回
- 核心特性:alarm 设置的是一次性定时器(默认触发一次 SIGALRM 后定时器失效),需在信号处理函数中重新调用 alarm 才能实现周期性触发(如 process.cc7)。
三、process.cc5/6/7 的核心逻辑对比
1. process.cc5:默认 SIGALRM 行为
- 逻辑:仅设置 5 秒后触发 SIGALRM 信号,未修改信号处理动作;
- 执行结果:进程每秒打印
process is running...,5 秒后收到 SIGALRM 信号(默认动作是终止进程),进程直接退出,不再打印。
2. process.cc6:自定义 SIGALRM 行为
- 逻辑:通过
signal(SIGALRM, handler)将 SIGALRM 的默认终止行为改为执行自定义 handler 函数; - 执行结果:5 秒后触发 SIGALRM,进程执行 handler(打印信号编号 14),但因未重新设置闹钟,后续仅持续打印
process is running...,无再次触发信号的逻辑。
3. process.cc7:周期性触发 SIGALRM
- 核心改进:在 handler 函数中调用
alarm(5),实现 “触发一次信号→重新设置闹钟→再次触发信号” 的循环; - 执行结果:
- 第 5 秒:首次触发 SIGALRM,执行 handler(打印
printf log...),并重新设置 5 秒闹钟; - 第 10 秒:再次触发 SIGALRM,重复执行 handler + 重置闹钟;
- 循环:每 5 秒执行一次 work 函数,进程主循环持续打印
process is running...,实现 “周期性执行业务任务” 的效果。
- 第 5 秒:首次触发 SIGALRM,执行 handler(打印
四、补充:SIGALRM 信号的特性
- SIGALRM 属于 1-31 号普通信号,可通过
signal接口修改处理动作(默认终止进程); - alarm 定时器由内核维护,与进程的睡眠 / 运行状态无关(即使进程 sleep,定时器仍会正常计时);
- 一个进程仅能设置一个 alarm 定时器,新的 alarm 调用会覆盖旧的未到期定时器。
总结
- 软件异常是软件层面的非预期行为,操作系统通过信号机制通知进程,管道读端关闭后写端持续写入会触发 13 号 SIGPIPE 信号,操作系统终止写端以避免无意义的资源消耗;
- alarm 函数参数为定时器超时秒数(0 表示取消闹钟),返回值为上一个未到期闹钟的剩余秒数,首次调用返回 0;
- 单次 alarm 仅触发一次 SIGALRM,需在信号处理函数中重新调用 alarm 才能实现周期性触发(如 process.cc7 的日志打印逻辑)。
信号的发送与保存

process.cc8(忽略某个信号)
#include <iostream> // 标准输入输出流(cout)
#include <signal.h> // 信号处理核心头文件(signal、SIG_IGN、SIGINT等)
#include <unistd.h> // Linux系统调用(getpid、sleep)
using namespace std;
int main()
{
// ==================== 忽略SIGINT信号(2号信号) ====================
// signal函数:修改指定信号的处理行为
// 参数1:SIGINT(2号信号,对应键盘Ctrl+C,默认行为是终止进程)
// 参数2:SIG_IGN(Signal Ignore,表示“忽略该信号”)
// 核心作用:进程运行期间,即使按下Ctrl+C,内核发送的SIGINT信号会被忽略,进程不会终止
signal(2, SIG_IGN); // 等价写法:signal(SIGINT, SIG_IGN); (2是SIGINT的宏定义值)
// ==================== 死循环模拟进程持续运行 ====================
while(true)
{
// 打印当前进程的PID(进程ID),便于通过kill命令手动终止进程(如kill -9 PID)
cout << "My pid is:" << getpid() << endl;
sleep(1); // 休眠1秒,每秒打印一次PID,降低CPU占用
}
// 死循环不会退出,以下代码永远不会执行
return 0;
}
一、信号的发送与保存:本质是修改进程 PCB 中的信号位图
操作系统给进程发送信号,本质上是修改进程的 PCB(进程控制块,即task_struct)内部的信号管理数据结构,而非直接 “传递” 一个数据对象。
- 进程的信号管理结构:在
task_struct中,存在用于管理普通信号(1-31 号)的位图(bitmask)变量。该变量的每一位对应一个信号,从第 1 位到第 31 位,分别对应信号 1 到信号 31。最低位(第 0 位)不使用。 - 发送信号的操作:当操作系统要向进程发送某个信号(如 2 号 SIGINT)时,只需将该进程 PCB 中信号位图对应信号的比特位从
0置为1。 - 权限约束:
task_struct是内核管理进程的核心数据结构,只有作为进程管理者的操作系统内核才有资格修改其内部属性,用户态进程无法直接修改。
二、信号的核心状态:未决(Pending)与递达(Delivery)
- 信号递达(Delivery):指信号被进程实际处理的过程,即执行该信号的默认动作、自定义处理函数或忽略该信号。
- 信号未决(Pending):指信号从产生(被操作系统发送,位图被置 1)到被递达(被进程处理)之间的状态。此时信号已被记录,但尚未处理。
三、信号保存与管理的底层原理:三张表
进程对信号的控制,是通过维护三张核心表来实现的:
1. 信号阻塞表(Block Mask)
- 结构:一个位图(bitmask),每一位对应一个信号。
- 含义:
- 位为
0:表示该信号未被阻塞,可以被递达。 - 位为
1:表示该信号被阻塞。即使进程收到该信号(Pending 位为 1),也不会被递达,即不会执行任何处理动作,直到阻塞被解除。
- 位为
- 示例:图中
SIGINT(2)和SIGQUIT(3)的 Block 位为1,表示这两个信号被阻塞。
2. 信号未决表(Pending Mask)
- 结构:一个位图(bitmask),每一位对应一个信号。
- 含义:
- 位为
0:表示进程未收到该信号。 - 位为
1:表示进程已收到该信号,该信号处于未决状态。
- 位为
- 普通信号特性:对于 1-31 号普通信号,Pending 表只记录 “是否收到”,不记录次数。如果多次发送同一个信号,Pending 位只会从
0置1一次。阻塞解除后,也只会被递达一次。 - 实时信号特性:31 号之后的实时信号(Real-time Signals)则通过队列管理,可以记录多次发送,阻塞解除后会按顺序递达多次。
- 示例:图中
SIGINT(2)的 Pending 位为1,表示进程已收到 2 号信号,但因 Block 位为1而处于未决状态。
3. 信号处理函数表(Handler Table)
- 结构:一个函数指针数组,数组下标对应信号编号。
- 含义:每个元素是一个函数指针,定义了当对应信号递达时,进程应执行的处理动作。支持三种取值:
SIG_DFL:执行操作系统为该信号预设的默认动作(如终止进程)。SIG_IGN:忽略该信号,递达后不做任何处理。- 自定义函数指针:执行用户通过
signal或sigaction注册的自定义处理函数。
signal函数的本质:修改 Handler 表中指定信号对应的函数指针,将其从默认值(如SIG_DFL)替换为用户提供的自定义函数或SIG_IGN。- 示例:图中
SIGHUP(1)的 Handler 为SIG_DFL(默认动作),SIGINT(2)的 Handler 为SIG_IGN(忽略),SIGQUIT(3)的 Handler 指向用户自定义函数sighandler。
四、关键概念区分:阻塞(Block)与忽略(Ignore)
- 阻塞(Block):是信号递达前的状态控制。只要信号被阻塞(Block 位为 1),无论是否已产生(Pending 位为 1),都不会被递达,进程不会执行任何处理。
- 忽略(Ignore):是信号递达后的一种处理动作。信号已被递达,但进程选择不执行任何操作。
- 示例:
process.cc8中,signal(2, SIG_IGN)将 2 号信号的 Handler 设置为SIG_IGN。当进程收到 Ctrl+C(2 号信号)时,信号会被递达,但进程选择忽略,因此不会终止,而是继续执行循环。
总结
- 信号发送:操作系统修改进程 PCB 中 Pending 表的对应位,记录信号的到来。
- 信号控制:通过 Block 表决定信号是否可以被递达;通过 Handler 表决定信号递达后的处理方式。
- 信号递达:当信号未被阻塞(Block 位为 0)且处于未决状态(Pending 位为 1)时,进程会在合适时机(如从内核态返回用户态)处理该信号,执行 Handler 表中指定的动作。
信号集操作函数
process.cc9(信号阻塞 / 未决队列 / 解除阻塞演示)
#include <iostream> // 标准输入输出流(cout)
#include <signal.h> // 信号处理核心头文件(signal、sigset_t、sigprocmask等)
#include <unistd.h> // Linux系统调用(sleep)
using namespace std;
// ==================== 自定义信号处理函数 ====================
// 参数signo:触发的信号编号(本例中为2号SIGINT)
// 作用:收到信号后打印信号编号,验证信号最终被处理
void handler(int signo)
{
cout << "...get a sig, number: " << signo << endl;
}
// ==================== 打印信号未决(pending)位图 ====================
// 参数pending:待打印的未决信号集(位图结构)
// 作用:以二进制形式打印1-31号信号的未决状态(1=未决,0=未决),直观看到信号是否被挂起
void PrintPending(sigset_t& pending)
{
// 遍历1-31号信号(Linux常用信号范围,32-64为实时信号,本例不关注)
for(int signo = 1; signo<= 31; signo++)
{
// sigismember:检查指定信号是否在信号集中(返回1=存在,0=不存在)
if(sigismember(&pending, signo))
{
cout << "1"; // 该信号处于未决状态(被阻塞,未处理)
}
else
{
cout << "0"; // 该信号无未决状态
}
}
cout << endl; // 每行打印完换行,便于观察
}
int main()
{
// ==================== 步骤1:注册2号信号的自定义处理函数 ====================
// 2号信号是SIGINT(Ctrl+C触发),默认行为是终止进程,此处改为执行handler函数
signal(2, handler);
// ==================== 步骤2:阻塞2号信号 ====================
// sigset_t:信号集类型,本质是位图(每一位对应一个信号的“阻塞/未决”状态)
sigset_t newSet; // 自定义的新阻塞信号集(要设置给进程的)
sigset_t oldSet; // 保存进程原来的阻塞信号集(用于后续恢复)
// 清空信号集:将newSet/oldSet的所有位置0(初始无任何信号被阻塞)
sigemptyset(&newSet);
sigemptyset(&oldSet);
// 将2号信号添加到newSet中(仅修改用户态的newSet位图,2号位置1,表示要阻塞2号信号)
// 注意:此时仅修改了用户态变量,未影响内核中进程的阻塞位图
sigaddset(&newSet, 2);
// 核心:修改进程内核中的阻塞(block)位图
// SIG_SETMASK:用newSet完全覆盖进程当前的block位图(而非追加/删除)
// 参数3 &oldSet:将修改前的block位图保存到oldSet中(用于后续恢复)
// 执行后,进程的block位图中2号位被置1,2号信号被阻塞
sigprocmask(SIG_SETMASK, &newSet, &oldSet);
// ==================== 步骤3:循环打印未决(pending)信号集 ====================
sigset_t pending; // 存储进程的未决信号集(被阻塞的信号会进入未决队列)
int count = 0; // 计数器:控制10秒后解除阻塞
while(true)
{
// 获取当前进程的未决(pending)信号集(内核态→用户态)
// pending位图中,某信号位为1表示:信号已到达,但因被阻塞未被处理(未决状态)
int n = sigpending(&pending);
// 错误处理:获取未决信号集失败则跳过本次循环
if(n < 0)
{
continue;
}
// 打印未决位图(直观看到2号信号是否未决)
PrintPending(pending);
sleep(1); // 每秒打印一次,便于观察状态变化
// ==================== 步骤4:10秒后解除2号信号的阻塞 ====================
count++;
if(count == 10)
{
cout << "unblock 2 signo" << endl;
// 恢复进程原来的阻塞位图(oldSet),解除对2号信号的阻塞
// 参数3 nullptr:无需保存当前的block位图(不需要)
sigprocmask(SIG_SETMASK, &oldSet, nullptr);
}
}
return 0;
}
process.cc9 代码现象
0000000000000000000000000000000 // 初始pending全0
0000000000000000000000000000000
(按下Ctrl+C)
0100000000000000000000000000000 // 2号信号未决(第2位为1)
0100000000000000000000000000000
...(持续打印,第2位保持1)
unblock 2 signo // 第10秒解除阻塞
...get a sig, number: 2 // 未决的2号信号被处理
0000000000000000000000000000000 // pending位图2号位变回0
(后续按Ctrl+C)
...get a sig, number: 2 // 信号立即处理,无未决
0000000000000000000000000000000 // pending位图会一直全为0
一、sigset_t 类型的核心含义
sigset_t是操作系统提供给用户态的专用数据类型,本质是一个位图结构(每一位对应一个信号的状态)。其核心作用是作为 “用户态与内核态交互的桥梁”:
- 用户态程序可通过 sigset_t 类型的变量,定义 “要阻塞的信号集合” 或 “要查询的未决信号集合”;
- 再通过 sig 系列系统调用,将该变量的内容同步到内核的
task_struct中,实现对 ** 信号阻塞表(Block Mask)或信号未决表(Pending Mask)** 的获取、修改、覆盖等操作; - sigset_t 变量仅在用户态生效,需通过系统调用才能影响内核中的信号管理表。
二、sig 系列函数的详细解析(参数、返回值、操作的表)
(注:以下函数分为 “用户态信号集操作” 和 “内核态信号表操作” 两类)
1. 用户态信号集操作函数(仅修改用户态 sigset_t 变量,不涉及内核表)sigemptyset 函数
- 函数原型:
int sigemptyset(sigset_t *set); - 参数:
set→ 指向要清空的用户态 sigset_t 变量的指针; - 功能:将用户态 sigset_t 变量的所有位置 0(初始化为 “无任何信号” 的状态);
- 返回值:成功返回
0,失败返回-1; - 操作的表:仅操作用户态 sigset_t 变量,不涉及内核的 Block/Pending/Handler 表。
sigaddset 函数
- 函数原型:
int sigaddset(sigset_t *set, int signum); - 参数:
set→ 指向目标用户态 sigset_t 变量的指针;signum→ 要添加到信号集中的信号编号(如 2 代表 SIGINT);
- 功能:将用户态 sigset_t 变量中指定信号的位置 1(表示该信号被纳入集合);
- 返回值:成功返回
0,失败返回-1; - 操作的表:仅操作用户态 sigset_t 变量,不涉及内核表。
sigdelset 函数
- 函数原型:
int sigdelset(sigset_t *set, int signum); - 参数:
set→ 指向目标用户态 sigset_t 变量的指针;signum→ 要从信号集中删除的信号编号;
- 功能:将用户态 sigset_t 变量中指定信号的位置 0(表示该信号被移出集合);
- 返回值:成功返回
0,失败返回-1; - 操作的表:仅操作用户态 sigset_t 变量,不涉及内核表。
sigismember 函数
- 函数原型:
int sigismember(const sigset_t *set, int signum); - 参数:
set→ 指向要查询的用户态 sigset_t 变量的指针;signum→ 要查询的信号编号;
- 功能:检查用户态 sigset_t 变量中指定信号的位是否为 1;
- 返回值:
- 位为 1 → 返回
1(信号在集合中); - 位为 0 → 返回
0(信号不在集合中); - 失败返回
-1;
- 位为 1 → 返回
- 操作的表:仅查询用户态 sigset_t 变量,不涉及内核表。
2. 内核态信号表操作函数(直接修改 / 获取内核 task_struct 中的表)sigprocmask 函数
- 函数原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); - 参数:
how→ 操作方式,支持三种宏:SIG_SETMASK:用set指向的信号集完全覆盖内核的 Block 表;SIG_BLOCK:将set中的信号追加到内核的 Block 表(原阻塞信号保留);SIG_UNBLOCK:将set中的信号从内核的 Block 表中移除(解除阻塞);
set→ 指向用户态 sigset_t 变量的指针,提供要设置给内核的信号集合;oldset→ 指向用户态 sigset_t 变量的指针,用于保存修改前内核的 Block 表(后续可恢复),若不需要保存可设为nullptr;
- 功能:修改内核的信号阻塞表(Block Mask),是控制信号阻塞的核心接口;
- 返回值:成功返回
0,失败返回-1; - 操作的表:直接修改 / 获取内核的信号阻塞表(Block Mask)。
sigpending 函数
- 函数原型:
int sigpending(sigset_t *set); - 参数:
set→ 指向用户态 sigset_t 变量的指针,用于存储从内核获取的未决信号集; - 功能:获取内核的信号未决表(Pending Mask),将其内容同步到用户态的
set变量中; - 返回值:成功返回
0,失败返回-1; - 操作的表:仅查询内核的信号未决表(Pending Mask),不修改。
signal 函数
- 函数原型:
sighandler_t signal(int signum, sighandler_t handler); - 参数:
signum→ 要修改处理动作的信号编号;handler→ 信号处理方式(SIG_DFL/SIG_IGN/ 自定义函数指针);
- 功能:修改内核的信号处理函数表(Handler Table),将指定信号的处理动作替换为用户设置的方式;
- 返回值:成功返回该信号之前的处理函数指针,失败返回
SIG_ERR; - 操作的表:直接修改内核的信号处理函数表(Handler Table)。
三、process.cc9 代码的逐部分逻辑解释
1. 注册 2 号信号的自定义处理函数
signal(2, handler);
- 逻辑:调用
signal函数,修改内核的 Handler 表,将 2 号信号(SIGINT)的处理动作从默认的 “终止进程” 改为执行自定义handler函数。
2. 阻塞 2 号信号
sigset_t newSet;
sigset_t oldSet;
sigemptyset(&newSet);
sigemptyset(&oldSet);
sigaddset(&newSet, 2);
sigprocmask(SIG_SETMASK, &newSet, &oldSet);
- 逻辑:
- 定义两个用户态 sigset_t 变量
newSet和oldSet; - 用
sigemptyset清空两个变量(所有位置 0); - 用
sigaddset将newSet的第 2 位置 1(表示要阻塞 2 号信号); - 用
sigprocmask将newSet的内容同步到内核的 Block 表,同时将修改前的 Block 表保存到oldSet中; - 执行后,内核的 Block 表第 2 位为 1,2 号信号被阻塞。
- 定义两个用户态 sigset_t 变量
3. 循环打印未决信号集
sigset_t pending;
int count = 0;
while(true)
{
sigpending(&pending);
if(n < 0) continue;
PrintPending(pending);
sleep(1);
count++;
if(count == 10)
{
cout << "unblock 2 signo" << endl;
sigprocmask(SIG_SETMASK, &oldSet, nullptr);
}
}
- 逻辑:
- 定义用户态 sigset_t 变量
pending,用于存储从内核获取的未决信号集; - 进入死循环,每秒调用一次
sigpending,将内核的 Pending 表同步到pending变量; - 调用
PrintPending函数,遍历 1-31 号信号,用sigismember检查每一位,打印 0/1 序列; - 计数器
count到 10 时,调用sigprocmask用oldSet(修改前的 Block 表)覆盖内核的 Block 表,解除对 2 号信号的阻塞。
- 定义用户态 sigset_t 变量
四、process.cc9 代码现象的底层原因
1. 初始阶段:pending 位图全 0
- 原因:未发送任何信号,内核的 Pending 表所有位为 0,同步到用户态后打印全 0 序列。
2. 按下 Ctrl+C 后:pending 位图第 2 位变 1
- 原因:
- Ctrl+C 向前台进程发送 2 号信号,操作系统将内核的 Pending 表第 2 位置 1;
- 此时 2 号信号被阻塞(Block 表第 2 位为 1),信号无法递达,停留在未决状态;
sigpending将 Pending 表同步到用户态,打印出第 2 位为 1 的序列。
3. 第 10 秒解除阻塞后:pending 位图变回全 0,且后续按 Ctrl+C 看不到 pending 位为 1
- 原因:
- 解除阻塞后,内核检测到 Pending 表第 2 位为 1 且 Block 表第 2 位为 0,立即将信号递达,执行
handler函数; - 信号递达后,内核自动将 Pending 表第 2 位清 0;
- 后续按 Ctrl+C 时,2 号信号未被阻塞,信号 “到达即处理”:Pending 表第 2 位被置 1 后,内核立即递达信号并清 0,整个过程发生在内核态,耗时极短(纳秒级);
- 等代码执行到
sigpending和PrintPending时,Pending 表早已恢复全 0,因此肉眼看不到第 2 位为 1 的状态。
- 解除阻塞后,内核检测到 Pending 表第 2 位为 1 且 Block 表第 2 位为 0,立即将信号递达,执行
五、9 号(SIGKILL)与 19 号(SIGSTOP)信号的特殊规则
1. 不可被阻塞
- 规则:9 号和 19 号信号无法被
sigprocmask阻塞,即使将其添加到 Block 表中,内核也会忽略该设置,信号仍能正常递达。
2. 不可被捕捉 / 自定义处理
- 规则:无法通过
signal或sigaction为 9 号和 19 号信号注册自定义处理函数,内核会强制忽略用户的设置,保持默认处理动作。
3. 不可被忽略
- 规则:无法将 9 号和 19 号信号的处理动作设置为
SIG_IGN,内核会强制忽略该设置,执行默认动作。
4. 强制默认动作
- 9 号信号(SIGKILL):默认动作是强制终止进程,无论进程处于什么状态,只要收到 9 号信号,必然被终止;
- 19 号信号(SIGSTOP):默认动作是强制暂停进程,进程进入停止状态,需通过
fg命令或发送 18 号信号(SIGCONT)恢复。
总结
- sigset_t 是用户态与内核态交互的位图桥梁,需通过 sig 系列系统调用影响内核的信号管理表;
- sigprocmask 操作 Block 表,sigpending 操作 Pending 表,signal 操作 Handler 表;
- 解除阻塞后,未决信号立即递达并清 0,后续无阻塞时信号处理极快,肉眼看不到 Pending 位变化;
- 9 号和 19 号信号是系统保护信号,不可被阻塞、捕捉或忽略,强制执行默认动作。
信号的处理
一、进程地址空间与内核态 / 用户态的底层架构
1. 进程虚拟地址空间的划分32 位 Linux 系统中,进程的虚拟地址空间固定划分为两部分:0-3GB 为用户空间,3-4GB 为内核空间。用户空间用于存放进程自身的代码、数据、堆、栈等私有内容;内核空间用于映射操作系统的核心代码、数据、硬件驱动接口,是所有进程共享的公共区域。
2. 页表的独立性与唯一性
- 用户级页表:每个进程拥有独立的用户级页表,负责映射 0-3GB 用户空间的虚拟地址到物理内存,保证进程地址空间的独立性,实现进程间的内存隔离。
- 内核级页表:全系统仅存在一份全局的内核级页表,负责映射 3-4GB 内核空间的虚拟地址到物理内存。所有进程的 3-4GB 虚拟地址空间,都通过这同一份内核级页表完成映射,因此所有进程看到的内核空间内容完全一致。
3. 进程访问内核的本质进程调用系统接口、访问内核数据时,本质是在自身虚拟地址空间的 3-4GB 内核空间执行对应代码,无需切换地址空间,仅需切换 CPU 的特权级,即可完成内核资源的访问。
二、操作系统运行的核心驱动:时钟中断
1. 操作系统的运行本质操作系统是基于硬件时钟中断驱动的死循环,无硬件中断触发时,操作系统处于等待状态,所有进程管理、资源调度、硬件管理等核心逻辑,均由硬件中断触发执行。
2. 时钟硬件的工作机制计算机主板集成可编程时钟芯片,每隔固定的微秒 / 纳秒级时间间隔,通过 CPU 针脚向 CPU 发送高低电平的时钟中断信号。CPU 的中断寄存器接收该信号后,会暂停当前执行的指令,触发中断处理流程。
3. 中断向量表的作用CPU 收到中断信号后,会查询内核预置的中断向量表,执行表中对应中断号的处理函数。时钟中断对应的处理函数,核心逻辑包含进程时间片更新、进程调度、系统计时、负载统计等。
4. 执行流的推进逻辑硬件时钟中断持续触发,推动操作系统执行调度、管理等核心逻辑;操作系统再通过调度机制,将 CPU 时间分片分配给各个用户进程,推动进程代码的持续执行。
三、CPU 特权级与内核态 / 用户态切换的硬件基础
1. cs 寄存器与特权级标识CPU 的 cs(代码段)寄存器的最低两位,存储 CPL(当前特权级),可表示 0-3 四个特权等级:
- 特权级 0(内核态):拥有最高权限,可执行所有 CPU 指令,访问所有内存地址、硬件寄存器;
- 特权级 3(用户态):拥有最低权限,仅可执行非特权 CPU 指令,仅能访问进程自身的用户空间虚拟地址。
2. 态切换的合法触发路径用户态切换到内核态,仅能通过三种合法路径触发,用户进程无法直接修改 cs 寄存器的特权级:
- 系统调用(软中断,如 int 80 指令、sysenter 指令);
- 硬件中断(如时钟中断、键盘中断、磁盘中断);
- 异常(如除零错误、缺页异常、段错误)。
3. 用户态与内核态的核心区别
- 用户态:仅能访问进程自身 0-3GB 用户空间的代码与数据,无法直接操作硬件、修改内核数据;
- 内核态:可访问整个 4GB 虚拟地址空间,可执行所有 CPU 指令,完成硬件操作、进程管理、资源调度等核心工作。
四、信号处理的完整底层流程(用户态 - 内核态往返)
信号处理的本质:信号处理是内核在进程从内核态返回用户态的窗口期,完成的异步事件处理流程。所有信号的递达与处理,都必须在这个窗口期完成,核心依赖进程 PCB 中的 block 表、pending 表、handler 表。
完整执行步骤1. 用户态进入内核态进程执行主控制流的指令时,因系统调用、硬件中断、异常触发,陷入内核,完成用户态到内核态的切换,内核完成对应事件的处理(如系统调用逻辑、中断处理、异常修复)。
2. 内核态的信号检查与处理入口内核准备从内核态返回用户态之前,会调用do_signal()函数,完成信号的合法性检查:
- 先检查 pending 表,确认是否有未决信号(对应位为 1);
- 再检查 block 表,确认该信号是否被阻塞(对应位为 0 表示可递达);
- 仅当信号处于未决且未被阻塞时,才会进入信号递达流程。
3. 信号的三种处理动作分支内核根据 handler 表中该信号的处理函数指针,执行对应分支:
- 分支 1:忽略信号(SIG_IGN)内核直接将 pending 表中该信号的对应位清 0,无需额外处理,直接从内核态返回用户态,继续执行主控制流被中断的指令。
- 分支 2:执行默认动作(SIG_DFL)内核执行该信号的预设默认动作,常见动作包括终止进程、终止进程并生成 core dump、暂停进程、继续进程。若动作是终止进程,内核会直接销毁进程,无需返回用户态;其他动作完成后,清 0 pending 位,返回用户态继续执行。
- 分支 3:执行用户自定义处理函数该分支需要两次用户态 - 内核态的往返,是信号处理最复杂的场景:第一步:内核修改进程的用户态栈,将信号处理函数的入口地址压入栈中,设置进程的指令指针寄存器(eip/rip)指向信号处理函数,然后从内核态返回用户态,CPU 直接执行用户自定义的信号处理函数,而非回到主控制流。第二步:信号处理函数执行完毕后,会自动执行特殊的系统调用
sigreturn,再次从用户态陷入内核态,完成信号处理的收尾工作。第三步:内核执行sys_sigreturn系统调用,恢复进程主控制流的上下文(寄存器、栈指针、指令指针),将 pending 表中该信号的对应位清 0,然后从内核态返回用户态,从主控制流上次被中断的位置继续向下执行。
五、无主动系统调用时进程仍能处理信号的底层原因
1. 进程调度与态切换的必然性即使进程未主动调用任何系统级接口,进程的运行周期内也会频繁触发用户态与内核态的切换,核心原因是进程的时间片调度机制:
- 每个进程被 CPU 调度执行时,都拥有固定的毫秒级时间片;
- 进程在用户态执行主控制流代码时,时钟中断会持续触发,内核会不断更新该进程的时间片剩余时长;
- 当进程的时间片耗尽时,CPU 会触发时钟中断,陷入内核态,内核执行调度函数,将当前进程从 CPU 上剥离,保存进程的上下文(寄存器、栈、页表等)到 PCB 中,然后调度其他进程到 CPU 上执行;
- 当该进程被二次调度时,内核会将进程的上下文恢复到 CPU 硬件中,然后从内核态返回用户态,继续执行进程的主控制流代码。
2. 信号处理的窗口期保证上述时间片耗尽、进程调度的过程,必然会触发用户态到内核态的切换。内核在每次从内核态返回用户态之前,都会执行do_signal()函数检查并处理可递达的信号。因此,即使进程无主动系统调用,也会在调度触发的态切换窗口期,完成信号的检查与处理,不会出现信号无法被接收的情况。
信号的捕捉
process.cc10(sigaction 实现信号处理 - 替代 signal 函数)
#include <iostream> // 标准输入输出流(cout)
#include <string> // C++字符串(本例未使用,预留)
#include <string.h> // C字符串操作(memset函数)
#include <sys/types.h>// 系统类型定义(如pid_t)
#include <unistd.h> // Linux系统调用(getpid、sleep)
#include <signal.h> // 信号处理核心头文件(sigaction、struct sigaction等)
using namespace std;
// ==================== 自定义信号处理函数 ====================
// 参数signo:触发的信号编号(本例中为2号SIGINT)
// 作用:收到信号后打印信号编号,验证sigaction的信号处理逻辑
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
}
int main()
{
// ==================== 定义sigaction结构体(信号处理配置) ====================
// struct sigaction:比signal函数更强大的信号处理配置结构体,支持更精细的信号控制
struct sigaction newact; // 新的信号处理配置(要设置给2号信号的)
struct sigaction oldact; // 保存原来的信号处理配置(用于后续恢复,本例未使用)
// 初始化结构体:将newact/oldact的所有字节置0,避免随机值影响配置
// memset是“内存填充函数”,第三个参数是填充的字节数
memset(&newact, 0, sizeof(newact));
memset(&oldact, 0, sizeof(oldact));
// ==================== 配置新的信号处理行为 ====================
// sa_handler:指定信号处理函数(等价于signal函数的第二个参数)
// 此处绑定自定义的handler函数,替代2号信号的默认行为(终止进程)
newact.sa_handler = handler;
// ==================== 应用信号处理配置(核心) ====================
// sigaction函数:修改指定信号的处理方式(比signal函数更标准、可移植性更强)
// 参数1:要处理的信号(2号=SIGINT,Ctrl+C触发)
// 参数2:新的处理配置(newact)
// 参数3:保存旧的处理配置到oldact(nullptr表示不保存)
// 作用:将2号信号的处理方式设置为newact中配置的handler函数
sigaction(2, &newact, &oldact);
// ==================== 死循环模拟进程持续运行 ====================
while(true)
{
// 打印当前进程PID(便于通过kill命令手动终止进程)
cout << "My pid is : " << getpid() << endl;
sleep(1); // 每秒打印一次,降低CPU占用
}
return 0;
}
process.cc11(sigaction 的 sa_mask 信号屏蔽集演示)
#include <iostream> // 标准输入输出流(cout)
#include <string> // C++字符串(本例未使用,预留)
#include <string.h> // C字符串操作(memset函数)
#include <sys/types.h>// 系统类型定义(如pid_t)
#include <unistd.h> // Linux系统调用(getpid、sleep)
#include <signal.h> // 信号处理核心头文件(sigaction、sigset_t等)
using namespace std;
// ==================== 打印未决(pending)信号位图 ====================
// 作用:以二进制形式打印1-31号信号的未决状态(1=未决,0=无未决),直观观察信号是否被挂起
void PrintPending()
{
sigset_t pending; // 存储进程的未决信号集(位图结构)
// 获取当前进程的未决信号集(内核态→用户态)
sigpending(&pending);
// 遍历1-31号信号(Linux常用非实时信号范围)
for(int signo = 1; signo <= 31; signo++)
{
// sigismember:检查指定信号是否在未决信号集中
if(sigismember(&pending, signo))
cout << "1"; // 该信号未决(到达但未处理)
else
cout << "0"; // 该信号无未决
}
cout << endl; // 每行打印完换行,便于观察
}
// ==================== 自定义信号处理函数 ====================
// 参数signo:触发的信号编号(本例中为2号SIGINT)
// 核心逻辑:打印信号编号 + 死循环打印未决位图,验证sa_mask的屏蔽效果
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
// 死循环打印未决位图:在处理2号信号期间,观察1/3/4号信号是否会进入未决队列
while(true)
{
PrintPending();
sleep(1);
}
}
int main()
{
// ==================== 定义sigaction结构体(信号处理配置) ====================
// newact:新的信号处理配置(针对2号信号)
// oldact:保存原来的信号处理配置(本例未使用,仅做备份)
struct sigaction newact;
struct sigaction oldact;
// 初始化结构体:将所有字节置0,避免随机值影响配置(如sa_mask、sa_flags等)
memset(&newact, 0, sizeof(newact));
memset(&oldact, 0, sizeof(oldact));
// ==================== 配置sa_mask信号屏蔽集(核心) ====================
// sa_mask:信号处理期间的“临时屏蔽集”——处理当前信号时,自动屏蔽sa_mask中的信号
// 1. 清空sa_mask(初始无任何信号被屏蔽)
sigemptyset(&newact.sa_mask);
// 2. 向sa_mask中添加1/3/4号信号:处理2号信号时,临时屏蔽这三个信号
sigaddset(&newact.sa_mask, 1);
sigaddset(&newact.sa_mask, 3);
sigaddset(&newact.sa_mask, 4);
// ==================== 配置信号处理函数 ====================
// 指定2号信号的处理函数为自定义的handler(替代默认终止行为)
newact.sa_handler = handler; // 可选值:SIG_IGN(忽略)/SIG_DFL(默认)/自定义函数
// ==================== 应用信号处理配置 ====================
// sigaction函数:将2号信号的处理方式设置为newact中的配置
// 参数1:2号信号(SIGINT,Ctrl+C触发)
// 参数2:新配置newact
// 参数3:保存旧配置到oldact(nullptr表示不保存)
sigaction(2, &newact, &oldact);
// ==================== 死循环模拟进程持续运行 ====================
while(true)
{
cout << "My pid is : " << getpid() << endl;
sleep(1); // 每秒打印PID,等待信号触发
}
return 0;
}
process.cc11 代码运行现象
My pid is : 12345 // 第1秒
My pid is : 12345 // 第2秒
(按下Ctrl+C,触发2号信号)
get a signo : 2 // 进入handler函数
0000000000000000000000000000000 // 初始pending全0
(另一个终端执行:kill -1 12345 → 发送1号信号)
1000000000000000000000000000000 // 1号信号未决(第1位为1)
(另一个终端执行:kill -3 12345 → 发送3号信号)
1010000000000000000000000000000 // 1/3号信号未决(第1/3位为1)
(另一个终端执行:kill -4 12345 → 发送4号信号)
1011000000000000000000000000000 // 1/3/4号信号未决(第1/3/4位为1)
...(死循环打印该位图,直到进程终止)
一、struct sigaction 结构体的.sa_handler 字段
sa_handler 字段是 struct sigaction 结构体中用于指定信号处理方式的核心字段,功能与 signal 函数的第二个参数完全等价,支持三种取值:
SIG_DFL:执行信号的默认处理动作(如 2 号信号默认终止进程);SIG_IGN:忽略该信号,递达后不做任何处理;- 自定义函数指针:指向用户定义的信号处理函数(如 process.cc10 中的
handler),信号递达时执行该函数。
二、sigaction 函数详解
sigaction 函数是 Linux 中修改信号处理方式的标准、可移植接口,函数原型为:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 参数解析:
signum:要修改处理方式的信号编号(如 2 代表 SIGINT);act:指向新的信号处理配置结构体(struct sigaction)的指针,包含处理函数、屏蔽集等配置;oldact:指向旧的信号处理配置结构体的指针,用于保存修改前的配置(后续可恢复),若不需要保存可设为nullptr。
- 返回值:成功返回
0,失败返回-1。 - 功能:将指定信号的处理方式,替换为
act结构体中配置的规则,是比 signal 函数更强大、更可靠的信号控制接口。
三、sigaction 函数与 signal 函数的区别
| 对比维度 | sigaction 函数 | signal 函数 |
|---|---|---|
| 可移植性 | POSIX 标准接口,所有 UNIX/Linux 系统行为一致 | 不同系统实现可能有差异,可移植性弱 |
| 信号屏蔽能力 | 支持通过 sa_mask 配置 “处理期间临时屏蔽的额外信号” | 仅能自动屏蔽当前正在处理的信号,无法屏蔽其他信号 |
| 可靠性 | 信号处理函数执行期间,信号屏蔽规则稳定,不会丢失信号 | 部分旧系统实现中,信号处理后会恢复为默认动作,需重新注册 |
| 功能丰富度 | 支持 sa_flags 扩展标志(如 SA_RESTART 重启被中断的系统调用、SA_SIGINFO 获取信号详细信息) | 仅支持基本的信号处理函数注册,无扩展功能 |
四、信号处理期间的自动屏蔽机制
当某个信号递达并执行对应的处理函数时,内核会自动完成以下操作:
- 自动屏蔽当前信号:将进程的信号阻塞表(Block Mask)中,当前正在处理的信号对应的位,从 0 置 1;
- 执行信号处理函数:进程在屏蔽状态下,执行用户定义的处理函数或默认动作;
- 自动恢复屏蔽字:处理函数执行完毕后,内核将信号阻塞表中该信号的位,从 1 恢复为 0。
简单示例:process.cc10 中,进程收到 2 号信号后,在执行handler函数期间,信号阻塞表的 2 号位被自动置 1,此时即使再次发送 2 号信号,该信号也会被阻塞,无法递达,直到handler执行完毕,2 号位恢复为 0 后,新的 2 号信号才能被处理。该机制保证了同一信号不会嵌套处理,避免处理逻辑混乱。
五、struct sigaction 结构体的.sa_mask 字段
sa_mask 字段是 struct sigaction 结构体中用于配置 “信号处理期间临时屏蔽集” 的字段,类型为 sigset_t(位图结构),核心作用是:
- 在执行当前信号的处理函数期间,除了自动屏蔽当前信号外,额外屏蔽 sa_mask 中指定的其他信号;
- 处理函数执行完毕后,sa_mask 中的临时屏蔽会自动解除,恢复原来的信号阻塞表。
配置方式:
- 先用
sigemptyset清空 sa_mask(初始无任何信号被屏蔽); - 再用
sigaddset向 sa_mask 中添加需要临时屏蔽的信号(如 process.cc11 中添加 1、3、4 号信号)。
与自动屏蔽的区别:自动屏蔽仅针对 “当前正在处理的信号”,sa_mask 则针对 “用户指定的其他信号”,两者可叠加使用。
六、process.cc11 代码现象解释
结合代码逻辑,现象的底层原因如下:
- 初始运行阶段:进程未收到任何信号,每秒打印 PID,pending 位图全 0。
- 按下 Ctrl+C 触发 2 号信号:
- 2 号信号递达,内核自动屏蔽 2 号信号(Block 表 2 号位置 1);
- 进入自定义
handler函数,打印get a signo : 2; handler中进入死循环,每秒打印 pending 位图,此时未发送其他信号,pending 全 0。
- 另一个终端执行 kill -1 12345(发送 1 号信号):
- 1 号信号到达,但 sa_mask 中配置了临时屏蔽 1 号信号,因此 1 号信号无法递达,进入未决状态;
- 内核将 pending 表的 1 号位置 1;
PrintPending打印出第 1 位为 1 的序列。
- 执行 kill -3 12345(发送 3 号信号):
- 3 号信号同样被 sa_mask 屏蔽,进入未决状态;
- pending 表的 3 号位置 1;
- 打印序列的第 1、3 位为 1。
- 执行 kill -4 12345(发送 4 号信号):
- 4 号信号被 sa_mask 屏蔽,进入未决状态;
- pending 表的 4 号位置 1;
- 打印序列的第 1、3、4 位为 1。
- 死循环打印:
handler函数是死循环,2 号信号的处理函数永远不会执行完毕;- 因此 sa_mask 的临时屏蔽永远不会解除,1、3、4 号信号始终停留在未决状态;
- pending 位图保持第 1、3、4 位为 1,持续打印该序列,直到进程被强制终止。
Volatile 关键字
proces.cc12(信号 + 全局变量实现进程优雅退出)
#include <iostream> // 标准输入输出流(cout)
#include <string> // C++字符串(本例未使用,预留)
#include <string.h> // C字符串操作(本例未使用,预留)
#include <sys/types.h>// 系统类型定义(如pid_t)
#include <unistd.h> // Linux系统调用(getpid、sleep)
#include <signal.h> // 信号处理核心头文件(signal、SIGINT等)
using namespace std;
// ==================== 全局变量:跨函数传递信号触发状态 ====================
// flag初始为0:表示进程未收到退出信号,继续运行
// flag设为1:表示收到退出信号,进程需正常退出
// 注:全局变量是信号处理函数与主逻辑传递状态的常用方式(需注意原子性,本例简单场景无问题)
int flag = 0;
// ==================== 自定义信号处理函数 ====================
// 参数signo:触发的信号编号(本例中为2号SIGINT)
// 核心逻辑:打印信号编号 + 修改全局变量flag,通知主逻辑退出
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
flag = 1; // 修改全局变量,触发主循环退出
}
int main()
{
// ==================== 注册2号信号的处理函数 ====================
// 2号信号是SIGINT(Ctrl+C触发),默认行为是终止进程,此处改为执行handler函数
signal(2, handler);
// ==================== 主循环:根据flag控制进程运行 ====================
// 只要flag为0(未收到退出信号),就持续循环
while(!flag);
// ==================== 正常退出逻辑 ====================
// flag变为1后,退出循环,执行收尾逻辑(如释放资源、保存数据等)
cout << "process quit normal" << endl;
return 0;
}
process.cc13(编译器优化禁止符)
#include <iostream> // 标准输入输出流(cout)
#include <string> // C++字符串(本例未使用,预留)
#include <string.h> // C字符串操作(本例未使用,预留)
#include <sys/types.h>// 系统类型定义(如pid_t)
#include <unistd.h> // Linux系统调用(本例未直接使用,预留)
#include <signal.h> // 信号处理核心头文件(signal、SIGINT等)
using namespace std;
// ==================== 全局变量:跨函数传递信号触发状态 ====================
// volatile关键字:核心作用是告诉编译器“该变量是易变的,可能被异步事件修改”
// 禁止编译器对该变量做“寄存器缓存优化”,每次读取都必须从内存中获取最新值
// 本例中:flag可能被信号处理函数(异步执行)修改,必须加volatile,否则主循环可能看不到flag的变化
volatile int flag = 0;
// ==================== 自定义信号处理函数 ====================
// 参数signo:触发的信号编号(本例中为2号SIGINT)
// 核心逻辑:打印信号编号 + 修改volatile全局变量flag,通知主逻辑退出
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
flag = 1; // 修改volatile变量,主循环会从内存中读取到最新值
}
int main()
{
// ==================== 注册2号信号的处理函数 ====================
// 2号信号是SIGINT(Ctrl+C触发),默认行为是终止进程,此处改为执行handler函数
signal(2, handler);
// ==================== 主循环:空循环,仅依赖volatile flag控制退出 ====================
// 注意:这是一个“忙等待”循环(无sleep,CPU占用会很高),仅用于演示volatile的作用
// 核心依赖:volatile保证每次检查!flag时,都从内存读取flag的最新值
// 如果没有volatile,编译器可能优化成“死循环”(认为flag在主循环中未被修改,直接跳过读取)
while(!flag);
// ==================== 正常退出逻辑 ====================
// flag变为1后,退出循环,执行收尾逻辑
cout << "process quit normal" << endl;
return 0;
}
一、编译器优化选项与默认行为
1. 编译指令的默认优化级别Linux 下使用 g++ 编译代码时,若未显式指定优化级别,默认等价于添加-O0选项。例如:
g++ -g -o myprocess -std=c++11 process.cc
与
g++ -g -O0 -o myprocess -std=c++11 process.cc
完全等价。
2. 不同优化级别的核心差异
-O0:不进行任何优化,编译器严格按照代码的字面逻辑生成机器指令,变量的读写操作完全对应内存访问,是调试阶段的常用选项。-O1/-O3:开启不同级别的优化,编译器会通过寄存器缓存、指令重排、死代码消除等手段提升程序运行效率,但可能改变代码的 “字面执行逻辑”,是发布阶段的常用选项。
二、process.cc12 在高优化级别下无法退出的原因
1. 编译器的寄存器缓存优化逻辑process.cc12 的主循环为while(!flag);,且主循环中没有任何修改flag的代码。当使用-O1或-O3优化时,编译器会进行如下分析:
- 主循环内未修改
flag,因此编译器认为flag的值在循环期间 “不会改变”; - 为提升效率,编译器会将
flag的初始值从内存加载到 CPU 的寄存器中,后续循环检查!flag时,直接从寄存器读取值,不再访问内存。
2. 信号处理函数修改与内存不可见问题当进程收到 2 号信号时,信号处理函数会执行flag = 1;,修改的是内存中的flag变量。但此时:
- CPU 寄存器中缓存的
flag值仍为初始的0; - 主循环仍在从寄存器读取
flag,无法感知到内存中flag的变化; - 这种现象称为 “寄存器屏障” 或 “内存不可见”,导致主循环永远不会退出。
三、volatile 关键字的核心作用
1. volatile 的定义与功能volatile 是 C/C++ 中的关键字,核心作用是告诉编译器该变量是 “易变的”,可能被异步事件(如信号、中断、多线程)修改,禁止编译器对该变量进行过度优化。
2. volatile 对内存可见性的保证当变量被 volatile 修饰后,编译器会强制遵循以下规则:
- 每次读取该变量时,必须从内存中重新加载,禁止将其缓存到寄存器中;
- 每次修改该变量时,必须立即写回内存,禁止将修改暂存到寄存器中延迟写入。
3. process.cc13 中 volatile 的效果process.cc13 中flag被声明为volatile int flag = 0;,因此:
- 主循环每次检查
!flag时,都会从内存中读取最新值; - 信号处理函数执行
flag = 1;后,修改会立即写回内存; - 主循环能立刻感知到
flag的变化,从而正常退出循环。
SIGCHLD 信号
process.cc14(子进程退出通知父进程)
#include <iostream> // 标准输入输出流(cout)
#include <sys/types.h>// 系统类型定义(pid_t)
#include <unistd.h> // Linux系统调用(fork、getpid、getppid、sleep)
#include <signal.h> // 信号处理核心头文件(signal、SIGCHLD等)
#include <stdlib.h> // 标准库(exit函数)
using namespace std;
// ==================== 自定义信号处理函数 ====================
// 参数signo:触发的信号编号(本例中为17号SIGCHLD)
// 核心逻辑:打印当前进程pid和收到的信号编号,验证子进程退出时父进程会收到SIGCHLD
void handler(int signo)
{
cout << "My pid is : " << getpid() << "; I get a signo is : " << signo << endl;
}
int main()
{
// ==================== 注册17号信号的处理函数 ====================
// 17号信号是SIGCHLD:子进程状态变化(退出、暂停、恢复)时,内核会向父进程发送该信号
// 此处绑定自定义handler,替代SIGCHLD的默认行为(默认忽略)
signal(17, handler);
// ==================== 创建子进程 ====================
// fork函数:创建一个新的子进程,返回值分三种情况
// - 父进程中:返回子进程的pid(>0)
// - 子进程中:返回0
// - 创建失败:返回-1
pid_t id = fork();
// ==================== 子进程执行逻辑 ====================
if(id == 0)
{
// 子进程循环:打印自身pid和父进程pid,休眠5秒后退出
while(true)
{
cout << "I am child, My pid is : " << getpid() << "; My father pid is : " << getppid() << endl;
sleep(5); // 休眠5秒,模拟子进程运行一段时间
break; // 5秒后跳出循环
}
// 子进程退出:exit(0)表示正常退出,此时内核会向父进程发送17号SIGCHLD信号
exit(0);
}
// ==================== 父进程执行逻辑 ====================
// 父进程循环:每秒打印自身pid,持续运行,等待接收SIGCHLD信号
while(true)
{
cout << "I am father, My pid is : " << getpid() << endl;
sleep(1); // 每秒打印一次,降低CPU占用
}
return 0;
}
process.cc15(基于信号捕捉对单个子进程进行回收)
#include <iostream> // 标准输入输出流(cout)
#include <sys/types.h>// 系统类型定义(pid_t)
#include <sys/wait.h> // 进程等待核心头文件(waitpid函数)
#include <unistd.h> // Linux系统调用(fork、getpid、getppid、sleep)
#include <signal.h> // 信号处理核心头文件(signal、SIGCHLD等)
#include <stdlib.h> // 标准库(exit函数)
using namespace std;
// ==================== 自定义信号处理函数 ====================
// 参数signo:触发的信号编号(本例中为17号SIGCHLD)
// 核心逻辑:调用waitpid回收任意已退出的子进程资源,打印相关信息
void handler(int signo)
{
// waitpid函数:等待并回收指定子进程的资源
// 参数1:-1 → 等待任意一个已退出的子进程
// 参数2:nullptr → 不获取子进程的退出状态(不需要时设为nullptr)
// 参数3:0 → 阻塞等待,直到有子进程退出(SIGCHLD触发时已有子进程退出,不会阻塞太久)
// 返回值:成功回收的子进程pid
pid_t rid = waitpid(-1, nullptr, 0);
// 打印当前进程pid、收到的信号编号、成功回收的子进程pid
cout << "My pid is : " << getpid() << "; I get a signo is : " << signo << "; My child quit : " << rid << endl;
}
int main()
{
// ==================== 注册17号信号的处理函数 ====================
// 17号信号是SIGCHLD:子进程状态变化(退出、暂停、恢复)时,内核会向父进程发送该信号
// 此处绑定自定义handler,替代SIGCHLD的默认行为(默认忽略)
signal(17, handler);
// ==================== 创建子进程 ====================
// fork函数:创建一个新的子进程,返回值分三种情况
// - 父进程中:返回子进程的pid(>0)
// - 子进程中:返回0
// - 创建失败:返回-1
pid_t id = fork();
// ==================== 子进程执行逻辑 ====================
if(id == 0)
{
// 子进程循环:打印自身pid和父进程pid,休眠5秒后退出
while(true)
{
cout << "I am child, My pid is : " << getpid() << "; My father pid is : " << getppid() << endl;
sleep(5); // 休眠5秒,模拟子进程运行一段时间
break; // 5秒后跳出循环
}
// 子进程退出前的提示
cout << "child quit" << endl;
// 子进程退出:exit(0)表示正常退出,此时内核会向父进程发送17号SIGCHLD信号
exit(0);
}
// ==================== 父进程执行逻辑 ====================
// 父进程循环:每秒打印自身pid,持续运行,等待接收SIGCHLD信号
while(true)
{
cout << "I am father, My pid is : " << getpid() << endl;
sleep(1); // 每秒打印一次,降低CPU占用
}
return 0;
}
process.cc16(基于信号捕捉对多个子进程进行回收)
#include <iostream> // 标准输入输出流(cout)
#include <sys/types.h>// 系统类型定义(pid_t)
#include <sys/wait.h> // 进程等待核心头文件(waitpid函数)
#include <unistd.h> // Linux系统调用(fork、getpid、getppid、sleep)
#include <signal.h> // 信号处理核心头文件(signal、SIGCHLD等)
#include <stdlib.h> // 标准库(exit函数、rand函数)
#include <time.h> // 时间库(time函数,用于初始化随机数种子)
using namespace std;
// ==================== 自定义信号处理函数 ====================
// 参数signo:触发的信号编号(本例中为17号SIGCHLD)
// 核心逻辑:**循环调用非阻塞waitpid**,回收所有已退出的子进程资源,避免信号不排队导致的僵尸进程
void handler(int signo)
{
pid_t rid;
// waitpid循环:WNOHANG表示非阻塞,没有子进程退出时返回0,有则返回子进程pid
// 循环条件:只要成功回收子进程(rid>0),就继续回收,直到没有可回收的子进程
while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
{
cout << "My pid is : " << getpid() << "; I get a signo is : " << signo << "; My child quit : " << rid << endl;
}
}
int main()
{
// 初始化随机数种子:用当前时间作为种子,保证每次运行的随机休眠时间不同
srand((unsigned int)time(nullptr));
// 注册17号信号的处理函数:替代SIGCHLD的默认忽略行为
signal(17, handler);
// 循环创建10个子进程:每个子进程的创建间隔为3-7秒(rand()%5+3)
for (int i = 1; i <= 10; i++)
{
pid_t id = fork();
if (id == 0)
{
// 子进程执行逻辑:打印自身pid和父进程pid,休眠10秒后退出
while (true)
{
cout << "I am child, My pid is : " << getpid() << "; My father pid is : " << getppid() << endl;
sleep(10);
break;
}
cout << "child quit" << endl;
exit(0);
}
// 父进程创建子进程后的随机休眠:3-7秒,模拟子进程分批创建的场景
sleep(rand() % 5 + 3);
}
// 父进程主循环:每秒打印自身pid,持续运行,等待接收SIGCHLD信号
while (true)
{
cout << "I am father, My pid is : " << getpid() << endl;
sleep(1);
}
return 0;
}
一、process.cc14:子进程退出时主动发送 SIGCHLD 信号
1. SIGCHLD 信号的触发规则SIGCHLD(17 号)是 Linux 系统中专门用于子进程状态通知的信号,触发场景包括:
- 子进程正常退出(如调用
exit、_exit或从 main 返回); - 子进程被信号终止;
- 子进程被信号暂停;
- 暂停的子进程被信号恢复。
2. process.cc14 的核心执行流程
- 父进程通过
signal(17, handler)注册 SIGCHLD 的自定义处理函数,替代默认的忽略行为; - 父进程调用
fork创建子进程,子进程执行循环打印逻辑,休眠 5 秒后调用exit(0)正常退出; - 子进程退出时,内核自动向父进程发送 17 号 SIGCHLD 信号;
- 父进程收到信号后,暂停主循环,执行
handler函数,打印自身 PID 和信号编号,验证信号的触发。
二、process.cc15:基于信号捕捉对单个子进程进行回收
1. 单个子进程回收的核心逻辑
- 父进程注册 SIGCHLD 的自定义处理函数,确保子进程退出时能收到通知;
- 子进程退出触发 SIGCHLD 信号后,父进程在
handler中调用waitpid(-1, nullptr, 0); waitpid的参数说明:- 参数 1
-1:等待任意一个已退出的子进程; - 参数 2
nullptr:不获取子进程的退出状态; - 参数 3
0:阻塞等待,直到有子进程退出(SIGCHLD 触发时已有子进程退出,因此不会长时间阻塞);
- 参数 1
waitpid成功回收子进程资源后,返回子进程的 PID,父进程打印回收信息,避免子进程成为僵尸进程。
三、process.cc16:基于信号捕捉对多个子进程进行回收
1. 多个子进程回收的核心问题1-31 号普通信号不支持排队,若多个子进程同时或短时间内退出,SIGCHLD 信号可能只被触发一次,导致部分子进程无法被回收,成为僵尸进程。
2. process.cc16 的解决方案:循环非阻塞 waitpid
- 父进程注册 SIGCHLD 的自定义处理函数;
handler中使用循环调用非阻塞 waitpid的方式:waitpid参数 3 设为WNOHANG:非阻塞模式,没有可回收的子进程时立即返回 0,不阻塞;- 循环条件为
(rid = waitpid(-1, nullptr, WNOHANG)) > 0:只要成功回收一个子进程(返回值 > 0),就继续回收,直到没有可回收的子进程(返回值为 0);
- 该方式确保即使 SIGCHLD 只触发一次,也能通过循环回收所有已退出的子进程,避免僵尸进程。
3. process.cc16 的执行场景
- 父进程循环创建 10 个子进程,每个子进程的创建间隔为 3-7 秒(随机);
- 每个子进程运行 10 秒后退出;
- 若多个子进程同时退出,SIGCHLD 触发一次,
handler中的循环会一次性回收所有已退出的子进程,打印对应的回收信息。
四、signal (17, SIG_IGN) 的特殊作用与 SIGCHLD 的默认动作
1. signal (17, SIG_IGN) 的代码含义将 17 号 SIGCHLD 信号的处理方式设置为SIG_IGN(忽略),此时内核会自动完成子进程的回收工作:
- 只要子进程退出,内核会直接释放子进程的 PCB 等资源,无需父进程调用
wait/waitpid; - 子进程不会进入僵尸状态,是一种简单的自动回收方式;
- 该方式的局限性:父进程无法获取子进程的退出状态(如退出码、终止原因)。
2. SIGCHLD 的默认动作(SIG_DFL)与 SIG_IGN 的区别
- SIGCHLD 的默认处理动作是
SIG_DFL(缺省 / 默认),其行为是忽略该信号,但与显式设置SIG_IGN有本质区别:- 默认
SIG_DFL:子进程退出后,内核不会自动回收,子进程会成为僵尸进程,父进程必须调用wait/waitpid手动回收; - 显式
SIG_IGN:子进程退出后,内核会自动回收所有资源,子进程不会成为僵尸进程,无需父进程手动回收。
- 默认
更多推荐




所有评论(0)