欢迎光临
我们一直在努力

Linux 学习笔记(三十):深入进程信号——屏蔽、未决与 volatile 关键字

一、 信号在底层是如何保存的?

在进程的 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 告诉编译器:“别自作聪明,每次都去内存里读!”。

    赞(0)
    未经允许不得转载:171主机测评 » Linux 学习笔记(三十):深入进程信号——屏蔽、未决与 volatile 关键字
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址