信号机制

信号机制

1. 什么是信号

1.1 核心定义

  • 通知机制:信号是事件发生时对进程的通知机制。例如,按下 Ctrl+C 实际上是向进程发送一个通知,要求进程关闭或中止。
  • 软件中断:信号有时也被称为软件中断。这一概念借鉴了硬件层面的 CPU 中断机制。
    • 硬件中断:硬件设备触发,打断 CPU 正常执行流程。
    • 软件中断:操作系统内核层面,打断进程的正常执行流程。

1.2 工作原理

信号的工作过程类似于中断:

  1. 程序正常执行。
  2. 信号到来,打断程序执行。
  3. 触发信号处理程序。
  4. 处理完成后,返回继续运行原程序(除非信号默认行为是终止进程)。

1.3 信号的本质

  • 来源:信号源于内核。对于应用层程序而言,信号是从内核发送过来的。
  • 表现形式:在程序中,信号是一个整数。
    • 每个信号对应一个唯一的数值。
    • 标准信号范围通常为 ​1 到 ​31。
    • 定义在头文件 signal.h 中。
    • 宏定义通常以 SIG 开头(例如 SIGINT)。

2. 信号的产生来源

信号的根本来源主要分为以下三类:

2.1 硬件异常

程序执行过程中出现错误,由硬件触发异常,内核转换为信号发送给进程。

  • 示例:
    • 除零错误 (Division by zero)。
    • 访问非法内存区域(如只读内存写入),触发 段错误 (Segmentation Fault),对应信号 SIGSEGV。
    • 结果:通常导致进程终止。

2.2 终端特殊字符

用户在终端输入特定组合键,内核捕获后发送信号。

  • Ctrl+C:发送 SIGINT 信号,默认终止进程。
  • Ctrl+\:发送 SIGQUIT 信号,默认终止进程并生成核心转储。
  • Ctrl+Z:发送 SIGTSTP 信号,默认停止进程(转入后台)。

2.3 软件事件

由系统状态变化或软件逻辑触发的信号。

  • 调整终端窗口大小:触发相应信号通知进程。
  • 定时器到期:使用 alarm 系统调用设置定时器,时间到后发送 SIGALRM 信号。
  • 子进程退出:子进程状态改变(退出或停止),向父进程发送 SIGCHLD 信号。
  • 终端关闭:关闭终端窗口时,向关联进程发送 SIGHUP 信号。

3. 信号的响应过程

信号从产生到被处理,经历三个阶段:产生 (Generation) -> 等待 (Pending) -> 处置 (Disposition)。

3.1 等待状态

  • 信号产生后,不一定立即传递给进程,可能处于等待状态。
  • 原因:进程拥有信号掩码 (Signal Mask) 属性。
  • 信号掩码:
    • 本质是一个位图(Bitmask),对应 ​1 到 ​31 号信号。
    • 如果掩码中某一位被置为 ​1(屏蔽/阻塞),则对应的信号即使产生,也会被阻塞在等待状态,无法传递给进程。
    • 当掩码中对应位变为 ​0 时,阻塞解除,信号传递给进程。
  • 区别:
    • 阻塞 (Block):信号被掩码拦截,处于等待状态,进程未收到信号。
    • 忽略 (Ignore):信号已传递给进程,但进程选择不予处理。

3.2 进程的处置方式

当信号突破掩码到达进程后,进程有三种处置方式:

  1. 默认行为 (Default Action)

    • 大多数信号的默认行为。
    • 包括:终止进程 (Terminate)、停止进程 (Stop)、继续进程 (Continue)、忽略 (Ignore)、核心转储 (Core Dump)。
    • 示例:SIGINT 默认终止进程。
  2. 忽略信号 (Ignore)

    • 进程显式设置忽略该信号。
    • 信号到达进程,但进程不做任何反应。
    • 注意:这与“信号掩码阻塞”不同,忽略是收到后不理睬,阻塞是根本收不到。
  3. 执行信号处理程序 (Catch/Handler)

    • 进程注册自定义的回调函数(信号处理函数)。
    • 信号到达时,内核暂停进程主流程,转而执行该处理函数。
    • 执行完毕后,返回原进程继续执行。
    • 术语:称为安装 (Install) 或 捕获 (Catch) 信号。

4. 常见信号类型及默认行为

信号名称 触发场景/含义 默认行为 特性说明
SIGSEGV 段错误 (Segmentation Fault) 终止 + 核心转储 访问非法内存时触发。
SIGINT 中断信号 (Interrupt) 终止进程 对应 Ctrl+C,可被捕获或忽略。
SIGQUIT 退出信号 (Quit) 终止 + 核心转储 对应 Ctrl+\。
SIGALRM 定时器到期 (Alarm) 终止进程 由 alarm 系统调用触发。
SIGCHLD 子进程状态改变 忽略 子进程退出或停止时发送给父进程。
SIGHUP 挂起信号 (Hangup) 终止进程 终端关闭或连接断开时触发。
SIGCONT 继续信号 (Continue) 继续执行 用于恢复被停止的进程。
SIGKILL 杀死信号 (Kill) 终止进程 不可捕获,不可忽略。强制终止进程的根本手段。
SIGSTOP 停止信号 (Stop) 停止进程 不可捕获,不可忽略。强制停止进程。
SIGUSR1 用户自定义信号 1 忽略 供用户程序自定义用途。
SIGUSR2 用户自定义信号 2 忽略 供用户程序自定义用途。

重要说明:

  • SIGKILL 和 SIGSTOP 是系统保留信号,进程无法通过编程改变其处置方式(无法安装处理程序,也无法忽略),确保系统拥有对进程的绝对控制权。
  • SIGINT 可以被捕获(例如实现优雅退出)或忽略。

5. 信号掩码与信号集编程

在编程层面,操作信号掩码需要使用信号集 (Signal Set) 数据类型。

5.1 信号集数据类型

  • 类型名:sigset_t
  • 含义:表示一组信号的集合,底层通常实现为位图结构。
  • 操作原则:不能直接操作 sigset_t 变量,必须使用提供的 API 函数。

5.2 信号集操作函数

以下函数用于初始化和操作 sigset_t 变量:

  1. 初始化信号集

    • int sigemptyset(sigset_t *set);
      • 将信号集初始化为空(不包含任何信号)。
    • int sigfillset(sigset_t *set);
      • 将信号集初始化为满(包含所有信号)。
  2. 添加或删除单个信号

    • int sigaddset(sigset_t *set, int signum);
      • 将指定信号 signum 添加到信号集 set 中。
    • int sigdelset(sigset_t *set, int signum);
      • 从信号集 set 中删除指定信号 signum。
  3. 测试信号成员

    • int sigismember(const sigset_t *set, int signum);
      • 测试信号 signum 是否是信号集 set 的成员。

5.3 设置进程信号掩码

  • 函数:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • 功能:检查或更改进程的信号掩码。
  • 参数说明:
    1. how:操作方式。
      • SIG_BLOCK:将 set 中的信号添加到当前掩码中(阻塞这些信号)。逻辑相当于:NewMask = OldMask | Set。
      • SIG_UNBLOCK:从当前掩码中移除 set 中的信号(解除阻塞)。逻辑相当于:NewMask = OldMask & ~Set。
      • SIG_SETMASK:将当前掩码直接设置为 set 的值。
    2. set:指向包含新信号掩码设置的 sigset_t 指针。如果为 NULL,则不改变掩码,仅查询。
    3. oldset:指向用于保存旧信号掩码的 sigset_t 指针。如果为 NULL,则不保存旧掩码。

6. 代码示例:阻塞信号

以下代码演示了如何使用 sigprocmask 和 sigset_t 来阻塞 SIGINT 信号(即让 Ctrl+C 暂时失效)。

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

int main() {
    sigset_t set;         // 定义信号集
    sigset_t prev_mask;   // 用于保存旧的信号掩码

    // 1. 初始化信号集为空
    sigemptyset(&set);

    // 2. 将 SIGINT 信号添加到信号集中
    sigaddset(&set, SIGINT);

    // 3. 设置进程信号掩码
    // SIG_BLOCK: 将 set 中的信号添加到当前掩码 (阻塞 SIGINT)
    // &prev_mask: 保存原来的掩码以便后续恢复 (本例未恢复)
    if (sigprocmask(SIG_BLOCK, &set, &prev_mask) < 0) {
        perror("sigprocmask");
        exit(1);
    }

    printf("SIGINT 已阻塞,请按 Ctrl+C 测试 (进程不会终止)...\n");
    printf("程序将暂停 5 秒。\n");

    // 4. 暂停 5 秒
    // 在此期间,即使按下 Ctrl+C,信号也会被内核阻塞在等待状态
    // 进程不会收到信号,因此不会终止
    sleep(5);

    printf("5 秒结束,程序继续运行。\n");
    printf("此时若再按 Ctrl+C,进程将终止 (除非恢复掩码)。\n");

    // 注意:程序结束后,进程销毁,掩码随之消失
    // 若要恢复,可再次调用 sigprocmask(SIG_SETMASK, &prev_mask, NULL);

    return 0;
}

代码逻辑解析

  1. 定义信号集:创建 sigset_t 变量 set。
  2. 初始化:使用 sigemptyset 确保集合干净。
  3. 添加信号:使用 sigaddset 将 SIGINT 加入集合。
  4. 应用掩码:调用 sigprocmask 并传入 SIG_BLOCK。
    • 此时内核中该进程的信号掩码对应 SIGINT 的位被置为 ​1。
    • 当用户按下 Ctrl+C,内核产生 SIGINT 信号,但检查掩码后发现被阻塞。
    • 信号进入等待 (Pending) 状态,不会传递给进程,进程继续执行 sleep。
  5. 结果:在 sleep(5) 期间,Ctrl+C 无效。5 秒后程序打印结束信息。若此时再次按下 Ctrl+C,由于程序未恢复掩码(或进程未退出),信号仍可能被阻塞,直到进程退出或显式解除阻塞。

7. 总结

  1. 信号本质:内核向进程发送的通知机制,本质是整数,属于软件中断。
  2. 生命周期:产生 (Generation) -> 等待/阻塞 (Pending/Blocked) -> 处置 (Disposition)。
  3. 处置方式:默认行为、忽略、捕获(执行自定义处理函数)。
  4. 特殊信号:SIGKILL 和 SIGSTOP 不可捕获、不可忽略,用于强制管理进程。
  5. 编程接口:
    • 使用 sigset_t 管理信号集合。
    • 使用 sigprocmask 修改进程信号掩码,实现信号的阻塞与解除阻塞。
    • 理解“阻塞”与“忽略”的区别:阻塞是信号未到达进程,忽略是信号到达但被丢弃。
孤儿进程、守护进程 2026-03-09
信号处理 2026-03-15

评论区