Linux--进程信号--信号生命周期与堵塞,捕捉信号

一、信号生命周期与堵塞

1. 信号的生命周期

  1. 信号的生命周期的概念
    信号从产生到最终被处理,会经历三个核心阶段:信号产生 → 信号未决 → 信号递达
    在这里插入图片描述
  2. 信号生命周期的完整流程
    在这里插入图片描述
  3. 各阶段详解
  • 信号产生:终端按键、系统调用、软件条件、硬件异常。
  • 信号未决(Pending):信号产生后,进入未决状态。未决信号被记录在进程的未决信号集中,同一个信号多次产生,通常只记录一次
  • 信号阻塞(Block):进程可以选择阻塞某些信号,被阻塞的信号保持在未决状态,不会递达,解除阻塞后,信号才会递达。
  • 信号递达(Delivery):信号实际到达进程,执行处理动作,处理方式有三种:
    ·默认:按系统预设方式处理(通常是终止进程)
    ·忽略:什么也不做
    ·捕捉:执行用户自定义的处理函数
  • 阻塞vs忽略
    在这里插入图片描述
    阻塞 ≠ 忽略。阻塞是"不让进",忽略是"进了当没看见"。

2. 信号在内核中的表示

  1. 信号在内核中的数据结构
    进程 PCB(task_struct)内部维护三套信号管理结构,均采用位图存储,位规则:1 代表生效,0 代表未生效
    在这里插入图片描述
    状态举例:SIGQUIT 的 block 位 = 1、pending 位 = 1 → 信号被阻塞,进入未决队列,解除阻塞后才会执行对应处理逻辑。
  2. 标准信号与实时信号核心差异
  • 标准信号(1~34):不支持排队
    信号阻塞期间重复多次发送同一信号,pending 位只会置 1 一次,解除阻塞后仅处理一次,多余信号直接丢失。
  • 实时信号(34~64):支持排队
    多次触发会依次入队,逐一递达处理;本章仅讲解标准信号。
  1. PCB内核原码简化定义
struct task_struct {
 //...
 struct sigpending pending;      // 未决信号集
 sigset_t blocked;               // 阻塞信号掩码(信号屏蔽字)
 struct sighand_struct *sighand; // 信号处理函数表
 //...
};
  1. 信号完整流转流程
    在这里插入图片描述
    流程文字梳理:
  • 信号产生,内核将对应 pending 位标记为 1;
  • 若信号处于阻塞状态,信号留在未决队列,等待解除阻塞;
  • 内核仅在进程从内核态切换回用户态时,统一校验 pending 信号;
  • 匹配 handler 规则,执行信号处理动作,完成递达。
  1. 信号递达时机说明
    信号不会立刻递达,内核只会在以下场景检查 pending 信号:
  • 进程从内核态切换回用户态(最常见)
  • 系统调用执行完毕
  • sleep()、pause() 等阻塞函数返回
  • 进程被调度恢复运行
    补充理解:
    如果进程长期运行在纯用户态、不触发任何系统调用,pending 信号暂时不会处理。平时按下Ctrl+C可以快速响应,是因为终端发送信号后,进程很快会因为时钟中断、系统调用进入内核态,从而触发信号检测。

3. 阻塞信号的操作

  1. sigset_t 信号集
    sigset_t 本质是位图结构,用来描述信号集合。阻塞信号集(信号屏蔽字)、未决信号集都使用该类型。
    每一个信号仅占用 1 个 bit,只有 0/1 两种状态:
  • 未决位图:bit=1 → 信号已产生、尚未递达;不记录同一个信号触发多少次
  • 阻塞位图:bit=1 → 信号被阻塞
    区分含义:
  • 在阻塞信号集中,bit 有效 = 信号被阻塞;
  • 在未决信号集中,bit 有效 = 信号处于未决状态。
    阻塞信号集又称为进程信号屏蔽字 (Signal Mask)。
    ⚠️ 注意:屏蔽 = 阻塞,不等于忽略。
    重要知识点:标准信号 (1~34) 不排队
    同一个信号阻塞期间多次发送,pending 位依旧保持 1,解除阻塞后只会处理一次;
    实时信号(34~64)支持排队,本章只讨论标准信号。
  1. sigset_t 的操作函数
    sigset_t内部存储格式由系统实现决定,禁止直接打印、强行解析内存,只能调用系统提供接口操作。
#include <signal.h>

// 初始化:清空信号集,所有bit置0
int sigemptyset(sigset_t *set);
// 初始化:填满信号集,所有bit置1
int sigfillset(sigset_t *set);
// 将指定信号加入信号集
int sigaddset(sigset_t *set, int signo);
// 将指定信号从信号集移除
int sigdelset(sigset_t *set, int signo);
// 判断信号是否在集合中:存在返回1,不存在返回0,出错返回-1
int sigismember(const sigset_t *set, int signo);

✅ 使用规范:定义sigset_t变量后,必须先调用 sigemptyset /sigfillset 初始化。

  1. 其它操作函数
  • sigprocmask:修改进程信号屏蔽字
    作用:读取 / 更改当前进程阻塞信号集(block 位图)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
// 返回值:成功0,失败-1

在这里插入图片描述
how三种模式
在这里插入图片描述
硬性限制:SIGKILL(9)、SIGSTOP(19) 无法阻塞、捕获、忽略,内核直接忽略相关设置。
关键特性:
调用 sigprocmask 解除若干未决信号阻塞时,函数返回前,内核至少递达其中一个信号。
简单使用范例:

sigset_t new_mask, old_mask;
sigemptyset(&new_mask);

// 仅读取当前屏蔽字
sigprocmask(0, NULL, &old_mask);

// 阻塞 SIGINT
sigaddset(&new_mask, SIGINT);
sigprocmask(SIG_BLOCK, &new_mask, NULL);

// 解除阻塞
sigprocmask(SIG_UNBLOCK, &new_mask, NULL);

// 备份旧掩码,执行临界区,之后恢复
sigprocmask(SIG_SETMASK, &new_mask, &old_mask);
// 临界区代码
sigprocmask(SIG_SETMASK, &old_mask, NULL);
  • sigpending:获取未决信号集
#include <signal.h>
int sigpending(sigset_t *set);

功能:读取进程 PCB 内的 pending 位图,拿到已经产生、被阻塞、还未递达的信号集合。
⚠️ 只能读取,无法主动修改 pending 位图,pending 由内核自动维护。
使用示例:

sigset_t pending_set;
sigprocmask(SIG_BLOCK, &some_set, NULL);
// 等待信号产生
sigpending(&pending_set);
if (sigismember(&pending_set, SIGINT)) {
 printf("SIGINT处于未决状态!\n");
}
  • 函数对比
    在这里插入图片描述
    关联逻辑:
    只有信号被 block 阻塞时,信号才会停留在 pending;修改阻塞掩码可能立刻触发信号递达;pending 集合仅反映 “被阻塞且已产生” 的信号。
  1. 示例代码
    功能:阻塞 SIGINT (Ctrl+C),循环打印未决信号集,5 秒后解除阻塞。
//signal-set.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

//打印1~32号信号未决位图
void printsigset(const sigset_t *set) 
{
 for (int i = 1; i <= 32; i++) {
  putchar(sigismember(set, i) ? '1' : '0');
 }
 putchar('\n');
}

int main() 
{
 sigset_t block_set, pending_set;
 sigemptyset(&block_set);
 sigaddset(&block_set, SIGINT);

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

 printf("PID: %d\n", getpid());
 printf("尝试按下 Ctrl+C,观察未决信号\n");

 int count = 0;
 while (1) {
  sigpending(&pending_set);
  printsigset(&pending_set);

  //5秒后解除阻塞
  if (++count == 5) {
   printf("\n解除SIGINT阻塞!\n");
   sigprocmask(SIG_UNBLOCK, &block_set, NULL);
  }
  sleep(1);
 }
 return 0;
}

运行现象:
阻塞期间按下Ctrl+C,SIGINT 对应 bit 置 1;5 秒解除阻塞,信号递达,进程终止。
补充:Ctrl+\(SIGQUIT)没有被阻塞,随时可以终止程序。

4. 深入理解:信号处理函数的限制

编写自定义信号处理函数(signal/sigaction注册的回调),存在两条核心约束:

  1. 可重入性
    信号能够随时打断主程序运行,主程序可能正在修改全局变量、链表 数据结构。
    一旦信号触发,立刻跳转执行信号处理函数。
    如果处理函数继续操作同一套全局数据,极易造成数据错乱、程序崩溃。

规则:
信号处理函数内,尽量不使用非可重入函数
典型危险函数:mallocfreeprintf

  1. 异步信号安全(Async-signal-safe)
    信号属于异步事件,执行时机不可预测。
    Linux内核规定:仅有少量系统调用属于异步信号安全函数,允许在信号回调中调用。
    ✅ 安全示例:write()_exit()
    ❌ 不安全:printf(内部封装缓冲区malloc,非异步安全)
  2. 大白话总结
    1.可重入:防止多执行路径争抢数据,避免数据破坏;
    2.异步信号安全:内核层面规定,禁止随意调用大部分库函数,防止死锁、内存异常。

拓展小知识点:
printf 不安全双重原因:
① 内部维护缓冲区(全局状态,不满足可重入)
② 不属于POSIX规定的异步信号安全函数。

二、捕捉信号

捕捉信号:进程提前注册自定义处理函数,信号递达时,内核调用该函数,不再执行信号默认行为。
简单理解:捕捉信号 = 使用自定义函数,替换信号默认处理动作

1. 为什么需要信号捕捉

  1. 信号默认行为由内核执行:程序终止、产生 Core 文件、忽略信号等。
  2. 我们可以通过 signal() / sigaction() 向内核注册自定义回调函数。
  3. 核心目的:程序收到信号后,执行我们自己编写的逻辑,摆脱内核预设的处理方式。
  4. 信号捕捉常见用途:
  • 程序收到退出信号时,先保存数据、释放资源,再优雅退出,防止数据丢失
  • 拦截危险信号,避免程序直接崩溃
  • 实现异步事件通知机制

2. 内核如何实现信号的捕捉

当信号处理方式设置为用户自定义函数,也就是捕捉信号。

  1. 场景假设
    我们提前为 SIGQUIT 注册自定义处理函数 sighandler
    进程此刻正常运行 main 主函数
  2. 信号捕捉流程图
    在这里插入图片描述
  • 用户态运行 main 函数,发生中断 / 异常,进程切换进入内核态
  • 内核完成中断处理,检测到当前进程有待递达信号
  • 准备从内核态返回用户态:不恢复 main 上下文,转而执行信号处理函数sighandler
  • 用户态运行 sighandler;函数结束后,自动触发 sigreturn 再次进入内核态
  • 内核借助 sigreturn 恢复主函数 main 的寄存器、运行上下文
  • 切换回用户态,继续执行原先的 main 代码
  1. 关键点说明
  • main 和 sighandler 是两条独立执行流,没有普通函数调用关系,拥有各自栈空间
  • sigreturn 是系统调用,信号处理函数结束后自动调用
  • 必须依靠 sigreturn 的原因:
    sighandler 不是 main 主动调用的普通函数,无法依靠普通 return 正常回到主程序,需要内核协助恢复现场。

3. 信号处理函数

  1. 回调函数
    回调函数:提前把函数注册给内核,由内核在合适时机调用,代码中不会手动调用。
    信号场景下,sa_handler指向的函数就是回调函数:
  • 不是 main 主动调用
  • 信号递达时,内核自动调用
  • 参数 signo 代表信号编号,同一个处理函数可以区分多种信号
    示例代码 handler_demo.c
// handler_demo.c
// 功能:演示信号处理回调函数
// 编译:gcc handler_demo.c -o handler_demo
// 运行:./handler_demo
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

/**
* 信号处理函数(回调函数)
* 收到信号后由内核自动调用
* @param signo 信号编号(2=SIGINT, 3=SIGQUIT)
*/
void handler(int signo) {
    if (signo == SIGINT) {
        // 信号处理函数禁止使用printf,选用异步安全函数write
        write(1, "Caught SIGINT\n", 14);
    } else if (signo == SIGQUIT) {
        write(1, "Caught SIGQUIT\n", 15);
    }
}

int main() {
    // 向内核注册回调函数
    signal(SIGINT, handler);
    signal(SIGQUIT, handler);

    printf("PID: %d\n", getpid());
    printf("Try Ctrl+C (SIGINT) or Ctrl+\\ (SIGQUIT)\n");

    while (1) {
        sleep(1);
    }
    return 0;
}

运行效果:按下 Ctrl+C / Ctrl+\ 不会终止程序,执行自定义打印;只能使用 kill -9 强制杀死进程。

$ ./handler_demo
PID: 1234
Try Ctrl+C (SIGINT) or Ctrl+\ (SIGQUIT)
^CCaught SIGINT
^CCaught SIGINT
^\Caught SIGQUIT
  1. signal 函数
    signal() 是简易的信号注册接口,功能有限,可移植性较差。
    函数原型:
#include <signal.h>
// 原始写法
void (*signal(int signo, void (*handler)(int)))(int);

// 使用typedef简化
typedef void (*sighandler_t)(int);
sighandler_t signal(int signo, sighandler_t handler);

返回值:成功返回旧的信号处理函数指针;失败返回 SIG_ERR,设置错误码
参数 signo:信号编号
参数 handler 三种取值:
在这里插入图片描述
返回值:成功返回旧处理函数指针;失败返回 SIG_ERR
三种使用场景:

  • signal(SIGINT, SIG_IGN); 忽略信号,Ctrl+C 失效
  • signal(SIGINT, SIG_DFL); 使用系统默认行为,按下 Ctrl+C 程序退出
  • signal(SIGINT, my_handler); 注册自定义捕捉函数
    signal主要缺点
  • 可移植性差,不同系统实现行为不一致
  • 执行信号处理函数时,不会自动屏蔽同类信号
  • 无法在处理期间额外屏蔽其他信号,控制粒度粗糙
  1. sigaction 函数
    sigaction 是 signal 的增强版本,POSIX 标准推荐,支持精细化管控信号行为。
    函数原型:
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • act:传入参数,新的信号配置;传 NULL 代表不修改
  • oact:传出参数,保存旧配置;不需要可置 NULL
    struct sigaction 结构体
struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
};
  • sa_handler:信号处理方式
  • sa_mask:执行处理函数期间,额外临时屏蔽的信号集
  • sa_flags:行为标志,常规使用置 0
    核心机制:自动屏蔽
    调用 sigaction 注册信号捕捉时:
    执行信号处理函数期间,内核自动屏蔽当前信号;同类信号多次抵达会阻塞,等待处理函数结束后再递达。结合sa_mask可以额外屏蔽其他指定信号。
    代码示例:
// sigaction_demo.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

void handler(int signo) {
    if (signo == SIGINT) {
        write(1, "Caught SIGINT (Ctrl+C)\n", 23);
        sleep(3);
    } else if (signo == SIGQUIT) {
        write(1, "Caught SIGQUIT (Ctrl+\\)\n", 24);
    }
}

int main() {
    struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = handler;

    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGQUIT); // 处理SIGINT时屏蔽SIGQUIT
    act.sa_flags = 0;

    sigaction(SIGINT, &act, NULL);
    sigaction(SIGQUIT, &act, NULL);

    printf("PID: %d\n", getpid());
    while (1) sleep(1);
    return 0;
}

现象:触发 SIGINT 后立刻按Ctrl+\,SIGQUIT 不会立刻响应,等待 3 秒处理结束后才执行,验证信号屏蔽效果。

  1. sigaction 相比 signal 的优势
    在这里插入图片描述
    ✅ 开发建议:正式项目优先使用 sigaction;signal 仅用于简单测试、兼容老旧代码。

4. 不可捕捉的信号

不是所有信号都支持捕捉、忽略、阻塞。SIGKILL(9) 与 SIGSTOP(19) 是特例。
在这里插入图片描述
底层原因:
倘若进程能够捕获 SIGKILL,恶意 / 失控程序就可以拦截信号,系统将无法使用 kill -9 强制回收进程。
这两个信号是操作系统留给管理员的最终强制手段,保障系统控制权。
代码验证:

// 下面两行代码不会生效
signal(SIGKILL, handler);  
signal(SIGSTOP, handler);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值