linux 信号

🌟 一、什么是信号(Signal)?

  • 信号是 Linux/Unix 系统中进程间通信(IPC)的一种机制
  • 它是一种异步通知 :当某个事件发生时(如用户按键、硬件错误、定时器到期),系统会向进程"发送一个信号",进程可以选择:
    • 忽略
    • 执行默认动作(如终止、暂停)
    • 自定义处理函数(捕获信号)

💡 类比:就像你正在写作业,突然手机响了(信号来了)------你可以挂掉(忽略)、接电话(处理)、或者让它一直响到自动挂断(默认动作)。


🧩 二、信号的五种产生方式(核心!)

1️⃣ 按键产生(来自终端)

组合键 信号名 信号编号 作用
Ctrl + C SIGINT 2 中断/终止进程(最常用)
Ctrl + \ SIGQUIT 3 退出 + 可能生成 core 文件(调试用)
Ctrl + Z SIGTSTP 20 暂停进程 (可恢复,用 fgbg

实际应用

  • 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() 的局限:

  1. 只能精确到秒(不能设 0.5 秒)

  2. 只能单次触发(响一次就没了,不能自动重复)

  3. 只能产生 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_valueit_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() 是"专业定时器"


🧠 第八步:实际应用场景

  1. 游戏/动画帧率控制:每 16ms 刷新一次(≈60 FPS)

  2. 网络超时重传:每 200ms 检查一次是否收到回复

  3. 性能监控工具 :用 ITIMER_PROF 定期采样 CPU 使用情况

  4. 守护进程心跳:定期上报"我还活着"


✅ 最后:一句话总结

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

你会看到:

  • 所有 catwcPGID(进程组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); // 恢复

⚠️ 注意:SIGKILLSIGSTOP 无法被屏蔽,即使你在 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");
}

🚫 常见错误:

cpp 复制代码
if (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**
                                 ↓
                          **跳过默认动作!**
相关推荐
初学者52135 小时前
服务器映射外网端口22连接不上,局域网能通
运维·服务器·ubuntu
一周困⁸天.5 小时前
Keepalived双机热备
linux·运维·keepalived
漏刻有时5 小时前
宝塔面板:基于 top 命令的服务器运行状态深度分析
运维·服务器
Ponp_6 小时前
Ubuntu 22.04 + ROS 2 Humble实现YOLOV5目标检测实时流传输(Jetson NX与远程PC通信)
linux·运维·yolo
亿坊电商8 小时前
PHP后端项目中多环境配置管理:开发、测试、生产的优雅解决方案!
服务器·数据库·php
ha20428941949 小时前
Linux操作系统学习之---线程池
linux·c++·学习
gfdgd xi10 小时前
GXDE 内核管理器 1.0.1——修复bug、支持loong64
android·linux·运维·python·ubuntu·bug
deng-c-f11 小时前
Linux C/C++ 学习日记(43):dpdk(六):dpdk实现发包工具:UDP的发包,TCP的泛洪攻击
linux·dpdk·泛洪
我命由我1234511 小时前
Derby - Derby 服务器(Derby 概述、Derby 服务器下载与启动、Derby 连接数据库与创建数据表、Derby 数据库操作)
java·运维·服务器·数据库·后端·java-ee·后端框架