一、核心必掌握知识点(高频考点 + 实操重点)
1. 信号递达与阻塞的深度理解(含内核态存储逻辑)
关键概念区分
概念 | 定义 | 核心差异 |
---|---|---|
信号未决(Pending) | 信号产生后到递达前的状态,进程已感知但未处理 | 未决是 “待处理” 状态,阻塞是 “禁止处理” 开关 |
信号阻塞(Block) | 进程主动设置 “屏蔽”,被阻塞信号即使产生也不会递达 | 阻塞不影响信号产生(仍会标记未决),仅阻止递达 |
信号递达(Delivery) | 实际执行信号处理动作(默认 / 忽略 / 自定义) | 递达是最终处理环节,需先解除阻塞(若被阻塞) |
内核中存储逻辑(必须理解)
进程 PCB(task_struct
)中通过 3 个核心结构管理信号:
blocked
(信号屏蔽字):sigset_t
类型,位图结构,某 bit 为 1 表示对应信号被阻塞。pending
(未决信号集):sigset_t
类型,位图结构,某 bit 为 1 表示对应信号已产生但未递达。sighand
(信号处理动作):存储每个信号的处理方式(SIG_DFL
/SIG_IGN
/ 自定义函数指针)。
示例场景:若进程阻塞SIGINT
(2 号信号),此时按下Ctrl+C
:
- 内核标记
pending
中SIGINT
的 bit 为 1(未决); - 因
blocked
中SIGINT
的 bit 为 1(阻塞),信号不递达; - 当进程解除
SIGINT
阻塞后,内核检测到未决信号,立即执行递达(如默认终止)。
实操练习:验证阻塞与未决
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
// 打印未决信号集
void printPending() {
sigset_t pending;
sigpending(&pending); // 获取当前进程未决信号集
cout << "Pending signals (31~1): ";
for (int i = 31; i >= 1; i--) {
if (sigismember(&pending, i)) cout << "1";
else cout << "0";
}
cout << endl;
}
int main() {
sigset_t blockSet, oldSet;
sigemptyset(&blockSet); // 初始化空信号集
sigaddset(&blockSet, SIGINT); // 将SIGINT(2号)加入阻塞集
// 设置阻塞信号集,备份原阻塞集到oldSet
sigprocmask(SIG_BLOCK, &blockSet, &oldSet);
cout << "Block SIGINT, press Ctrl+C (5s later unblock)..." << endl;
for (int i = 5; i > 0; i--) {
printPending();
sleep(1);
}
// 解除阻塞(恢复原阻塞集)
sigprocmask(SIG_SETMASK, &oldSet, nullptr);
cout << "Unblock SIGINT, now Ctrl+C will terminate process" << endl;
while (1) sleep(1);
return 0;
}
运行结果:按下Ctrl+C
后,未决信号集第 2 位(SIGINT
)会显示 “1”;5 秒后解除阻塞,进程立即被SIGINT
终止。
2. 信号捕捉的完整流程(含内核态 / 用户态切换)
核心流程(必须能复述)
- 用户态执行主流程:进程在用户态运行
main
函数,此时发生中断 / 异常 / 系统调用,切换到内核态。 - 内核态检测信号:内核处理完中断 / 异常后,准备返回用户态前,检查进程的
pending
和blocked
:- 若存在未被阻塞的信号,且处理方式为 “自定义函数”,则不返回主流程,而是切换到用户态执行自定义处理函数。
- 用户态执行处理函数:处理函数执行完毕后,自动调用
sigreturn
系统调用,再次切换到内核态。 - 内核态恢复主流程:内核确认无新信号需递达,恢复主流程的上下文,切换回用户态继续执行
main
函数。
关键函数:sigaction
(比signal
更稳定,推荐使用)
signal
函数存在兼容性问题,sigaction
是 POSIX 标准接口,支持更精细的信号控制:
#include <signal.h>
// 函数原型
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
struct sigaction
核心字段:sa_handler
:处理函数指针(SIG_DFL
/SIG_IGN
/ 自定义函数)。sa_mask
:执行处理函数期间,额外阻塞的信号集(防止处理函数被其他信号打断)。sa_flags
:控制信号行为(如SA_RESTART
使被信号打断的系统调用自动重启)。
实操练习:用sigaction
捕捉SIGINT
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sigintHandler(int signo) {
cout << "Catch SIGINT (signo=" << signo << "), wait 2s..." << endl;
sleep(2); // 执行处理期间,若再次按Ctrl+C,因sa_mask会阻塞SIGINT,需等待处理完毕
}
int main() {
struct sigaction act, oldAct;
// 初始化act
act.sa_handler = sigintHandler; // 设置自定义处理函数
sigemptyset(&act.sa_mask); // 初始化额外阻塞集
sigaddset(&act.sa_mask, SIGINT); // 执行处理函数时,额外阻塞SIGINT
act.sa_flags = 0; // 默认行为
// 设置SIGINT的处理动作,备份原动作到oldAct
sigaction(SIGINT, &act, &oldAct);
cout << "Wait for SIGINT (press Ctrl+C), enter 'q' to quit" << endl;
while (cin.get() != 'q');
// 恢复SIGINT原处理动作
sigaction(SIGINT, &oldAct, nullptr);
return 0;
}
运行结果:第一次按Ctrl+C
会执行处理函数(2 秒内再次按Ctrl+C
无反应),处理完毕后恢复响应;输入q
退出程序。
3. 可重入函数与竞态条件(避坑重点)
可重入函数定义
- 可重入:多个控制流程(如主流程 + 信号处理函数)同时调用该函数,不会导致数据错乱(仅访问局部变量 / 参数,无全局 / 静态变量操作)。
- 不可重入:调用后可能导致数据错乱,常见场景:
- 操作全局 / 静态变量(如全局链表插入);
- 调用
malloc
/free
(堆管理用全局链表); - 调用标准 I/O 函数(如
printf
,内部用全局缓冲区)。
竞态条件示例(不可重入函数问题)
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
struct Node { int val; Node* next; } node1, node2, *head = nullptr;
// 不可重入函数:操作全局链表head
void insert(Node* p) {
p->next = head; // 步骤1:保存原head
// 若此时发生信号,进入处理函数调用insert,会修改head
sleep(1); // 模拟信号打断
head = p; // 步骤2:更新head为p
}
void sigHandler(int signo) {
insert(&node2); // 信号处理函数调用insert
cout << "Signal handler insert node2" << endl;
}
int main() {
node1.val = 1; node2.val = 2;
signal(SIGINT, sigHandler); // 注册SIGINT处理函数
insert(&node1); // 主流程调用insert
cout << "Main insert node1" << endl;
// 遍历链表(预期node1->node2,实际可能只有node2)
Node* cur = head;
while (cur) {
cout << cur->val << " ";
cur = cur->next;
}
return 0;
}
运行结果:主流程执行insert(&node1)
到sleep(1)
时,按下Ctrl+C
,信号处理函数插入node2
并修改head
;主流程恢复后继续执行head = &node1
,最终链表仅node1
(数据错乱)。
避坑方案
- 优先使用可重入函数(如自己实现无全局变量的工具函数);
- 若必须操作全局数据,在信号处理函数执行期间,通过
sigprocmask
阻塞相关信号(避免打断)。
4. SIGCHLD
信号与僵尸进程处理(实战必备)
核心作用
子进程终止时,会向父进程发送SIGCHLD
信号(默认处理动作是忽略),父进程可通过捕捉该信号,调用waitpid
清理僵尸进程(避免轮询)。
关键特性
- 子进程终止后若父进程未处理,会变为僵尸进程(
Z
状态); - 父进程若用
sigaction
将SIGCHLD
设为SIG_IGN
,子进程终止后会自动清理(不产生僵尸进程,Linux 特有)。
实操练习:捕捉SIGCHLD
清理僵尸进程
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
// SIGCHLD处理函数:清理所有终止的子进程
void sigchldHandler(int signo) {
pid_t id;
// WNOHANG:非阻塞,有子进程终止则返回其PID,否则返回0
while ((id = waitpid(-1, nullptr, WNOHANG)) > 0) {
cout << "Clean zombie child, PID=" << id << endl;
}
}
int main() {
// 注册SIGCHLD处理函数
struct sigaction act;
act.sa_handler = sigchldHandler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, nullptr);
// 创建3个子进程
for (int i = 0; i < 3; i++) {
pid_t cpid = fork();
if (cpid == 0) {
cout << "Child PID=" << getpid() << ", exit after 2s" << endl;
sleep(2);
exit(0); // 子进程终止,发送SIGCHLD
}
}
// 父进程持续运行,等待子进程终止
while (1) sleep(1);
return 0;
}
运行结果:3 个子进程 2 秒后陆续终止,父进程通过SIGCHLD
处理函数自动清理,无僵尸进程残留(可通过ps aux | grep 程序名
验证)。
二、重要理解类知识点(原理性内容)
1. 内核态与用户态的切换(信号处理的底层支撑)
核心区别
维度 | 用户态(Ring 3) | 内核态(Ring 0) |
---|---|---|
权限 | 仅能访问用户空间(0~3GB),无硬件操作权限 | 可访问内核空间(3~4GB),有所有硬件操作权限 |
执行代码 | 应用程序代码(如main 函数) |
内核代码(如系统调用、中断处理) |
切换触发 | 系统调用(如fork )、异常(如除零)、中断(如键盘) |
处理完毕后自动切换回用户态 |
与信号处理的关联
信号处理函数在用户态执行,但信号的检测、未决 / 阻塞标记的维护在内核态完成:
- 当内核检测到可递达信号时,会 “跳转” 到用户态执行处理函数;
- 处理函数返回后,通过
sigreturn
系统调用切回内核态,再恢复主流程。
2. volatile
关键字(解决编译器优化导致的 bug)
作用
告知编译器:被修饰的变量不允许优化,每次访问必须从内存读取(而非寄存器缓存),保证内存可见性。
典型场景
信号处理函数修改全局变量,主流程循环判断该变量(无volatile
会导致优化 bug):
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
// 必须加volatile,否则编译器可能将flag缓存到寄存器
volatile int flag = 0;
void sigHandler(int signo) {
flag = 1;
cout << "Signal set flag=1" << endl;
}
int main() {
signal(SIGINT, sigHandler);
cout << "Wait for SIGINT (press Ctrl+C)" << endl;
// 若flag无volatile,编译器可能优化为while(1)(认为flag不会变)
while (!flag);
cout << "Main exit (flag=1)" << endl;
return 0;
}
注意:编译时加-O2
(优化选项),若不加volatile
,程序按下Ctrl+C
后仍会卡在while
循环;加volatile
则正常退出。
三、了解类知识点(扩展认知)
1. 实时信号与常规信号(编号差异)
- 常规信号:编号 1~31,不支持排队(同一信号多次产生,递达时仅处理 1 次);
- 实时信号:编号 34~64,支持排队(多次产生会依次递达),用于需要精确处理的场景(如工业控制),本章暂不涉及。
2. Core Dump(核心转储)
- 作用:进程异常终止时(如
SIGSEGV
段错误),将用户空间内存数据保存到core
文件,用于事后调试(gdb ./程序名 core.进程号
)。 - 开启方法:默认关闭,通过
ulimit -c 1024
(允许最大 1024KB 的 core 文件)开启,测试代码:#include <stdio.h> int main() { int* p = nullptr; *p = 100; // 非法内存访问,产生SIGSEGV,触发Core Dump return 0; }
编译运行后会生成
core.xxx
文件,用gdb ./a.out core.xxx
可定位到错误行。
3. 操作系统的中断驱动模型
- 操作系统通过 “中断”(硬件中断如键盘、软中断如系统调用)驱动运行,核心是 “中断向量表(IDT)”:
- 每个中断对应一个处理例程(如
0x20
是时钟中断,0x80
是系统调用); - 时钟中断定期触发,推动进程调度(如时间片轮转),是操作系统的 “心脏”。
- 每个中断对应一个处理例程(如