1. 什么是信号
1.1 核心定义
- 通知机制:信号是事件发生时对进程的通知机制。例如,按下
Ctrl+C实际上是向进程发送一个通知,要求进程关闭或中止。 - 软件中断:信号有时也被称为软件中断。这一概念借鉴了硬件层面的 CPU 中断机制。
- 硬件中断:硬件设备触发,打断 CPU 正常执行流程。
- 软件中断:操作系统内核层面,打断进程的正常执行流程。
1.2 工作原理
信号的工作过程类似于中断:
- 程序正常执行。
- 信号到来,打断程序执行。
- 触发信号处理程序。
- 处理完成后,返回继续运行原程序(除非信号默认行为是终止进程)。
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 进程的处置方式
当信号突破掩码到达进程后,进程有三种处置方式:
-
默认行为 (Default Action)
- 大多数信号的默认行为。
- 包括:终止进程 (Terminate)、停止进程 (Stop)、继续进程 (Continue)、忽略 (Ignore)、核心转储 (Core Dump)。
- 示例:
SIGINT默认终止进程。
-
忽略信号 (Ignore)
- 进程显式设置忽略该信号。
- 信号到达进程,但进程不做任何反应。
- 注意:这与“信号掩码阻塞”不同,忽略是收到后不理睬,阻塞是根本收不到。
-
执行信号处理程序 (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 变量:
-
初始化信号集
int sigemptyset(sigset_t *set);- 将信号集初始化为空(不包含任何信号)。
int sigfillset(sigset_t *set);- 将信号集初始化为满(包含所有信号)。
-
添加或删除单个信号
int sigaddset(sigset_t *set, int signum);- 将指定信号
signum添加到信号集set中。
- 将指定信号
int sigdelset(sigset_t *set, int signum);- 从信号集
set中删除指定信号signum。
- 从信号集
-
测试信号成员
int sigismember(const sigset_t *set, int signum);- 测试信号
signum是否是信号集set的成员。
- 测试信号
5.3 设置进程信号掩码
- 函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); - 功能:检查或更改进程的信号掩码。
- 参数说明:
how:操作方式。SIG_BLOCK:将set中的信号添加到当前掩码中(阻塞这些信号)。逻辑相当于:NewMask = OldMask | Set。SIG_UNBLOCK:从当前掩码中移除set中的信号(解除阻塞)。逻辑相当于:NewMask = OldMask & ~Set。SIG_SETMASK:将当前掩码直接设置为set的值。
set:指向包含新信号掩码设置的sigset_t指针。如果为NULL,则不改变掩码,仅查询。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;
}
代码逻辑解析
- 定义信号集:创建
sigset_t变量set。 - 初始化:使用
sigemptyset确保集合干净。 - 添加信号:使用
sigaddset将SIGINT加入集合。 - 应用掩码:调用
sigprocmask并传入SIG_BLOCK。- 此时内核中该进程的信号掩码对应
SIGINT的位被置为 1。 - 当用户按下
Ctrl+C,内核产生SIGINT信号,但检查掩码后发现被阻塞。 - 信号进入等待 (Pending) 状态,不会传递给进程,进程继续执行
sleep。
- 此时内核中该进程的信号掩码对应
- 结果:在
sleep(5)期间,Ctrl+C无效。5 秒后程序打印结束信息。若此时再次按下Ctrl+C,由于程序未恢复掩码(或进程未退出),信号仍可能被阻塞,直到进程退出或显式解除阻塞。
7. 总结
- 信号本质:内核向进程发送的通知机制,本质是整数,属于软件中断。
- 生命周期:产生 (Generation) -> 等待/阻塞 (Pending/Blocked) -> 处置 (Disposition)。
- 处置方式:默认行为、忽略、捕获(执行自定义处理函数)。
- 特殊信号:
SIGKILL和SIGSTOP不可捕获、不可忽略,用于强制管理进程。 - 编程接口:
- 使用
sigset_t管理信号集合。 - 使用
sigprocmask修改进程信号掩码,实现信号的阻塞与解除阻塞。 - 理解“阻塞”与“忽略”的区别:阻塞是信号未到达进程,忽略是信号到达但被丢弃。
- 使用