Linux——进程信号(3)(信号保存与信号捕捉)

发布于:2025-04-03 ⋅ 阅读:(18) ⋅ 点赞:(0)

信号保存

信号相关概念详解

1.核心概念

(1) 信号递达(Delivery)
定义:信号递达是指信号被进程实际处理的过程。

触发时机:当进程从内核态返回用户态时,内核检查并处理未决信号。

处理方式

默认动作(SIG_DFL):系统预定义行为(如终止、暂停、忽略等)。

忽略(SIG_IGN):直接丢弃信号,不执行任何操作。

自定义处理函数:通过signal()或sigaction()注册的用户函数。

(2) 信号未决(Pending)
定义:信号从生成到递达之间的中间状态。

原因

  • 信号被进程阻塞(Blocked)(通过sigprocmask()设置)。

  • 进程正在处理其他同类型信号(非实时信号可能合并)。

未决信号集:内核为每个进程维护一个集合,记录所有未决信号(可通过sigpending()读取)。

(3) 信号阻塞(Block)
定义:阻止信号递达(即使信号已生成),使其保持未决状态。

关键点

  • 阻塞是未决的前提,未被阻塞的信号会立即递达(除非进程正处理其他信号)。

  • 阻塞解除后,未决信号会递达(除非被忽略)。

(4) 信号忽略(Ignore)
1.定义:信号递达后,进程选择不执行任何操作(通过SIG_IGN或空处理函数)。

与阻塞的区别

  • 阻塞:信号未递达,停留在未决状态。

  • 忽略:信号已递达,但处理动作为“丢弃”。

2. 三者的关系

信号生成
   │
   ├── 未被阻塞 ──── 立即递达(执行默认/自定义动作或忽略)
   │
   └── 被阻塞 ────── 加入未决信号集
                        │
                        └── 阻塞解除后 ──── 递达
                        

3.关键区别总结

概念 阶段 效果 系统调用
阻塞(Block) 信号递达前 信号保持未决,不处理 sigprocmask()
忽略(Ignore) 信号递达后 丢弃信号,不执行任何动作 signal(sig, SIG_IGN)
未决(Pending) 信号生成到递达间 信号等待被处理 sigpending()

4. 示例代码说明

复制
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void handler(int sig) {
    printf("递达信号 %d\n", sig);
}

int main() {
    // 注册SIGINT处理函数
    signal(SIGINT, handler);

    // 阻塞SIGINT
    sigset_t block_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);
    sigprocmask(SIG_BLOCK, &block_set, NULL);

    printf("按下 Ctrl+C,SIGINT 将被阻塞...\n");
    sleep(3);  // 此时发送SIGINT,信号处于未决状态

    // 检查未决信号
    sigset_t pending_set;
    sigpending(&pending_set);
    if (sigismember(&pending_set, SIGINT)) {
        printf("SIGINT 处于未决状态!\n");
    }

    // 解除阻塞,信号递达
    sigprocmask(SIG_UNBLOCK, &block_set, NULL);
    printf("SIGINT 已解除阻塞。\n");
    return 0;
}

输出结果:

在sleep(3)期间按下Ctrl+C,SIGINT被阻塞,处于未决状态。

sigpending()检测到未决的SIGINT。

解除阻塞后,handler函数执行,信号递达。

(5)注意事项
信号丢失:非实时信号(如SIGINT)在未决期间多次生成,可能仅保留一次。

实时信号(如SIGRTMIN)支持排队,不会丢失。

原子性:信号处理函数应使用可重入函数,避免竞态条件。

(6)常见问题
Q:阻塞和忽略有何本质区别?
A:阻塞阻止信号递达(未决状态),忽略是递达后的处理方式之一。

Q:如何永久阻塞一个信号?
A:通过sigprocmask()设置阻塞掩码后,除非显式解除,否则信号一直未决。

Q:sigpending()的作用?
A:获取当前进程所有未决信号,用于调试或条件处理。

信号集(sigset_t)及操作函数详解

(1) 信号集(sigset_t)的基本概念
用途:表示一组信号,每个信号对应一个比特位(1有效,0无效)。

未决信号集(Pending Set):记录信号是否处于未决状态(已产生但未递达)。

阻塞信号集(Block Set / Signal Mask):记录信号是否被阻塞(屏蔽)。

特性:

仅记录信号是否有效,不记录次数(多次触发同一信号仍只显示一次)。

内部实现由系统决定,用户只能通过标准函数操作,不可直接访问比特位。

在这里插入图片描述

(2)信号集操作函数

以下函数均来自 <signal.h>,成功返回 0,失败返回 -1(sigismember 返回布尔值)

函数 功能
sigemptyset(sigset_t *set) 初始化信号集为空(所有比特置 0)。
sigfillset(sigset_t *set) 初始化信号集包含所有信号(所有比特置 1)。
sigaddset(sigset_t *set, int signo) 将指定信号(如 SIGINT)添加到信号集。
sigdelset(sigset_t *set, int signo) 从信号集中删除指定信号。
sigismember(const sigset_t *set, int signo) 检查信号是否在集合中(1=存在,0=不存在)。

注意:

使用 sigset_t 前必须初始化(sigemptyset 或 sigfillset),否则行为未定义。

不可直接打印 sigset_t 的内容(如 printf),需通过 sigismember 逐信号检查

(3)信号屏蔽字操作(sigprocmask)

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

功能:读取或更改进程的阻塞信号集(Signal Mask)。

参数

how:修改方式,可选值

  • SIG_BLOCK:将 set 中的信号添加到当前阻塞集(mask = mask | set)。

  • SIG_UNBLOCK:从当前阻塞集中移除 set 中的信号(mask = mask & ~set)。

  • SIG_SETMASK:直接设置阻塞集为 set(mask = set)。

set:新信号集(若为 NULL,则不修改)。

oset:保存旧的阻塞集(若为 NULL,则不保存)。

返回值:成功返回 0,失败返回 -1。

关键行为
若解除对未决信号的阻塞,sigprocmask 返回前至少会递达一个信号。

(4)获取未决信号集(sigpending)

int sigpending(sigset_t *set);

功能:读取当前进程的未决信号集,通过 set 传出。

返回值:成功返回 0,失败返回 -1。

(5) 实验代码解析
以下代码演示了信号屏蔽与未决信号的检测:

#include <iostream>
#include <signal.h>
#include <unistd.h>

void PrintPending(sigset_t &pending) {
    std::cout << "Pending signals: ";
    for (int signo = 31; signo >= 1; signo--) {
        std::cout << (sigismember(&pending, signo) ? "1" : "0");
    }
    std::cout << std::endl;
}

void handler(int signo) {
    std::cout << "Signal " << signo << " delivered!\n";
}

int main() {
    signal(SIGINT, handler);  // 捕捉 SIGINT (Ctrl+C)

    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);  // 屏蔽 SIGINT
    sigprocmask(SIG_BLOCK, &block_set, &old_set);  // 设置阻塞

    int cnt = 5;
    while (cnt--) {
        sigset_t pending;
        sigpending(&pending);  // 获取未决信号
        PrintPending(pending);
        sleep(1);
    }

    // 解除屏蔽
    sigprocmask(SIG_SETMASK, &old_set, NULL);
    return 0;
}

运行结果

按下 Ctrl+C 后,SIGINT 被阻塞,未决信号集中对应位变为 1。

解除阻塞后,SIGINT 递达,触发 handler 函数。

(6) 关键区别与注意事项
阻塞 vs 忽略

  • 阻塞(Block):信号被暂时屏蔽,处于未决状态,解除阻塞后会递达。

  • 忽略(Ignore):信号直接被丢弃,不会递达(通过 signal(signo, SIG_IGN) 设置)。

信号集初始化:未初始化的 sigset_t 可能导致未定义行为,必须调用 sigemptyset 或 sigfillset。

多线程环境:sigprocmask 仅影响当前线程,应使用 pthread_sigmask。

(7)总结图示

+-------------------+      sigprocmask()       +-------------------+
|   Block Set       | <---------------------- | 用户设置的信号集   |
|  (Signal Mask)    |                         | (sigset_t)        |
+-------------------+                         +-------------------+
        |                                             ^
        | 信号产生但被阻塞                            | sigpending()
        v                                             |
+-------------------+                         +-------------------+
|   Pending Set     | ----------------------> | 未决信号集        |
|  (待处理信号)      |                         | (sigset_t)        |
+-------------------+                         +-------------------+
        |
        | 解除阻塞后
        v
+-------------------+
|   信号递达         |
|   (执行处理函数)    |
+-------------------+

信号捕捉

信号捕捉的详细流程解析

信号捕捉的核心概念

当信号的处理动作用户自定义函数(而非 SIG_IGN 或 SIG_DFL),内核会在信号递达时调用该函数,这一过程称为 信号捕捉。

由于信号处理函数运行在用户态,其执行流程涉及 用户态-内核态切换。

信号捕捉的完整流程(以 SIGQUIT 为例)

1.注册信号处理函数
用户程序通过 signal() 或 sigaction() 注册信号处理函数(如 sighandler):

signal(SIGQUIT, sighandler);  // 自定义SIGQUIT处理函数

2.主程序执行时发生中断/异常

假设 main() 函数正在执行,此时发生 硬件中断(如时钟中断)或 异常(如缺页异常),CPU 切换到 内核态 处理中断。

3.内核检查未决信号

中断处理完成后,内核在 返回用户态前 检查进程是否有未决信号(如 SIGQUIT)。

若发现未决信号且其动作为“捕捉”,内核准备切换到用户态执行 sighandler。

4.切换到信号处理函数

内核 不恢复 main() 的上下文,而是:

为用户态信号处理函数 sighandler 创建独立的 临时栈帧(与 main() 栈隔离)。

修改用户态栈和寄存器,使 sighandler 成为下一个执行入口。

切换回用户态后,CPU 开始执行 sighandler。

5.信号处理函数执行完毕

sighandler 执行完成后,会 隐式调用 sigreturn() 系统调用,再次进入内核态。

内核此时

销毁临时栈帧。

恢复 main() 的原始上下文(寄存器、栈指针等)。

若无其他未决信号,返回用户态继续执行 main()。
在这里插入图片描述
在这里插入图片描述

(3) 关键点与注意事项

独立性
sighandler 和 main() 是 两个独立的控制流,它们:

使用不同栈空间(内核为 sighandler 分配临时栈)。

无直接调用关系(非函数调用,而是通过内核调度切换)。

可重入性问题
信号处理函数中应避免调用 非异步信号安全函数(如 printf、malloc),否则可能导致死锁或数据损坏。

内核态-用户态切换
一次完整的信号捕捉涉及 两次状态切换:

用户态 → 内核态(中断/异常处理)。

内核态 → 用户态(执行 sighandler)。

用户态 → 内核态(sigreturn)。

内核态 → 用户态(恢复 main())。
在这里插入图片描述

信号屏蔽与竞态条件
内核会在执行 sighandler 时 自动阻塞同一信号(除非显式设置 SA_NODEFER),防止递归触发。

(4)代码示例与验证
以下代码演示信号捕捉流程,并打印栈地址以验证独立性:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sighandler(int signo) {
    printf("Signal %d caught!\n", signo);
    printf("sighandler stack frame: %p\n", (void*)&signo);  // 打印处理函数栈地址
    sleep(1);  // 模拟处理耗时
}

int main() {
    printf("Main stack frame: %p\n", (void*)&main);  // 打印main函数栈地址
    signal(SIGQUIT, sighandler);  // 注册SIGQUIT处理函数

    while (1) {
        printf("Main running...\n");
        sleep(1);
    }
    return 0;
}

输出分析:

main 和 sighandler 的栈地址差异显著,说明二者栈空间独立。

按下 Ctrl+\(触发 SIGQUIT)后,sighandler 打断 main 的执行流。

(5) 信号捕捉的底层机制

内核数据结构:

进程的 task_struct 中维护:

  • pending:未决信号集。

  • blocked:阻塞信号集。

  • sighand:指向信号处理函数表。

sigreturn 的作用:
恢复原始上下文的关键系统调用,由 glibc 在信号处理函数返回前自动触发。

(6)总结图示

用户态 内核态 硬件 main()正常执行 发生中断/异常(如Ctrl+\) 处理硬件中断 检查未决信号(发现SIGQUIT需捕捉) 构建sighandler执行环境(临时栈+寄存器) sighandler()执行 sighandler返回(自动调用sigreturn) 恢复main()上下文 返回main()继续执行 用户态 内核态 硬件

网站公告

今日签到

点亮在社区的每一天
去签到