🌟 一、什么是信号(Signal)?
- 信号是 Linux/Unix 系统中进程间通信(IPC)的一种机制。
- 它是一种异步通知 :当某个事件发生时(如用户按键、硬件错误、定时器到期),系统会向进程"发送一个信号",进程可以选择:
- 忽略
- 执行默认动作(如终止、暂停)
- 自定义处理函数(捕获信号)
💡 类比:就像你正在写作业,突然手机响了(信号来了)------你可以挂掉(忽略)、接电话(处理)、或者让它一直响到自动挂断(默认动作)。
🧩 二、信号的五种产生方式(核心!)
1️⃣ 按键产生(来自终端)
| 组合键 | 信号名 | 信号编号 | 作用 |
|---|---|---|---|
Ctrl + C |
SIGINT |
2 | 中断/终止进程(最常用) |
Ctrl + \ |
SIGQUIT |
3 | 退出 + 可能生成 core 文件(调试用) |
Ctrl + Z |
SIGTSTP |
20 | 暂停进程 (可恢复,用 fg 或 bg) |
✅ 实际应用:
ping命令无限发包,按Ctrl+C停止- 程序卡死,用
Ctrl+\强制退出并看 core 文件
⚠️ 易错点:
Ctrl+Z不是"退出",而是"暂停"!进程还在后台。SIGQUIT(3号)比SIGINT(2号)更"狠",会尝试生成 core。
2️⃣ 系统调用产生(程序主动发信号)
| 函数 | 产生信号 | 编号 | 说明 |
|---|---|---|---|
alarm(n) |
SIGALRM |
14 | n 秒后发信号(用于定时) |
abort() |
SIGABRT |
6 | 异常终止,强制生成 core 文件 |
raise(sig) |
指定信号 | - | 向自己 发信号(如 raise(SIGTERM)) |
✅ 特点:
- 是程序主动行为,不是被动响应
alarm()常用于超时控制(如网络请求超时)abort()常用于断言失败(assert 失败时调用)
⚠️ 注意:
- 每个进程只有一个 alarm 定时器,再次调用会覆盖
alarm(0)可取消定时器,并返回剩余秒数
第一步:先搞清楚「什么是系统调用产生信号」?
✅ 核心概念:
系统调用产生信号 = 程序自己主动"发信号给自己或别人"。
这和「用户按 Ctrl+C」或「程序崩溃」不同------那是被动 收到信号。
而这里,是程序主动调用函数,说:"嘿,给我自己(或别人)发个信号!
🕒 第二步:先学 alarm(n) ------ "闹钟信号"
🔹 函数原型:
cpp
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
🔹 功能:
-
设置一个闹钟 :n 秒后 ,系统会向当前进程发送
SIGALRM(14号信号)。 -
如果之前已经设过闹钟,新的
alarm(n)会覆盖旧的。 -
如果你调用
alarm(0),表示取消闹钟 ,并返回之前闹钟还剩多少秒。
🔹 举个生活例子:
你正在煮泡面,设了个 3 分钟闹钟。
但 1 分钟后你突然想改成 5 分钟------于是你重新设一个 5 分钟闹钟 ,旧的就没了。
如果你突然不想煮了,就把闹钟关掉(
alarm(0)),顺便看看还剩几分钟。
🔹 代码演示:
cpp
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig) {
printf("闹钟响了!收到信号 %d\n", sig);
}
int main() {
// 注册信号处理函数(捕获 SIGALRM)
signal(SIGALRM, handler);
printf("设置 3 秒闹钟...\n");
alarm(3); // 3秒后发 SIGALRM
printf("睡觉等闹钟...\n");
sleep(5); // 睡5秒,肯定能等到闹钟
printf("程序结束。\n");
return 0;
}
✅ 输出:
设置 3 秒闹钟...
睡觉等闹钟...
闹钟响了!收到信号 14
程序结束。
🔹 关键点总结:
| 问题 | 答案 |
|---|---|
| 能设多个 alarm 吗? | ❌ 不能!每个进程只有一个 alarm 定时器 |
alarm(0) 干嘛用? |
取消定时器,并返回剩余秒数 |
| 默认会终止程序吗? | ✅ 会!SIGALRM 默认动作是终止进程 ,除非你用 signal() 捕获它 |
| 用在哪儿? | 网络超时、防止死循环、定时任务等 |
💥 第三步:学 abort() ------ "我崩溃了!"
🔹 函数原型:
cpp
#include <stdlib.h>
void abort(void);
🔹 功能:
-
立即向自己发送
SIGABRT(6号信号) -
强制终止进程
-
通常会生成 core 文件(用于调试)
-
即使你捕获了 SIGABRT,abort() 仍会确保进程退出
🔹 什么时候用?
-
程序遇到严重错误,无法继续运行
-
C 语言中的
assert()宏在条件失败时就会调用abort()
🔹 例子:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main() {
int x = 0;
assert(x != 0); // 断言失败 → 调用 abort()
printf("这行不会执行\n");
return 0;
}
运行结果(可能):
a.out: test.c:6: main: Assertion `x != 0' failed.
Aborted (core dumped)
🔹 关键点:
| 问题 | 答案 |
|---|---|
| 能阻止 abort() 吗? | ❌ 不能!即使你写了 signal(SIGABRT, handler),abort() 仍会退出 |
| 会生成 core 吗? | ✅ 通常会(取决于系统设置) |
| 和 exit() 有什么区别? | exit() 是正常退出;abort() 是异常退出 + 调试信息 |
📣 第四步:学 raise(sig) ------ "我自己发个信号"
🔹 函数原型:
cpp
#include <signal.h>
int raise(int sig);
🔹 功能:
-
向当前进程自己发送一个指定信号
-
等价于:
kill(getpid(), sig)
🔹 用途:
-
主动触发信号处理逻辑(比如模拟错误)
-
在特定条件下"优雅退出"
🔹 例子:
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void quit_handler(int sig) {
printf("收到退出信号 %d,准备清理...\n", sig);
// 做一些清理工作
_exit(0); // 安全退出
}
int main() {
signal(SIGTERM, quit_handler); // 捕获 SIGTERM
printf("程序运行中...\n");
sleep(2);
printf("主动发送 SIGTERM 给自己\n");
raise(SIGTERM); // 相当于 kill(getpid(), SIGTERM)
printf("这行不会执行\n");
return 0;
}
✅ 输出:
程序运行中...
主动发送 SIGTERM 给自己
收到退出信号 15,准备清理...
🔹 关键点:
| 问题 | 答案 |
|---|---|
| 和 kill(getpid(), sig) 一样吗? | ✅ 基本一样 |
| 能发 SIGKILL 给自己吗? | ✅ 可以,但无法被捕获,会立即终止 |
| 为什么要用它? | 方便、清晰地表达"我要触发某个信号" |
🧠 第五步:对比总结三者
| 函数 | 发什么信号 | 谁发给谁 | 能否被捕获 | 典型用途 |
|---|---|---|---|---|
alarm(n) |
SIGALRM (14) |
系统 n 秒后发给自己 | ✅ 可以 | 定时、超时控制 |
abort() |
SIGABRT (6) |
自己立刻发给自己 | ❌ 无法阻止退出 | 严重错误、断言失败 |
raise(sig) |
任意信号 | 自己发给自己 | ✅ 取决于信号类型 | 主动触发信号处理 |
| 函数 | 你的理解 | 精准表达(保留你的风格) |
|---|---|---|
alarm(n) |
是定时处理,给定时间 | "定时器模式":设定一个倒计时,时间一到,系统自动发信号提醒我。适合做超时控制。 |
abort() |
则是用来判断处理 | "紧急熔断":程序发现严重错误(比如断言失败),立刻自爆并留下现场证据(core 文件),不给继续运行的机会。 |
raise(sig) |
则是自由发挥,随时处理 | "手动触发":我想在任何时刻、主动给自己发一个信号,用来模拟中断、优雅退出或测试信号处理逻辑。 |
3️⃣ 软件条件产生(定时器类)
- 主要函数:
setitimer()(比alarm更强大) - 可设置高精度定时器(微秒级)
- 也能产生
SIGALRM(或其他如SIGVTALRM,SIGPROF)
✅ 与系统调用的关系:
- 和
alarm()功能重叠,但setitimer更灵活(可周期性触发) - 都属于"软件主动触发",不是硬件或用户行为
🧭 第一步:为什么需要 setitimer()?alarm() 不够用吗?
✅ alarm() 的局限:
-
只能精确到秒(不能设 0.5 秒)
-
只能单次触发(响一次就没了,不能自动重复)
-
只能产生
SIGALRM
👉 如果你想:
-
每 500 毫秒打印一次 "心跳"
-
做一个高精度的性能分析工具
-
实现周期性任务(比如每秒刷新)
那么 alarm() 就不够用了!
⏱️ 第二步:认识 setitimer() ------ 高级定时器
🔹 函数原型:
cpp
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
🔹 核心功能:
-
设置一个高精度、可重复的定时器
-
精度到微秒(μs)
-
可以自动周期性触发
-
支持三种类型的定时器(对应不同信号)
🔧 第三步:三种定时器类型(which 参数)
| 类型 | 信号 | 用途说明 |
|---|---|---|
ITIMER_REAL |
SIGALRM |
真实时间:墙上时钟,不管进程是否在运行 |
ITIMER_VIRTUAL |
SIGVTALRM |
用户态 CPU 时间:只计算进程在用户态运行的时间 |
ITIMER_PROF |
SIGPROF |
用户+内核 CPU 时间:用于性能分析(profiling) |
✅ 最常用的是
ITIMER_REAL(和alarm()一样发SIGALRM)
📦 第四步:理解 struct itimerval
这是设置定时器的关键结构体:
cpp
struct itimerval {
struct timeval it_value; // 第一次触发的延迟时间
struct timeval it_interval; // 之后每次重复的间隔时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒(1秒 = 1,000,000 微秒)
};
💡 关键逻辑:
-
it_value= 0 → 定时器不启动 -
it_interval= 0 → 只触发一次 (类似alarm) -
it_interval> 0 → 周期性触发 (第一次等it_value,之后每隔it_interval触发一次)
🧪 第五步:代码演示 ------ 每 0.5 秒打印一次
cpp
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
void timer_handler(int sig) {
printf("滴答!收到信号 %d\n", sig);
}
int main() {
// 1. 注册信号处理函数
signal(SIGALRM, timer_handler);
// 2. 配置定时器
struct itimerval timer;
timer.it_value.tv_sec = 1; // 第一次:1秒后开始
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 0; // 之后每隔 0.5 秒重复
timer.it_interval.tv_usec = 500000; // 500,000 微秒 = 0.5 秒
// 3. 启动定时器(ITIMER_REAL → SIGALRM)
setitimer(ITIMER_REAL, &timer, NULL);
// 4. 主程序继续运行(等待信号)
printf("定时器已启动,每0.5秒滴答一次...\n");
while (1) {
sleep(1); // 防止程序太快退出
}
return 0;
}
✅ 输出(每 0.5 秒一行):
cpp
定时器已启动,每0.5秒滴答一次...
滴答!收到信号 14
滴答!收到信号 14
滴答!收到信号 14
...
🔚 按
Ctrl+C终止程序。
🔁 第六步:如何停止定时器?
只需把 it_value 和 it_interval 都设为 0:
cpp
struct itimerval stop = {{0, 0}, {0, 0}};
setitimer(ITIMER_REAL, &stop, NULL);
或者更简单:
cpp
struct itimerval zero = {0};
setitimer(ITIMER_REAL, &zero, NULL);
🆚 第七步:setitimer() vs alarm() 对比表
| 特性 | alarm(n) |
setitimer() |
|---|---|---|
| 精度 | 秒级 | 微秒级 ✅ |
| 触发次数 | 仅一次 | 可周期性重复 ✅ |
| 信号类型 | 仅 SIGALRM |
SIGALRM / SIGVTALRM / SIGPROF ✅ |
| 是否覆盖旧定时器 | 是 | 是(同类型) |
| 复杂度 | 简单 | 稍复杂(需结构体) |
💡 所以说:
alarm()是"简版闹钟",setitimer()是"专业定时器"
🧠 第八步:实际应用场景
-
游戏/动画帧率控制:每 16ms 刷新一次(≈60 FPS)
-
网络超时重传:每 200ms 检查一次是否收到回复
-
性能监控工具 :用
ITIMER_PROF定期采样 CPU 使用情况 -
守护进程心跳:定期上报"我还活着"
✅ 最后:一句话总结
setitimer()是alarm()的"超级升级版"------更高精度、可重复、多模式,专为专业定时任务而生。每次使用 setitimer 时,你都需要设置一个 struct itimerval 结构体变量,指定定时器的初始延迟和间隔。
4️⃣ 硬件异常产生(最常见错误来源!)
这是程序 bug 触发的信号,由 CPU 或内存硬件异常引发,内核代为发送信号:
| 异常类型 | 信号名 | 编号 | 原因 |
|---|---|---|---|
| 段错误 | SIGSEGV |
11 | 访问非法内存(如空指针、越界) |
| 除零 / 浮点错误 | SIGFPE |
8 | 5 / 0、浮点运算异常("F" = float) |
| 总线错误 | SIGBUS |
7 | 内存地址未对齐(如某些架构要求 4 字节对齐) |
✅ 关键点:
- 默认动作:终止进程 + 可能生成 core 文件
- 这是 C/C++ 程序崩溃的主要原因
- 无法用
signal()忽略SIGKILL/SIGSTOP,但这些硬件信号可以被捕获(不过一般不建议,应修复 bug)
🧭 第一步:什么是"硬件异常产生信号"?
✅ 核心概念:
当你的程序执行了非法的硬件操作 (比如访问不存在的内存、除以零),CPU 会立刻检测到异常 ,然后通知操作系统(内核)。
内核不会直接杀掉你,而是向你的进程发送一个对应的信号(如 SIGSEGV),让你有机会处理(或默认终止)。
这和"用户按 Ctrl+C"完全不同------
-
用户信号:外部主动中断
-
硬件异常信号 :程序自己"作死"触发的,是 bug 的直接体现!
🧱 第二步:逐个击破三种常见硬件异常
1️⃣ 段错误(Segmentation Fault)→ SIGSEGV(11号)
❓ 什么是"段错误"?
-
你的程序试图访问它无权访问的内存地址。
-
"段"是内存管理中的一个概念(现代系统用页,但名字沿用下来)。
🚫 常见原因:
cpp
// 1. 解引用空指针
int *p = NULL;
*p = 10; // ❌ 段错误!
// 2. 数组越界(访问栈外或堆外)
int arr[5];
arr[10] = 100; // ❌ 可能段错误(不一定立即崩溃)
// 3. 访问已释放的内存
int *p = malloc(4);
free(p);
*p = 5; // ❌ 危险!可能段错误
💡 为什么叫 SIGSEGV?
- SEGV = Segmentation Violation(段违规)
✅ 默认行为:
- 终止进程 + 生成
core文件(如果系统允许)
2️⃣ 除零 / 浮点异常 → SIGFPE(8号)
❓ 什么是 FPE?
-
FPE = Floating-Point Exception(浮点异常)
-
虽然名字叫"浮点",但整数除零也会触发!
🚫 常见原因:
cpp
// 1. 整数除零
int a = 5 / 0; // ❌ 触发 SIGFPE!
// 2. 浮点运算异常(如 sqrt(-1) 在某些实现中)
double x = 0.0 / 0.0; // NaN,可能触发
⚠️ 注意:不是所有浮点错误都触发信号(取决于 CPU 和编译器设置),但整数除零一定会!
💡 为什么除零是硬件异常?
- CPU 的除法指令在检测到除数为 0 时,会直接抛出异常 ,由操作系统转换为
SIGFPE。
3️⃣ 总线错误 → SIGBUS(7号)
❓ 什么是总线错误?
-
CPU 通过"总线"访问内存,但地址不符合硬件要求。
-
最常见于:内存地址未对齐(alignment)。
🚫 举例(在某些架构上,如 ARM、SPARC):
cpp
char data[5] = {1,2,3,4,5};
int *p = (int*)(data + 1); // 指向地址 data+1(不是4字节对齐)
int x = *p; // ❌ 可能触发 SIGBUS!
💡 在 x86 架构上,通常不会报
SIGBUS(硬件容忍不对齐),但在 ARM、RISC-V 等架构上会!
其他原因:
-
访问 mmap 映射但已被截断的文件
-
硬件故障(极少见)
kill() 函数
1️⃣ 函数原型
cpp
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
✅ 记住:
kill不是"杀",是"发信号"!
2️⃣ 参数 pid 的四种取值(重点!考试高频!)
pid 值 |
含义 | 举例 | 用途 |
|---|---|---|---|
> 0 |
发给指定 PID 的进程 | kill(1234, SIGTERM) |
终止某个具体进程 |
= 0 |
发给当前进程所在进程组的所有进程 | kill(0, SIGTERM) |
批量终止同组进程(如管道中的所有命令) |
< -1 |
发给进程组 ID = |pid| 的所有进程 | kill(-8514, SIGKILL) |
终止整个进程组 |
= -1 |
发给调用者有权限的所有进程(除 init) | kill(-1, SIGTERM) |
极其危险!慎用 |
💡 进程组(Process Group) :一组逻辑相关的进程(比如
cat | grep | wc属于同一组)
3️⃣ 参数 sig = 0 的特殊用法(面试常问!)
cpp
if (kill(pid, 0) == 0) {
printf("进程 %d 存在且我有权限操作\n", pid);
} else {
printf("进程不存在或无权限\n");
}
✅ 不发信号,只检查进程是否存在 + 权限是否足够。
⚠️ 第三步:权限规则(为什么 kill 有时失败?)
普通用户只能向自己创建的进程发信号(除非是 root)。
具体规则:
- 发送者的 real/effective UID 必须等于接收者的 real/saved set-user-ID
- 特权进程(如 root)可以发给任何进程
❌ 你不能随便
kill别人的进程(安全机制)
💻 第四步:代码实战 ------ 子进程发信号给父进程
cpp
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
sleep(2);
printf("子进程:向父进程发送 SIGTERM\n");
kill(getppid(), SIGTERM); // getppid() = 父进程 PID
} else {
// 父进程
printf("父进程 PID: %d,等待信号...\n", getpid());
while (1) {
printf("父进程运行中...\n");
sleep(1);
}
wait(NULL); // 实际不会执行到这里
}
return 0;
}
✅ 运行效果:
- 父进程打印几行后被子进程"通知退出"
- 如果用
SIGKILL,父进程无法捕获,直接终止 - 如果用
SIGTERM,父进程可以注册signal(SIGTERM, handler)来优雅退出
🖥️ 第五步:kill 命令 vs kill() 函数
| 对比项 | kill 命令 |
kill() 函数 |
|---|---|---|
| 语法 | kill -信号 PID |
kill(PID, 信号) |
| 参数顺序 | 先信号,后 PID | 先 PID,后信号 |
| 进程组操作 | kill -9 -8514 |
kill(-8514, SIGKILL) |
| 底层实现 | 调用 kill() 系统调用 |
直接系统调用 |
✅ 记住口诀:命令是"-信号 PID",函数是"PID, 信号"
🧪 第六步:进程组实验(管道 + kill)
1. 创建管道进程组
cpp
cat | cat | cat | wc -l
2. 查看进程关系
cpp
ps ajx | grep cat
你会看到:
- 所有
cat和wc的 PGID(进程组ID)相同 - PGID = 第一个
cat的 PID
3. 一次性终止整个管道
cpp
kill -9 -<PGID> # 比如 kill -9 -8514
✅ 所有相关进程都会被杀死!
🖼️ 进程组关系图(文本版 + 说明)
bash
终端 Shell (bash)
│
└─┬─ 进程组 (PGID = 4269)
│
├─ PID=4269: cat ← 第一个进程,PGID = 自己的 PID
├─ PID=4270: cat ← 从上一个 cat 读取数据
├─ PID=4271: cat ← 继续传递
└─ PID=4272: wc -l ← 最终统计行数
bash# 1. 启动管道(保持运行) cat | cat | cat | wc -l & # 2. 查看进程关系 ps ajx | grep -E 'cat|wc' # 3. 输出示例(简化): # PPID PID PGID CMD # 3178 5001 5001 cat # 3178 5002 5001 cat # 3178 5003 5001 cat # 3178 5004 5001 wc -l
信号集操作
信号集概述
信号在产生后,并不会总是被立即处理。内核通过两个位图集合来管理信号的交付:
- 未决信号集(pending):记录哪些信号已经产生但尚未被处理;
- 阻塞信号集(mask):表示进程当前屏蔽(即暂缓处理)哪些信号。
当一个信号产生时,内核会立即将其在 pending 集合中对应的位置为 1 ,然后检查 mask 集合中该信号对应的位:
- 若 mask 中该位为 0(未屏蔽),则内核立即执行该信号的处理动作(默认行为、忽略或用户自定义处理函数);
- 若 mask 中该位为 1 (已屏蔽),则信号保持在 pending 状态,暂缓处理。
用户程序无法直接修改 pending 集合,但可以通过 sigprocmask() 函数修改自身的 mask 集合 。
当程序将某个信号在 mask 中的位从 1 改为 0(即解除屏蔽)时,内核会自动检查 pending 集合:
- 如果该信号在 pending 中为 1,则立即交付并处理该信号。
因此,sigprocmask() 的作用是通过调整阻塞掩码,间接控制被暂缓信号的处理时机。
bash
1. 信号产生(如 Ctrl+C → SIGINT)
↓
2. 内核设置 pending[SIGINT] = 1
↓
3. 内核检查 mask[SIGINT]:
├─ 若 mask[SIGINT] == 0 → 立即执行处理函数(或默认动作)
└─ 若 mask[SIGINT] == 1 → 保持 pending=1,不处理(暂缓)
↓
4. 后续某时刻,程序调用 sigprocmask(...) 将 mask[SIGINT] 设为 0
↓
5. 内核检测到:mask=0 且 pending=1 → 立即处理 SIGINT!
cpp
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
// 打印信号集中的信号(用于调试 pending 和 mask)
void print_sigset(const char *label, const sigset_t *set) {
printf("%s: ", label);
for (int sig = 1; sig <= 31; sig++) { // 通常 1~31 是标准信号
if (sigismember(set, sig)) {
printf("%d ", sig);
}
}
printf("\n");
}
// 信号处理函数(仅用于演示)
void handler(int sig) {
printf("\n[!] 收到信号 %d,正在处理...\n", sig);
}
int main() {
sigset_t mask, oldmask, pending;
// 1. 设置 SIGINT 的处理函数
signal(SIGINT, handler);
// 2. 初始化 mask:清空后添加 SIGINT
sigemptyset(&mask);
sigaddset(&mask, SIGINT); // 屏蔽 SIGINT
// 3. 阻塞 SIGINT
printf("🔒 正在屏蔽 SIGINT (信号 2)...\n");
sigprocmask(SIG_BLOCK, &mask, &oldmask);
// 4. 检查当前 pending(应该为空)
sigpending(&pending);
print_sigset("屏蔽后 pending", &pending);
printf("⏳ 请在 5 秒内多次按 Ctrl+C(信号不会立即处理)\n");
sleep(5);
// 5. 再次检查 pending(应该包含 SIGINT)
sigpending(&pending);
print_sigset("5秒后 pending", &pending);
printf("🔓 现在解除 SIGINT 屏蔽...\n");
// 6. 解除屏蔽 → pending 中的 SIGINT 会被立即处理
sigprocmask(SIG_UNBLOCK, &mask, NULL);
printf("✅ 程序继续运行(若收到 SIGINT 会调用 handler)\n");
pause(); // 等待任意信号(防止程序退出太快)
return 0;
}
信号集函数
sigset_t 是一个不透明的数据结构(通常是一个位图数组),用来表示"一组信号"。
- 每个信号对应 1 位(bit)
- 例如:
SIGINT = 2→ 第 2 位(从 1 开始计数) - 你不能直接操作它的内部 (比如不能
set[2] = 1) - 必须通过 5 个标准函数来操作
🔧 1️⃣ sigemptyset() ------ 清空信号集(初始化)
函数原型
cpp
int sigemptyset(sigset_t *set);
功能
将 set 中所有信号位清零(即:不包含任何信号)
为什么必须先调用?
-
sigset_t 变量在声明时内容是随机的 (就像 int x; 未初始化)
-
如果不先清空,直接
sigaddset()可能操作"脏数据",导致意外屏蔽其他信号
✅ 正确用法(黄金法则):
cpp
sigset_t set;
sigemptyset(&set); // 第一步:清空!
sigaddset(&set, SIGINT); // 第二步:添加你需要的
2️⃣ sigfillset() ------ 填满信号集(全选)
函数原型
cpp
int sigfillset(sigset_t *set);
功能
将 set 中所有标准信号位设为 1(即:包含所有信号)
典型用途
-
进入临界区(critical section)时,临时屏蔽所有信号,防止中断
-
配合
sigprocmask(SIG_SETMASK, ...)实现"原子操作"
示例
cpp
sigset_t set;
sigfillset(&set); // 所有信号都选中
sigprocmask(SIG_BLOCK, &set, NULL); // 全部屏蔽!
// ... 执行关键代码(不能被信号打断)...
sigprocmask(SIG_UNBLOCK, &set, NULL); // 恢复
⚠️ 注意:
SIGKILL和SIGSTOP无法被屏蔽,即使你在 set 中包含它们,内核也会忽略。
3️⃣ sigaddset() ------ 添加一个信号
函数原型
cpp
int sigaddset(sigset_t *set, int signum);
功能
将 signum 对应的位设为 1(加入集合)
参数说明
-
signum:信号编号,如SIGINT(2)、SIGTERM(15) -
必须是有效信号(通常 1~64)
示例
cpp
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT); // 屏蔽 Ctrl+C
sigaddset(&set, SIGQUIT); // 屏蔽 Ctrl+\
✅ 常用于构建"需要屏蔽的信号列表"
4️⃣ sigdelset() ------ 移除一个信号
函数原型
cpp
int sigdelset(sigset_t *set, int signum);
功能
将 signum 对应的位清零(从集合中移除)
典型场景
-
先全屏蔽(
sigfillset),再逐个放行某些信号 -
动态调整屏蔽策略
示例
cpp
sigset_t set;
sigfillset(&set); // 全屏蔽
sigdelset(&set, SIGUSR1); // 但允许 SIGUSR1 通过
sigprocmask(SIG_BLOCK, &set, NULL);
🔧 5️⃣ sigismember() ------ 检查信号是否在集合中
函数原型
cpp
int sigismember(const sigset_t *set, int signum);
返回值(⚠️ 特别注意!)
| 返回值 | 含义 |
|---|---|
| 1 | 信号 signum 在集合中 |
| 0 | 信号 signum 不在集合中 |
| -1 | 出错(如 signum 无效) |
正确用法
cpp
if (sigismember(&set, SIGINT) == 1) {
printf("SIGINT 在集合中(可能被屏蔽)\n");
} else if (sigismember(&set, SIGINT) == 0) {
printf("SIGINT 不在集合中\n");
} else {
perror("sigismember error");
}
🚫 常见错误:
cppif (sigismember(&set, SIGINT)) { ... } // ❌ 错!-1 也会进入 if!
| 函数 | 作用 | 成功返回 | 失败返回 | 注意事项 |
|---|---|---|---|---|
sigemptyset |
清空集合 | 0 | -1 | 必须先调用! |
sigfillset |
填满集合 | 0 | -1 | 无法屏蔽 SIGKILL/SIGSTOP |
sigaddset |
添加信号 | 0 | -1 | 信号编号需有效 |
sigdelset |
移除信号 | 0 | -1 | 信号不在集合中?无害 |
sigismember |
检查成员 | 1/0 | -1 | 返回值特殊! |
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void print_set(const char *name, sigset_t *set) {
printf("%s: ", name);
for (int i = 1; i <= 5; i++) { // 只看前5个信号
printf("%d", sigismember(set, i));
}
printf("...\n");
}
int main() {
sigset_t my_set;
printf("=== 信号集操作演示 ===\n");
// 1. 初始化空集合
sigemptyset(&my_set);
print_set("初始空集", &my_set); // 输出: 00000...
// 2. 添加信号
sigaddset(&my_set, SIGINT); // 信号2
sigaddset(&my_set, SIGQUIT); // 信号3
print_set("添加2,3后", &my_set); // 输出: 01100...
// 3. 检查成员
printf("检查SIGINT: %d\n", sigismember(&my_set, SIGINT)); // 1
printf("检查SIGHUP: %d\n", sigismember(&my_set, SIGHUP)); // 0
// 4. 移除信号
sigdelset(&my_set, SIGINT);
print_set("移除2后", &my_set); // 输出: 00100...
// 5. 填充所有信号
sigfillset(&my_set);
print_set("全填充", &my_set); // 输出: 11111...
return 0;
}
这样的也是所谓的信号处理就相当于红绿灯,每个进程相当于车子在启动。而红绿灯怎么执行则交给用户自定义。例如红绿灯只有红灯or只有绿灯,这里就相当于信号设置为忽略其他灯。而每个时刻红绿灯怎么闪烁由用户自定义,也就是信号怎么处理,mask和pending交给用户来间接操作。
信号捕捉
🌟 一句话定义(先给你答案)
信号捕捉(Signal Handling)就是:当某个信号(比如 Ctrl+C)发给你的程序时,你不让它执行默认动作(比如退出),而是运行你自己写的代码。
🔍 举个生活例子 🌰
想象你正在用微波炉热饭:
-
默认行为 :
如果你按"取消"(相当于发
SIGINT信号),微波炉立刻停转、开门(相当于程序退出)。 -
但你想自定义行为 :
你希望按"取消"时,先"叮"一声提醒你,再停转(比如防止烫伤)。
👉 这个"先叮一声"的动作,就是你捕捉了"取消"信号,并替换了默认行为!
🛠️ 技术实现:怎么捕捉?
步骤 1:写一个处理函数(Handler)
cpp
void my_handler(int sig) {
printf("你按了 Ctrl+C!但我还不想退出!\n");
// 可以在这里:保存文件、清理资源、设置退出标志等
}
步骤 2:向系统"注册"这个函数
cpp
#include <signal.h>
signal(SIGINT, my_handler); // 当 SIGINT 来时,调用 my_handler
结果:
- 用户按
Ctrl+C - 程序不会退出
- 而是打印那句话,然后继续运行
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h> // for pause()
void sig_catch(int signum) {
if (signum == SIGINT)
printf("catch you! %d\n", signum);
else if (signum == SIGQUIT)
printf("哈哈,%d,你被我抓住了\n", signum);
// 不要处理 SIGKILL!
}
int main() {
signal(SIGINT, sig_catch); // Ctrl+C
signal(SIGQUIT, sig_catch); // Ctrl+\
printf("等待信号... (Ctrl+C 或 Ctrl+\\)\n");
pause(); // 挂起进程,等待信号(不消耗 CPU)
return 0;
}
Ctrl+C→ 打印"catch you! 2",程序继续运行Ctrl+\→ 打印"哈哈,3,你被我抓住了",程序继续运行kill -9 <pid>→ 进程立即终止,无任何打印
更推荐sigaction函数
cpp
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
检查或修改指定信号的设置(或同时执行这两种操作)。
参数:
signum:要操作的信号。
act: 要设置的对信号的新处理方式(传入参数)。
oldact:原来对信号的处理方式(传出参数)。
如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。
返回值:
成功:0
失败:-1
struct sigaction结构体:
cpp
struct sigaction {
void(*sa_handler)(int); //旧的信号处理函数指针
void(*sa_sigaction)(int, siginfo_t *, void *); //新的信号处理函数指针
sigset_t sa_mask; //信号阻塞集
int sa_flags; //信号处理的方式
void(*sa_restorer)(void); //已弃用
};
标准使用步骤(4 步法)
第 1 步:定义你的信号处理函数
cpp
void my_handler(int sig) {
// 注意:这里只能调用"异步信号安全"函数!
write(STDOUT_FILENO, "收到信号!\n", 12); // ✅ 安全
// printf("..."); // ❌ 危险!不要用
}
第 2 步:初始化 struct sigaction
cpp
struct sigaction sa;
memset(&sa, 0, sizeof(sa)); // 清零(重要!)
sa.sa_handler = my_handler; // 设置处理函数
sigemptyset(&sa.sa_mask); // 初始化 mask(通常先清空)
第 3 步(可选):设置额外屏蔽信号
cpp
// 如果你希望在 handler 执行期间,也屏蔽 SIGTERM:
sigaddset(&sa.sa_mask, SIGTERM);
第 4 步:设置标志位(最常用)
cpp
sa.sa_flags = 0; // 默认行为:屏蔽自身信号,不自动恢复
// 如果你想要"执行完 handler 后自动恢复为默认行为":
// sa.sa_flags = SA_RESETHAND;
// 如果你需要更详细的信号信息(如发信号的进程 PID):
// sa.sa_flags = SA_SIGINFO;
// 此时要用 sa_sigaction 而不是 sa_handler
第 5 步:注册信号
cpp
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
static volatile sig_atomic_t keep_running = 1;
void signal_handler(int sig) {
// 使用 sig_atomic_t + volatile 保证原子读写
keep_running = 0;
}
int main() {
struct sigaction sa;
// 1. 清零结构体
memset(&sa, 0, sizeof(sa));
// 2. 设置 handler
sa.sa_handler = signal_handler;
// 3. 设置 mask(这里不额外屏蔽其他信号)
sigemptyset(&sa.sa_mask);
// 4. 设置标志(默认即可)
sa.sa_flags = 0; // 自动屏蔽 SIGINT 自身,防止重入
// 5. 注册信号
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction SIGINT");
exit(1);
}
if (sigaction(SIGTERM, &sa, NULL) == -1) {
perror("sigaction SIGTERM");
exit(1);
}
printf("服务启动,按 Ctrl+C 或 kill 终止...\n");
while (keep_running) {
sleep(1);
write(STDOUT_FILENO, ".", 1); // 安全输出
}
printf("\n正在清理资源...\n");
// 这里可以关闭文件、断开连接等
printf("服务已安全退出。\n");
return 0;
}
XML信号产生 ↓ 内核检查该信号是否被 mask 阻塞? ├─ 是 → 记入 pending,**暂不递送** └─ 否 → **递送信号** ↓ 执行该信号当前的"处置方式"(disposition) ├─ SIG_DFL → 执行默认动作(如退出) ├─ SIG_IGN → 直接丢弃 └─ 自定义函数 → **调用你的 handler** ↓ **跳过默认动作!**