详细介绍:linux 信号

一、什么是信号(Signal)?

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

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


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

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

组合键信号名信号编号作用
Ctrl + CSIGINT2中断/终止进程(最常用)
Ctrl + \SIGQUIT3退出 + 可能生成 core 文件(调试用)
Ctrl + ZSIGTSTP20暂停进程(可恢复,用 fg 或 bg

实际应用

  • ping 命令无限发包,按 Ctrl+C 停止
  • 程序卡死,用 Ctrl+\ 强制退出并看 core 文件

⚠️ 易错点

  • Ctrl+Z 不是“退出”,而是“暂停”!进程还在后台。
  • SIGQUIT(3号)比 SIGINT(2号)更“狠”,会尝试生成 core。

2️⃣ 系统调用产生(程序主动发信号)

函数产生信号编号说明
alarm(n)SIGALRM14n 秒后发信号(用于定时)
abort()SIGABRT6异常终止,强制生成 core 文件
raise(sig)指定信号-自己发信号(如 raise(SIGTERM)

特点

  • 是程序主动行为,不是被动响应
  • alarm() 常用于超时控制(如网络请求超时)
  • abort() 常用于断言失败(assert 失败时调用)

⚠️ 注意:

  • 每个进程只有一个 alarm 定时器,再次调用会覆盖
  • alarm(0) 可取消定时器,并返回剩余秒数

第一步:先搞清楚「什么是系统调用产生信号」?

✅ 核心概念:

系统调用产生信号 = 程序自己主动“发信号给自己或别人”

这和「用户按 Ctrl+C」或「程序崩溃」不同——那是被动收到信号。
而这里,是程序主动调用函数,说:“嘿,给我自己(或别人)发个信号!

第二步:先学 alarm(n) —— “闹钟信号”

函数原型:

#include 
unsigned int alarm(unsigned int seconds);

功能:

  • 设置一个闹钟n 秒后,系统会向当前进程发送 SIGALRM(14号信号)

  • 如果之前已经设过闹钟,新的 alarm(n) 会覆盖旧的

  • 如果你调用 alarm(0),表示取消闹钟,并返回之前闹钟还剩多少秒

举个生活例子:

你正在煮泡面,设了个 3 分钟闹钟。
但 1 分钟后你突然想改成 5 分钟——于是你重新设一个 5 分钟闹钟,旧的就没了。
如果你突然不想煮了,就把闹钟关掉(alarm(0)),顺便看看还剩几分钟。

代码演示:

#include 
#include 
#include 
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() —— “我崩溃了!”

函数原型:

#include 
void abort(void);

功能:

  • 立即向自己发送 SIGABRT(6号信号)

  • 强制终止进程

  • 通常会生成 core 文件(用于调试)

  • 即使你捕获了 SIGABRT,abort() 仍会确保进程退出

什么时候用?

  • 程序遇到严重错误,无法继续运行

  • C 语言中的 assert() 宏在条件失败时就会调用 abort()

例子:

#include 
#include 
#include 
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) —— “我自己发个信号”

函数原型:

#include 
int raise(int sig);

功能:

  • 当前进程自己发送一个指定信号

  • 等价于:kill(getpid(), sig)

用途:

  • 主动触发信号处理逻辑(比如模拟错误)

  • 在特定条件下“优雅退出”

例子:

#include 
#include 
#include 
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(或其他如 SIGVTALRMSIGPROF

✅ 与系统调用的关系:

  • 和 alarm() 功能重叠,但 setitimer 更灵活(可周期性触发)
  • 都属于“软件主动触发”,不是硬件或用户行为

第一步:为什么需要 setitimer()alarm() 不够用吗?

✅ alarm() 的局限:

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

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

  3. 只能产生 SIGALRM

如果你想:

  • 每 500 毫秒打印一次 “心跳”

  • 做一个高精度的性能分析工具

  • 实现周期性任务(比如每秒刷新)

那么 alarm() 就不够用了!


⏱️ 第二步:认识 setitimer() —— 高级定时器

函数原型:

#include 
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

这是设置定时器的关键结构体:

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 秒打印一次

#include 
#include 
#include 
#include 
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 秒一行):

定时器已启动,每0.5秒滴答一次...
滴答!收到信号 14
滴答!收到信号 14
滴答!收到信号 14
...

Ctrl+C 终止程序。


第六步:如何停止定时器?

只需把 it_valueit_interval 都设为 0:

struct itimerval stop = {{0, 0}, {0, 0}};
setitimer(ITIMER_REAL, &stop, NULL);

或者更简单:

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 或内存硬件异常引发,内核代为发送信号:

异常类型信号名编号原因
段错误SIGSEGV11访问非法内存(如空指针、越界)
除零 / 浮点错误SIGFPE85 / 0、浮点运算异常("F" = float)
总线错误SIGBUS7内存地址未对齐(如某些架构要求 4 字节对齐)

关键点

  • 默认动作:终止进程 + 可能生成 core 文件
  • 这是 C/C++ 程序崩溃的主要原因
  • 无法用 signal() 忽略 SIGKILL/SIGSTOP,但这些硬件信号可以被捕获(不过一般不建议,应修复 bug)

第一步:什么是“硬件异常产生信号”?

✅ 核心概念:

当你的程序执行了非法的硬件操作(比如访问不存在的内存、除以零),CPU 会立刻检测到异常,然后通知操作系统(内核)。
内核不会直接杀掉你,而是向你的进程发送一个对应的信号(如 SIGSEGV),让你有机会处理(或默认终止)。

这和“用户按 Ctrl+C”完全不同——

  • 用户信号:外部主动中断

  • 硬件异常信号:程序自己“作死”触发的,是 bug 的直接体现


第二步:逐个击破三种常见硬件异常


1️⃣ 段错误(Segmentation Fault)→ SIGSEGV(11号)

❓ 什么是“段错误”?

  • 你的程序试图访问它无权访问的内存地址

  • “段”是内存管理中的一个概念(现代系统用页,但名字沿用下来)。

常见原因:

// 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(浮点异常)

  • 虽然名字叫“浮点”,但整数除零也会触发

常见原因:

// 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):

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️⃣ 函数原型

#include 
#include 
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 的特殊用法(面试常问!)

if (kill(pid, 0) == 0) {
    printf("进程 %d 存在且我有权限操作\n", pid);
} else {
    printf("进程不存在或无权限\n");
}

不发信号,只检查进程是否存在 + 权限是否足够

⚠️ 第三步:权限规则(为什么 kill 有时失败?)

普通用户只能向自己创建的进程发信号(除非是 root)。

具体规则:

  • 发送者的 real/effective UID 必须等于接收者的 real/saved set-user-ID
  • 特权进程(如 root)可以发给任何进程

❌ 你不能随便 kill 别人的进程(安全机制)


第四步:代码实战 —— 子进程发信号给父进程

#include 
#include 
#include 
#include 
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 -信号 PIDkill(PID, 信号)
参数顺序先信号,后 PID先 PID,后信号
进程组操作kill -9 -8514kill(-8514, SIGKILL)
底层实现调用 kill() 系统调用直接系统调用

✅ 记住口诀:命令是“-信号 PID”,函数是“PID, 信号”

第六步:进程组实验(管道 + kill)

1. 创建管道进程组

cat | cat | cat | wc -l

2. 查看进程关系

ps ajx | grep cat

你会看到:

  • 所有 cat 和 wc 的 PGID(进程组ID)相同
  • PGID = 第一个 cat 的 PID

3. 一次性终止整个管道

kill -9 -   # 比如 kill -9 -8514

✅ 所有相关进程都会被杀死!

️ 进程组关系图(文本版 + 说明)

终端 Shell (bash)
     │
     └─┬─ 进程组 (PGID = 4269)
       │
       ├─ PID=4269: cat          ← 第一个进程,PGID = 自己的 PID
       ├─ PID=4270: cat          ← 从上一个 cat 读取数据
       ├─ PID=4271: cat          ← 继续传递
       └─ PID=4272: wc -l        ← 最终统计行数
# 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() 的作用是通过调整阻塞掩码,间接控制被暂缓信号的处理时机

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!
#include 
#include 
#include 
#include 
// 打印信号集中的信号(用于调试 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() —— 清空信号集(初始化)

函数原型

int sigemptyset(sigset_t *set);

功能

set所有信号位清零(即:不包含任何信号)

为什么必须先调用?

  • sigset_t 变量在声明时内容是随机的(就像 int x; 未初始化)

  • 如果不先清空,直接 sigaddset() 可能操作“脏数据”,导致意外屏蔽其他信号

✅ 正确用法(黄金法则):

sigset_t set;
sigemptyset(&set);  // 第一步:清空!
sigaddset(&set, SIGINT);  // 第二步:添加你需要的

2️⃣ sigfillset() —— 填满信号集(全选)

函数原型

int sigfillset(sigset_t *set);

功能

set所有标准信号位设为 1(即:包含所有信号)

典型用途

  • 进入临界区(critical section)时,临时屏蔽所有信号,防止中断

  • 配合 sigprocmask(SIG_SETMASK, ...) 实现“原子操作”

示例

sigset_t set;
sigfillset(&set);               // 所有信号都选中
sigprocmask(SIG_BLOCK, &set, NULL);  // 全部屏蔽!
// ... 执行关键代码(不能被信号打断)...
sigprocmask(SIG_UNBLOCK, &set, NULL); // 恢复

⚠️ 注意:SIGKILLSIGSTOP无法被屏蔽,即使你在 set 中包含它们,内核也会忽略。

3️⃣ sigaddset() —— 添加一个信号

函数原型

int sigaddset(sigset_t *set, int signum);

功能

signum 对应的位设为 1(加入集合)

参数说明

  • signum:信号编号,如 SIGINT(2)、SIGTERM(15)

  • 必须是有效信号(通常 1~64)

示例

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);    // 屏蔽 Ctrl+C
sigaddset(&set, SIGQUIT);   // 屏蔽 Ctrl+\

✅ 常用于构建“需要屏蔽的信号列表”

4️⃣ sigdelset() —— 移除一个信号

函数原型

int sigdelset(sigset_t *set, int signum);

功能

signum 对应的位清零(从集合中移除)

典型场景

  • 先全屏蔽(sigfillset),再逐个放行某些信号

  • 动态调整屏蔽策略

示例

sigset_t set;
sigfillset(&set);           // 全屏蔽
sigdelset(&set, SIGUSR1);   // 但允许 SIGUSR1 通过
sigprocmask(SIG_BLOCK, &set, NULL);

5️⃣ sigismember() —— 检查信号是否在集合中

函数原型

int sigismember(const sigset_t *set, int signum);

返回值(⚠️ 特别注意!)

返回值

含义

1

信号 signum 在集合中

0

信号 signum 不在集合中

-1

出错(如 signum 无效)

正确用法

if (sigismember(&set, SIGINT) == 1) {
    printf("SIGINT 在集合中(可能被屏蔽)\n");
} else if (sigismember(&set, SIGINT) == 0) {
    printf("SIGINT 不在集合中\n");
} else {
    perror("sigismember error");
}

常见错误:

if (sigismember(&set, SIGINT)) { ... }  // ❌ 错!-1 也会进入 if!

函数

作用

成功返回

失败返回

注意事项

sigemptyset

清空集合

0

-1

必须先调用!

sigfillset

填满集合

0

-1

无法屏蔽 SIGKILL/SIGSTOP

sigaddset

添加信号

0

-1

信号编号需有效

sigdelset

移除信号

0

-1

信号不在集合中?无害

sigismember

检查成员

1/0

-1

返回值特殊!

#include 
#include 
#include 
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)

void my_handler(int sig) {
    printf("你按了 Ctrl+C!但我还不想退出!\n");
    // 可以在这里:保存文件、清理资源、设置退出标志等
}

步骤 2:向系统“注册”这个函数

#include 
signal(SIGINT, my_handler);  // 当 SIGINT 来时,调用 my_handler

结果:

  • 用户按 Ctrl+C
  • 程序不会退出
  • 而是打印那句话,然后继续运行
#include 
#include 
#include   // 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函数

#include 
​
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
    检查或修改指定信号的设置(或同时执行这两种操作)。
​
参数:
    signum:要操作的信号。
    act:   要设置的对信号的新处理方式(传入参数)。
    oldact:原来对信号的处理方式(传出参数)。
​
    如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。
​
返回值:
    成功:0
    失败:-1

struct sigaction结构体:

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 步:定义你的信号处理函数

void my_handler(int sig) {
    // 注意:这里只能调用“异步信号安全”函数!
    write(STDOUT_FILENO, "收到信号!\n", 12);  // ✅ 安全
    // printf("...");  // ❌ 危险!不要用
}

第 2 步:初始化 struct sigaction

struct sigaction sa;
memset(&sa, 0, sizeof(sa));        // 清零(重要!)
sa.sa_handler = my_handler;        // 设置处理函数
sigemptyset(&sa.sa_mask);          // 初始化 mask(通常先清空)

第 3 步(可选):设置额外屏蔽信号

// 如果你希望在 handler 执行期间,也屏蔽 SIGTERM:
sigaddset(&sa.sa_mask, SIGTERM);

第 4 步:设置标志位(最常用)

sa.sa_flags = 0;  // 默认行为:屏蔽自身信号,不自动恢复
// 如果你想要“执行完 handler 后自动恢复为默认行为”:
// sa.sa_flags = SA_RESETHAND;
// 如果你需要更详细的信号信息(如发信号的进程 PID):
// sa.sa_flags = SA_SIGINFO;
// 此时要用 sa_sigaction 而不是 sa_handler

第 5 步:注册信号

if (sigaction(SIGINT, &sa, NULL) == -1) {
    perror("sigaction");
    exit(1);
}
#include 
#include 
#include 
#include 
#include 
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;
}
信号产生
   ↓
内核检查该信号是否被 mask 阻塞?
   ├─ 是 → 记入 pending,**暂不递送**
   └─ 否 → **递送信号**
            ↓
            执行该信号当前的“处置方式”(disposition)
               ├─ SIG_DFL → 执行默认动作(如退出)
               ├─ SIG_IGN → 直接丢弃
               └─ 自定义函数 → **调用你的 handler**
                                 ↓
                          **跳过默认动作!**

posted on 2026-02-02 13:00  ljbguanli  阅读(0)  评论(0)    收藏  举报