一、 信号在底层是如何保存的?
在进程的 PCB(task_struct)中,有三个极其重要的结构:
Block(阻塞)表:位图,如果某位置 1,代表该信号被屏蔽。
Pending(未决)表:位图,记录信号已经产生,但因为被阻塞等原因还未被处理,处于“排队”状态。
Handler(处理动作)表:函数指针数组,存放每个信号对应的处理函数地址。
阻塞与忽略的区别:
-
阻塞:信号根本递达不了(不会去处理)。
-
忽略:信号已经递达,但处理方式是不做任何事。
二、 系统调用接口实战
-
sigprocmask():用于读取或修改当前进程的阻塞表(Block)。
-
sigpending():用于获取当前进程的未决表(Pending)。

在实战中,我们利用 sigprocmask 屏蔽了 Ctrl+C(SIGINT)。此时按下 Ctrl+C,信号不会立刻生效,而是被压在未决表(Pending)里排队。等待我们解除阻塞的那一瞬间,信号才会立刻“递达”并执行处理函数。
三、 灵魂考点:volatile 关键字
(插入实战 3 的代码和现象)
现象:写了一个 while(!flag) 的循环,在信号处理函数中把 flag 改为 1。但在加了 -O2 优化编译后,按下 Ctrl+C 打印了修改信息,但主循环依然死循环。
原因:由于 CPU 极快而内存相对较慢,编译器优化(如 -O2)会自动把经常使用的全局变量缓存到 CPU 寄存器中。主函数一直在寄存器里读 flag,而信号处理函数改的是真实内存中的 flag,两者出现了数据二义性。
解决:在变量前加上 volatile 关键字。
volatile 的作用是:强制 CPU 每次访问该变量时,都必须从真实的内存中读取,禁止编译器将其优化到寄存器中。这是编写信号处理、多线程程序时的必备常识。
四、 操作系统运行的真相:用户态与内核态
操作系统为了保证安全,将 CPU 的指令权限分为 Ring 3(用户态)和 Ring 0(内核态)。
-
用户代码跑在用户态,不能直接访问硬件。
-
当发生系统调用(软中断 int 0x80/syscall)、硬件中断或异常(除零、缺页)时,CPU 会提权切换到内核态,执行内核代码后,再回到用户态。
所谓的“信号递达”,正是发生在从内核态返回用户态的那一刻!
自我实战操作
实战 1:体验“信号屏蔽”
cd ~/linux_study/signal_test
vim sig_mask.c
(输入以下代码)
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo) {
printf("\\n[处理] 终于递达了,处理信号: %d\\n", signo);
}
int main() {
signal(SIGINT, handler); // 注册 2号信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT); // 把 2号信号加入屏蔽集
// 设置屏蔽字(阻塞信号)
sigprocmask(SIG_BLOCK, &block_set, &old_set);
printf("我已经屏蔽了 Ctrl+C(2号信号),你现在按没反应了!\\n");
int count = 5;
while(count > 0) {
printf("等 1 秒… 还有 %d 秒解除屏蔽\\n", count);
sleep(1);
count–;
}
// 解除屏蔽
sigprocmask(SIG_SETMASK, &old_set, NULL);
printf("解除屏蔽了!刚才按的 Ctrl+C 现在会立刻生效!\\n");
return 0;
}
编译并运行:
gcc sig_mask.c -o sig_mask
./sig_mask
👉 现象:程序运行后,你按 Ctrl+C,它不会立刻打印处理结果,也不会退出,而是继续倒计时。倒计时结束后,你刚才按的 Ctrl+C 会立刻“补发”执行,打印出 [处理] 终于递达了…。
实战 2:亲眼看看“未决表(Pending)”
修改上面的代码,在倒计时循环里每次都调用 sigpending 打印当前的未决状态:
sigset_t pending_set;
while(count > 0) {
sigpending(&pending_set); // 获取未决表
if (sigismember(&pending_set, SIGINT)) { // 看 2号 是否在未决表里
printf("【发现 2号信号 正在排队(未决)】\\n");
}
sleep(1);
count–;
}
👉 现象:一旦你按下 Ctrl+C,屏幕上会立刻开始疯狂打印“发现 2号信号 正在排队(未决)”。这就是 Pending 位图被置 1 的实证!
实战 3:复现并解决 volatile 死循环(大厂必考)
vim sig_volatile.c
(输入以下代码)
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int flag = 0; // 这里我们先不加 volatile
void handler(int signo) {
printf("\\n信号处理函数:把 flag 改成 1\\n");
flag = 1;
}
int main() {
signal(SIGINT, handler);
while(!flag) {
// 死循环等待 flag 变成 1
}
printf("主函数:flag 变成了 1,我退出了!\\n");
return 0;
}
编译并运行(注意:一定要加 -O2 优化选项,才能复现问题!):
# 加上 -O2 优化
gcc sig_volatile.c -o sig_volatile -O2
./sig_volatile

👉 现象:你按 Ctrl+C,它打印了“把 flag 改成 1”,但是主函数的死循环依然没有退出!
👉 原因:编译器加上 -O2 后,发现 while(!flag) 里 flag 没有修改,觉得这是废代码,直接把它优化成 while(1) 了(把 flag 缓存到了寄存器里,根本不去内存读了)。
👉 解决:把变量定义改成 volatile int flag = 0;,重新编译运行,立刻就能正常退出了。volatile 告诉编译器:“别自作聪明,每次都去内存里读!”。




