信号机制

信号机制

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 忽略 供用户程序自定义用途。

重要说明

  • SIGKILLSIGSTOP 是系统保留信号,进程无法通过编程改变其处置方式(无法安装处理程序,也无法忽略),确保系统拥有对进程的绝对控制权。
  • 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. 代码示例:阻塞信号

以下代码演示了如何使用 sigprocmasksigset_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. 添加信号:使用 sigaddsetSIGINT 加入集合。
  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. 特殊信号SIGKILLSIGSTOP 不可捕获、不可忽略,用于强制管理进程。
  5. 编程接口
    • 使用 sigset_t 管理信号集合。
    • 使用 sigprocmask 修改进程信号掩码,实现信号的阻塞与解除阻塞。
    • 理解“阻塞”与“忽略”的区别:阻塞是信号未到达进程,忽略是信号到达但被丢弃。
孤儿进程、守护进程 2026-03-09

评论区