本节重点:
- 掌握Linux信号的基本概念
- 掌握信号产生的一般方式
- 理解信号递达和阻塞的概念,原理。
- 掌握信号捕捉的一般方式。
- 重新了解可重入函数的概念。
- 了解竞态条件的情景和处理方式
- 了解SIGCHLD信号, 重新编写信号处理函数的一般处理机制
一.信号入门
**1.**生活角度的信号
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能"识别快递"
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。
那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成"在合适的时候去取"。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你"记住了有一个快递要去取"
当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话.
**2.**技术应用角度的信号
- 用户输入命令,在Shell下启动一个前台进程。
- 用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程.
- 前台进程因为收到信号,进而引起进程退出.
bash
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
int main()
{
while(1){
printf("I am a process, I am waiting signal!\n");
sleep(1);
}
}
[hb@localhost code_test]$ ./sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C
[hb@localhost code_test]$
请将生活例子和 Ctrl-C 信号处理过程相结合,解释一下信号处理过程
进程就是你,操作系统就是快递员,信号就是快递
**3.**注意
- Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步
(Asynchronous)的。
**4.**信号概念
信号是Linux 进程间通信(IPC)的一种异步通信机制 ,也是内核向进程传递事件通知的核心方式,本质是内核给进程发送的软中断------ 进程无需主动轮询,内核会在合适的时机打断进程的正常执行流程,触发信号相关的处理逻辑。
简单来说,信号是进程之间事件异步通知的一种方式,属于软中断。
5.用kill -l****命令可以察看系统定义的信号列表

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define
SIGINT 2
编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

**6.**信号处理常见方式概览
(sigaction函数稍后详细介绍),可选的处理动作有以下三种:
- 忽略此信号。
- 执行该信号的默认处理动作。
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。
二.产生信号
**1.**通过终端按键产生信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
Core Dump
Core Dump(核心转储)是 Linux/Unix 系统中进程异常终止时,内核将进程的内存镜像、寄存器状态、调用栈等核心数据保存到磁盘文件的机制,本质是进程 "崩溃现场" 的完整快照。它是定位程序崩溃(如段错误、非法指令、内存越界)的 "终极工具",能让开发者在事后还原崩溃瞬间的进程状态,而非仅依赖运行时的错误提示。
一、Core Dump 的核心本质
当进程因未捕获的信号 (如段错误SIGSEGV、除零错误SIGFPE、非法内存访问SIGBUS等)异常终止时,内核会触发 Core Dump:
- 内核暂停进程的所有执行流,冻结当前的内存布局(堆、栈、数据段、代码段);
- 将进程的核心数据(内存、寄存器、程序计数器、调用栈、文件描述符等)写入磁盘文件(默认文件名为
core或core.<pid>); - 进程按信号的默认行为终止(如退出)。
简单来说:Core Dump = 进程崩溃瞬间的 "内存快照 + 运行上下文",是调试崩溃问题的 "黑匣子"。
二、Core Dump 的触发条件
并非所有进程异常都会生成 Core Dump,需满足系统配置 + 信号类型 + 进程权限三重条件:
1. 系统层面:开启 Core Dump 限制
Linux 默认会限制 Core Dump 的生成(避免磁盘被大量核心文件占满),需先通过命令开启:
bash
# 查看当前 Core Dump 大小限制(0 表示禁用)
ulimit -c
# 临时开启:允许生成任意大小的 Core Dump(仅当前终端有效)
ulimit -c unlimited
# 永久开启:修改 /etc/security/limits.conf,添加以下行(需重启生效)
* soft core unlimited
* hard core unlimited
2. 信号层面:仅特定信号触发
只有进程因带 "核心转储" 属性的信号终止时,才会生成 Core Dump,常见触发信号:
| 信号编号 | 信号名 | 触发场景 | 是否生成 Core Dump |
|---|---|---|---|
| 4 | SIGILL |
非法指令(如错误的机器码) | ✅ |
| 6 | SIGABRT |
进程主动调用abort() |
✅ |
| 8 | SIGFPE |
浮点异常(如整数除零) | ✅ |
| 11 | SIGSEGV |
段错误(非法内存访问) | ✅ |
| 7 | SIGBUS |
总线错误(内存对齐错误) | ✅ |
| 10 | SIGUSR1 |
用户自定义信号(默认行为) | ❌(需自定义) |
| 9 | SIGKILL |
强制终止进程 | ❌(不可捕获 / 转储) |
| 2 | SIGINT |
Ctrl+C 中断进程 | ❌(默认行为不转储) |
关键:
SIGKILL(9)和SIGSTOP(19)永远不会触发 Core Dump(内核强制终止,不保留现场);SIGINT(2)默认也不触发,需通过signal()自定义处理并调用abort()才会生成。
3. 进程权限层面
- 进程的工作目录需有写入权限(Core Dump 文件默认生成在进程当前工作目录);
- 进程不能是
setuid/setgid权限的程序(系统安全限制,避免普通用户获取特权进程的内存数据); - Core Dump 文件的大小不能超过磁盘剩余空间。
三、Core Dump 文件的生成路径与命名
1. 默认行为
- 路径:进程的当前工作目录 (可通过
pwd查看,或代码中getcwd()获取); - 命名:默认文件名是
core,部分系统会自动添加 PID,如core.1234(1234 是崩溃进程的 PID)。
2. 自定义命名规则(推荐)
通过修改内核参数,让 Core Dump 文件按 "进程名 + PID + 时间" 命名,方便管理:
bash
# 临时设置(仅当前系统有效)
echo "/var/core/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
# 永久设置(修改 /etc/sysctl.conf,添加以下行,执行 sysctl -p 生效)
kernel.core_pattern = /var/core/core-%e-%p-%t
参数说明:
%e:进程的可执行文件名;%p:进程 PID;%t:崩溃时间戳(秒级);%u:进程所属用户 ID;%g:进程所属组 ID。
示例:core-myapp-1234-1738321200 表示myapp进程(PID 1234)在时间戳 1738321200 时崩溃生成的核心文件。
四、如何使用 Core Dump 调试崩溃问题
Core Dump 文件本身是二进制数据,需通过调试器(如gdb)解析,核心步骤:
前提:编译程序时保留调试信息
必须在编译时添加-g参数(保留源码行号、变量名等调试信息),否则 gdb 无法关联源码:
bash
# 编译时添加 -g,生成带调试信息的可执行文件
gcc -o crash_demo crash_demo.c -g
步骤 1:触发 Core Dump
运行程序,使其崩溃并生成核心文件:
bash
# 先开启 Core Dump 限制
ulimit -c unlimited
# 运行程序(示例:模拟段错误)
./crash_demo
# 查看生成的核心文件
ls -l core* # 会看到 core 或 core.xxx 文件
步骤 2:用 gdb 加载 Core Dump 文件
bash
# 格式:gdb <可执行文件> <core文件>
gdb ./crash_demo core.1234
步骤 3:gdb 核心调试命令
加载后,通过以下命令还原崩溃现场:
| 命令 | 功能 |
|---|---|
bt / backtrace |
打印调用栈,定位崩溃发生的函数 / 行号 |
frame <n> |
切换到调用栈的第 n 层(n 从 0 开始) |
info locals |
查看当前栈帧的局部变量值 |
print <var> |
打印指定变量的值(如 print ptr) |
list |
显示崩溃行附近的源码 |
info registers |
查看崩溃时的寄存器状态 |
调试示例(模拟段错误)
假设有以下崩溃代码crash_demo.c:
cpp
#include <stdio.h>
void crash_func() {
// 非法访问空指针,触发 SIGSEGV
int *ptr = NULL;
*ptr = 10; // 第 6 行:崩溃位置
}
int main() {
crash_func(); // 第 10 行
return 0;
}
编译运行后生成core.1234,用 gdb 调试:
bash
gdb ./crash_demo core.1234
# 输入 bt 查看调用栈
(gdb) bt
#0 0x000055f8b77a9149 in crash_func () at crash_demo.c:6
#1 0x000055f8b77a9162 in main () at crash_demo.c:10
# 输入 list 查看崩溃行源码
(gdb) list
1 #include <stdio.h>
2
3 void crash_func() {
4 // 非法访问空指针,触发 SIGSEGV
5 int *ptr = NULL;
6 *ptr = 10; // 第 6 行:崩溃位置
7 }
8
9 int main() {
10 crash_func(); // 第 10 行
11 return 0;
12 }
从调用栈可直接定位:崩溃发生在crash_func()的第 6 行,是访问空指针导致的段错误。
五、Core Dump 的常见应用场景
- 定位生产环境崩溃问题:生产服务器程序崩溃时,无法实时调试,Core Dump 是唯一能还原现场的手段;
- 复现偶发崩溃:部分崩溃(如多线程竞态、内存越界)难以复现,Core Dump 可保存崩溃瞬间的所有状态;
- 分析内存泄漏 / 越界 :结合 gdb 的
x(查看内存)、p(打印变量)命令,定位非法内存操作; - 调试第三方程序:无源码时,可通过 Core Dump 分析崩溃的函数调用栈(虽无行号,但能看到崩溃的函数名)。
六、Core Dump 的注意事项
1. 安全风险
Core Dump 文件会包含进程的敏感数据(如密码、密钥、内存中的业务数据),调试完成后需及时删除,避免泄露;生产环境可将 Core Dump 目录设置为仅管理员可访问。
2. 磁盘占用
Core Dump 文件大小通常等于进程占用的内存大小(如 1GB 内存的进程,Core Dump 文件约 1GB),需定期清理旧的核心文件。
3. 容器 / 虚拟化环境
Docker、K8s 等容器环境默认禁用 Core Dump,需手动配置:
- 容器内执行
ulimit -c unlimited; - 挂载宿主机目录到容器,让 Core Dump 文件生成在宿主机(避免容器销毁后文件丢失)。
4. 多线程程序调试
多线程程序崩溃时,gdb 中可通过info threads查看所有线程,thread <n>切换到崩溃线程,再用bt查看该线程的调用栈。
七、总结
- Core Dump 是进程崩溃时的 "内存快照",由内核生成,需先通过
ulimit -c unlimited开启系统限制; - 仅特定信号(如
SIGSEGV、SIGFPE)会触发 Core Dump,SIGKILL永远不会; - 调试 Core Dump 需编译程序时加
-g,通过gdb <可执行文件> <core文件>定位崩溃位置; - 生产环境建议自定义 Core Dump 命名规则,并做好文件的安全与磁盘管理。
**2.**调用系统函数向进程发信号
首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号.

4568是test进程的id。之所以要再次回车才显示 Segmentation fault ,是因为在4568进程终止掉 之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用 户的输入交错在一起,所以等用户输入命令之后才显示。
指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGSEGV 4568 或 kill -11
4568 , 11是信号SIGSEGV的编号。以往遇 到的段错误都是由非法内存访问产生的,而这个程序本身没错, 给它发SIGSEGV也能产生段错误。
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
bash
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。
abort函数使当前进程接收到信号而异常终止。
bash
#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
kill、raise、abort 函数深度解析
这三个函数都是 Linux 中用于发送信号的核心接口,但适用场景、作用对象、底层逻辑差异显著。我会从函数定义、使用场景、底层原理、典型示例四个维度,帮你彻底理清它们的区别和用法。
一、核心对比总览
先通过表格快速掌握三者的核心差异:
| 特性 | kill () 函数 | raise () 函数 | abort () 函数 |
|---|---|---|---|
| 作用对象 | 任意进程 / 进程组(指定 PID) | 仅当前进程(自身) | 仅当前进程(自身) |
| 发送信号 | 可发送任意信号(除 SIGKILL/SIGSTOP 外可捕获) | 可发送任意信号 | 固定发送 SIGABRT(6) 信号 |
| 底层实现 | 直接调用系统调用 sys_kill |
封装 kill(getpid(), signo) |
封装 raise(SIGABRT) + 兜底逻辑 |
| 是否可被忽略 | 信号可被捕获 / 忽略(除 SIGKILL/SIGSTOP) | 信号可被捕获 / 忽略 | SIGABRT 可捕获,但最终必终止进程 |
| 典型场景 | 进程间发送信号(如主进程控制子进程) | 进程向自身发送信号 | 进程主动触发崩溃(生成 Core Dump) |
二、kill () 函数:进程间信号发送的核心
1. 函数定义
kill() 是最基础的信号发送接口,允许一个进程向另一个进程 / 进程组发送任意信号,原型如下:
cpp
#include <signal.h>
// 返回值:成功返回 0,失败返回 -1(设置 errno)
int kill(pid_t pid, int sig);
2. 参数详解
(1)pid(目标对象):核心是 pid 的取值规则,决定信号发给谁
| pid 取值 | 含义 | ||
|---|---|---|---|
| pid > 0 | 信号发送给 PID 为该值的单个进程 (最常用,如 kill(1234, SIGINT)) |
||
| pid = 0 | 信号发送给当前进程所属进程组的所有进程 | ||
| pid = -1 | 信号发送给当前进程有权限发送的所有进程(除 init 进程) | ||
| pid < -1 | 信号发送给进程组 ID 为 ` | pid | ` 的所有进程(如 pid=-100,发给组 100) |
(2)sig(信号编号)
- 可传入任意合法信号编号(如
SIGINT(2)、SIGKILL(9)、SIGRTMIN+6); - 若
sig=0:不发送任何信号,但会检查目标 pid 是否存在(常用于检测进程是否存活)。
3. 关键注意事项
- 权限限制:普通用户只能向同属一个用户的进程发送信号;root 用户可向任意进程发送信号;
- 不可拦截信号 :
SIGKILL(9)和SIGSTOP(19)无法被捕获 / 忽略,kill发送后内核强制执行; - 错误码 :常见
ESRCH(pid 不存在)、EPERM(无权限发送信号)。
4. 典型示例
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork 失败");
exit(1);
}
if (pid == 0) {
// 子进程:无限循环,等待信号
while (1) {
printf("子进程运行中(PID:%d)\n", getpid());
sleep(1);
}
} else {
// 父进程:3 秒后向子进程发送 SIGINT 信号
sleep(3);
printf("父进程向子进程(%d)发送 SIGINT 信号\n", pid);
if (kill(pid, SIGINT) == -1) {
perror("kill 失败");
exit(1);
}
// 等待子进程退出
wait(NULL);
printf("子进程已退出\n");
}
return 0;
}
三、raise () 函数:进程向自身发送信号
1. 函数定义
raise() 是简化版的 kill(),仅用于当前进程向自身发送信号,原型如下:
cpp
#include <signal.h>
// 返回值:成功返回 0,失败返回非 0
int raise(int sig);
2. 底层实现
raise() 本质是对 kill() 的封装,等价于:
cpp
kill(getpid(), sig);
(部分系统还会处理线程场景,等价于 pthread_kill(pthread_self(), sig))
3. 关键特点
- 使用简单:无需指定 PID,仅需传入信号编号,适合进程自我中断 / 通知;
- 信号处理:发送的信号遵循普通信号规则(可捕获、可忽略、可默认处理);
- 返回值 :与
kill()不同,失败返回非 0(而非 -1),且不设置errno(可移植性更好)。
4. 典型示例
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
// 信号处理函数
void sig_handler(int signo) {
printf("进程捕获到信号 %d\n", signo);
}
int main() {
// 注册 SIGUSR1 信号的处理函数
signal(SIGUSR1, sig_handler);
printf("进程向自身发送 SIGUSR1 信号\n");
// 向自身发送 SIGUSR1 信号
raise(SIGUSR1);
printf("信号处理完成,程序继续运行\n");
return 0;
}
输出:
plaintext
进程向自身发送 SIGUSR1 信号
进程捕获到信号 10
信号处理完成,程序继续运行
四、abort () 函数:进程主动触发崩溃
1. 函数定义
abort() 是专用接口,用于当前进程主动触发异常终止 ,固定发送 SIGABRT(6) 信号,原型如下:
cpp
#include <stdlib.h>
// 无返回值(调用后进程必然终止)
void abort(void);
2. 底层逻辑(核心:"不可逃避的终止")
abort() 不是简单的 raise(SIGABRT),而是有兜底逻辑,确保进程必然终止:
- 首先调用
raise(SIGABRT)向自身发送SIGABRT信号; - 若进程捕获了
SIGABRT且处理函数返回(未终止进程),则abort()会重置 SIGABRT 为默认处理方式; - 再次发送
SIGABRT,此时内核强制执行默认行为:终止进程 + 生成 Core Dump(若开启)。
3. 关键特点
- 必终止进程 :即使捕获了
SIGABRT信号,abort()最终也会终止进程(无法忽略); - 生成 Core Dump :
SIGABRT是带 "核心转储" 属性的信号,开启ulimit -c unlimited后会生成 Core Dump 文件; - 资源清理 :调用
abort()时,进程会刷新并关闭所有打开的文件流,保证数据不丢失。
4. 典型示例
cpp
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 捕获 SIGABRT 信号
void abort_handler(int signo) {
printf("捕获到 SIGABRT 信号,尝试阻止终止...\n");
// 处理函数返回后,abort() 会再次发送 SIGABRT
}
int main() {
// 开启 Core Dump 限制
system("ulimit -c unlimited");
// 注册 SIGABRT 的处理函数
signal(SIGABRT, abort_handler);
printf("调用 abort() 函数\n");
abort();
// 以下代码永远不会执行(abort() 必终止进程)
printf("程序不会执行到这里\n");
return 0;
}
输出:
plaintext
调用 abort() 函数
捕获到 SIGABRT 信号,尝试阻止终止...
Aborted (core dumped) // 最终仍会终止并生成 Core Dump
五、三者的使用场景选择
- 进程间通信 / 管控 → 用
kill()比如主进程向子进程发送 "停止 / 继续" 信号、监控进程向业务进程发送告警信号。 - 进程自我通知 / 中断 → 用
raise()比如进程检测到错误后,向自身发送SIGUSR1信号触发自定义处理逻辑。 - 进程主动崩溃 / 调试 → 用
abort()比如程序检测到严重错误(如内存越界、参数非法),调用abort()生成 Core Dump,方便后续调试。
六、总结
kill()是通用信号发送接口,支持跨进程,参数 pid 决定目标范围,是最灵活的选择;raise()是kill(getpid(), sig)的封装,仅用于进程向自身发信号,使用更简单;abort()是专用崩溃接口,固定发送SIGABRT,且保证进程必然终止,适合主动触发核心转储;- 优先级:跨进程用
kill(),自身发任意信号用raise(),自身崩溃调试用abort()。
**3.**由软件条件产生信号
SIGPIPE是一种由软件条件产生的信号,在"管道"中已经介绍过了。本节主要介绍alarm函数 和SIGALRM信号。
cpp
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程。
alarm 函数深度解
alarm()(闹钟函数)是 Linux 中用于设置一次性定时器 的核心接口,本质是让内核在指定秒数后向当前进程发送 SIGALRM(14) 信号,实现 "延时触发事件" 的异步功能。它是实现简单定时逻辑的轻量级方案,无需创建额外线程 / 进程,仅依赖信号机制完成异步通知。
一、核心定义与基础用法
1. 函数原型
cpp
#include <unistd.h>
// 返回值:成功返回上一个定时器的剩余秒数;失败返回 -1(极少出现,通常仅参数非法时)
unsigned int alarm(unsigned int seconds);
2. 参数与返回值详解
| 要素 | 说明 |
|---|---|
seconds |
定时时长(秒):✅ seconds > 0:设置定时器,内核在 seconds 秒后发送 SIGALRM;✅ seconds = 0:取消当前已设置的定时器(无信号发送)。 |
| 返回值 | ✅ 若之前无未到期的定时器:返回 0;✅ 若之前有未到期的定时器:返回该定时器的剩余秒数(原定时器被新定时器覆盖)。 |
3. 核心特性
- 一次性定时器 :
alarm()设置的定时器触发一次后自动失效(区别于周期性定时器setitimer()); - 异步性 :定时器由内核维护,进程无需轮询,到时间后内核通过
SIGALRM信号异步通知进程; - 全局唯一性 :一个进程同一时间只能有一个
alarm定时器,重复调用会覆盖之前的设置(返回剩余时间)。
二、工作原理
- 进程调用
alarm(n)时,向内核注册一个 "n 秒后触发" 的定时器,内核记录该定时器的到期时间; - 进程继续执行正常逻辑,内核在后台计时;
- 定时器到期后,内核向该进程发送
SIGALRM(14)信号; - 进程处理
SIGALRM信号:- 未自定义处理函数:执行默认行为(终止进程);
- 自定义处理函数:跳转到处理函数执行,完成后回到原执行流程。
关键:
alarm()仅 "设置定时器",不阻塞进程,也不直接执行任何逻辑 ------ 所有定时后的操作都需通过SIGALRM信号处理函数实现。
三、典型使用场景与示例
场景 1:基础定时通知(避免进程默认终止)
核心:注册 SIGALRM 处理函数,避免定时器到期后进程被终止。
cpp
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
// SIGALRM 信号处理函数
void alarm_handler(int signo) {
printf("定时器到期!收到 SIGALRM 信号(%d)\n", signo);
}
int main() {
// 1. 注册 SIGALRM 处理函数(必须!否则进程会被终止)
if (signal(SIGALRM, alarm_handler) == SIG_ERR) {
perror("signal 注册失败");
return 1;
}
// 2. 设置 3 秒后触发的定时器
unsigned int prev = alarm(3);
printf("设置 3 秒定时器,之前无定时器,返回值:%u\n", prev); // 输出 0
// 3. 进程正常执行(模拟业务逻辑)
printf("进程执行中,等待定时器到期...\n");
for (int i = 1; i <= 5; i++) {
sleep(1);
printf("已运行 %d 秒\n", i);
}
return 0;
}
输出结果:
plaintext
设置 3 秒定时器,之前无定时器,返回值:0
进程执行中,等待定时器到期...
已运行 1 秒
已运行 2 秒
定时器到期!收到 SIGALRM 信号(14)
已运行 3 秒
已运行 4 秒
已运行 5 秒
场景 2:覆盖已有的定时器
核心:重复调用 alarm() 会覆盖旧定时器,返回旧定时器的剩余时间。
cpp
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void alarm_handler(int signo) {
printf("定时器到期!\n");
}
int main() {
signal(SIGALRM, alarm_handler);
// 第一次设置:5 秒定时器
unsigned int prev = alarm(5);
printf("第一次设置 5 秒定时器,返回值:%u\n", prev); // 0
// 1 秒后覆盖定时器:设置 3 秒定时器
sleep(1);
prev = alarm(3);
printf("1 秒后覆盖为 3 秒定时器,返回旧定时器剩余时间:%u\n", prev); // 4(5-1=4)
// 等待定时器到期
sleep(5);
return 0;
}
输出结果:
plaintext
第一次设置 5 秒定时器,返回值:0
1 秒后覆盖为 3 秒定时器,返回旧定时器剩余时间:4
定时器到期! // 总计 1+3=4 秒后触发(而非原 5 秒)
场景 3:取消定时器(seconds=0)
核心:alarm(0) 可取消当前未到期的定时器,返回剩余时间。
cpp
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void alarm_handler(int signo) {
printf("定时器到期(此消息不会打印)\n");
}
int main() {
signal(SIGALRM, alarm_handler);
// 设置 5 秒定时器
alarm(5);
printf("设置 5 秒定时器\n");
// 2 秒后取消定时器
sleep(2);
unsigned int prev = alarm(0);
printf("2 秒后取消定时器,返回剩余时间:%u\n", prev); // 3(5-2=3)
// 等待足够久,验证无信号触发
sleep(5);
printf("程序正常退出,无定时器触发\n");
return 0;
}
输出结果:
plaintext
设置 5 秒定时器
2 秒后取消定时器,返回剩余时间:3
程序正常退出,无定时器触发
场景 4:限时任务(比如 "5 秒内未输入则超时")
核心:结合 alarm() 和 pause()(阻塞等待信号),实现限时等待。
cpp
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
// 超时标记
volatile sig_atomic_t timeout = 0;
void alarm_handler(int signo) {
timeout = 1;
printf("\n输入超时!\n");
}
int main() {
signal(SIGALRM, alarm_handler);
// 设置 5 秒超时定时器
alarm(5);
printf("请在 5 秒内输入任意字符:");
char buf[10];
// 读取输入(若 5 秒内无输入,SIGALRM 会打断 read 调用)
if (read(STDIN_FILENO, buf, sizeof(buf)) == -1 && timeout) {
printf("任务超时,程序退出\n");
exit(1);
}
// 输入成功,取消定时器
alarm(0);
printf("输入成功:%s\n", buf);
return 0;
}
测试结果:
- 5 秒内输入:输出 "输入成功";
- 5 秒未输入:触发
SIGALRM,输出 "输入超时" 并退出。
四、关键注意事项
1. 精度限制
alarm() 的定时单位是秒 ,且受内核调度影响,实际触发时间可能比设置值略晚(误差通常在毫秒级),不适合高精度定时(高精度需用 setitimer() 或 timerfd)。
2. SIGALRM 的默认行为
若未注册 SIGALRM 处理函数,定时器到期后进程会直接终止 (SIGALRM 默认行为是终止进程),这是新手最容易踩的坑!
3. 与 sleep () 的区别
| 特性 | alarm() | sleep() |
|---|---|---|
| 核心逻辑 | 异步定时,发送信号 | 阻塞进程,内核暂停进程指定秒数 |
| 进程状态 | 定时期间进程正常运行 | 定时期间进程处于阻塞(TASK_INTERRUPTIBLE) |
| 中断方式 | 可被任意信号打断 | 仅能被信号打断(返回剩余睡眠秒数) |
| 用途 | 异步定时通知 | 同步阻塞等待 |
4. 信号处理函数的可重入性
SIGALRM 处理函数中需遵循 "可重入原则":
- 仅使用
volatile sig_atomic_t类型的变量(避免编译器优化); - 不调用非可重入函数(如
printf、malloc,建议仅做 "标记更新",主进程轮询标记处理业务)。
5. 多线程场景的限制
alarm() 是进程级 定时器,仅向进程的主线程发送 SIGALRM 信号(多线程中无法指定某线程接收信号)。若需线程级定时,需用 pthread_kill() 结合 sleep(),或 timer_create()(POSIX 定时器)。
五、进阶替代方案
若 alarm() 无法满足需求,可选择更强大的接口:
setitimer():支持微秒级精度,可设置周期性定时器(ITIMER_REAL 模式等价于增强版alarm());timerfd_create():将定时器封装为文件描述符,可结合epoll监听,适合事件驱动模型;clock_nanosleep():高精度睡眠,支持绝对时间 / 相对时间,适合同步定时。
六、总结
alarm()是轻量级的一次性秒级定时器,核心是通过SIGALRM信号实现异步定时;- 核心用法:注册
SIGALRM处理函数 → 调用alarm(seconds)设置定时 → 处理信号完成业务逻辑; - 关键坑点:未注册处理函数会导致进程终止,重复调用会覆盖旧定时器;
- 适用场景:简单定时通知、限时任务、异步超时检测(无需高精度的场景)
**4.**硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
信号捕捉初识
bash
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
signal(2, handler); //前文提到过,信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的,提前了
解一下
while(1);
return 0;
}
[hb@localhost code_test]$ ./sig
^Ccatch a sig : 2
^Ccatch a sig : 2
^Ccatch a sig : 2
^Ccatch a sig : 2
^\Quit (core dumped)
[hb@localhost code_test]$
模拟一下野指针异常
bash
//默认行为
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
//signal(SIGSEGV, handler);
sleep(1);
int *p = NULL;
*p = 100;
while(1);
return 0;
}
[hb@localhost code_test]$ ./sig
Segmentation fault (core dumped)
[hb@localhost code_test]$
//捕捉行为
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
//signal(SIGSEGV, handler);
sleep(1);
int *p = NULL;
*p = 100;
while(1);
return 0;
}
[hb@localhost code_test]$ ./sig
[hb@localhost code_test]$ ./sig
catch a sig : 11
catch a sig : 11
catch a sig : 11
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。
三.阻塞信号
**1.**信号其他相关常见概念
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
**2.**在内核中的表示
信号在内核中的表示示意图

1. 核心结构对应关系
这张图里的三个核心数组是内核管理进程信号的关键,每个信号对应数组中的一个条目:
| 结构 | 含义 |
|---|---|
block(阻塞信号集) |
标记哪些信号被进程阻塞(1 = 阻塞,0 = 不阻塞) |
pending(未决信号集) |
标记哪些信号已产生但未递达(1 = 未决,0 = 无) |
handler(信号处理方式集) |
定义每个信号的处理行为:SIG_DFL(默认)、SIG_IGN(忽略)、自定义函数指针 |
2. 逐信号解析图中状态
我们结合图中三个信号的具体值来分析:
🔹 SIGHUP(1)
block=0:不阻塞该信号pending=0:无未决的 SIGHUP 信号handler=SIG_DFL:采用默认处理方式(终止进程)
🔹 SIGINT (2)(Ctrl+C 对应的信号)
block=1:当前被阻塞pending=1:已产生但未递达(比如用户按了 Ctrl+C,但进程正处于阻塞该信号的状态)handler=SIG_IGN:设置为忽略处理(即使解除阻塞,信号也会被直接丢弃)
🔹 SIGQUIT (3)(Ctrl+\ 对应的信号)
block=1:当前被阻塞pending=0:无未决的 SIGQUIT 信号handler=自定义函数:指向用户态的sighandler函数,信号递达时会跳转到该函数执行
3. 背后的信号处理逻辑
-
信号产生与未决 当用户按 Ctrl+C 时,内核会将
SIGINT的pending位设为 1。但因为block位为 1,信号不会立即递达,处于 "未决" 状态。 -
信号阻塞与解除 进程可以通过
sigprocmask修改block集:- 若将
SIGINT的block位设为 0(解除阻塞),内核会检查pending位 - 此时
pending=1且handler=SIG_IGN,所以内核会直接丢弃该信号,不会触发任何处理
- 若将
-
信号处理方式的优先级 信号处理的优先级为:自定义处理 > 忽略处理 > 默认处理 。例如
SIGQUIT绑定了自定义函数,当它解除阻塞且有未决信号时,内核会切换到用户态执行sighandler。
4. 关键结论
- 内核通过
block、pending、handler三个集合,完整管理每个信号的生命周期 - 信号的 "阻塞" 与 "未决" 是独立状态:阻塞仅影响递达时机,不影响信号的产生
- 处理方式的配置决定了信号递达后的行为,而用户态的自定义函数是实现业务逻辑的核心
3. sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。
**4.**信号集操作函数
sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的.
cpp
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
一、核心背景:信号集(sigset_t)
在讲解函数前,先明确核心概念:
- 信号集 :用
sigset_t类型表示,本质是一个 "位图"(或数组),每一位对应一个信号(比如第 2 位对应SIGINT(2),第 14 位对应SIGALRM(14)); - 作用:用于标记 "哪些信号被阻塞""哪些信号未决""哪些信号需要操作";
- 注意 :
sigset_t是内核定义的结构体,不能直接手动修改位值,必须通过这 5 个专用函数操作(保证跨系统兼容性)。
二、5 个函数的核心定义与对比
先通过表格快速掌握每个函数的核心作用:
| 函数名 | 函数原型 | 核心作用 | 返回值 |
|---|---|---|---|
sigemptyset |
int sigemptyset(sigset_t *set); |
清空信号集(所有信号位设为 0) | 成功返回 0,失败返回 - 1 |
sigfillset |
int sigfillset(sigset_t *set); |
填满信号集(所有信号位设为 1) | 成功返回 0,失败返回 - 1 |
sigaddset |
int sigaddset(sigset_t *set, int signo); |
向信号集中添加指定信号(对应位设为 1) | 成功返回 0,失败返回 - 1 |
sigdelset |
int sigdelset(sigset_t *set, int signo); |
从信号集中删除指定信号(对应位设为 0) | 成功返回 0,失败返回 - 1 |
sigismember |
int sigismember(const sigset_t *set, int signo); |
判断指定信号是否在信号集中 | 存在返回 1,不存在返回 0,失败返回 - 1 |
通用注意事项
- 参数
signo:必须是合法的信号编号(1~64),不能是 0 或无效值(否则函数失败,设置errno=EINVAL); - 线程安全 :这些函数本身是线程安全的,但操作同一个
sigset_t变量时需加锁; - 兼容性:遵循 POSIX 标准,所有 Linux/Unix 系统均支持,替代手动操作位图的方式(避免不同系统信号位布局差异)。
三、逐个函数深度解析
1. sigemptyset:清空信号集
作用
初始化信号集,将所有信号对应的位设为 0(表示 "无任何信号"),是操作信号集的第一步(必须先初始化,否则信号集内容是随机的)。
典型用法
cpp
sigset_t set;
// 初始化信号集为空(必须先调用,否则set的值未定义)
if (sigemptyset(&set) == -1) {
perror("sigemptyset 失败");
return 1;
}
// 此时set中无任何信号
2. sigfillset:填满信号集
作用
将信号集中所有信号对应的位设为 1(表示 "包含所有信号"),常用于 "阻塞所有信号""检查所有信号是否未决" 等场景。
典型用法
cpp
sigset_t set;
// 初始化信号集为包含所有信号
if (sigfillset(&set) == -1) {
perror("sigfillset 失败");
return 1;
}
// 此时set中包含所有1~64号信号
3. sigaddset:添加指定信号到信号集
作用
向已初始化的信号集中添加单个信号(对应位设为 1),是最常用的函数(比如 "只阻塞 SIGINT 和 SIGQUIT")。
典型用法
cpp
sigset_t set;
// 第一步:清空信号集
sigemptyset(&set);
// 第二步:添加 SIGINT(2) 和 SIGQUIT(3)
if (sigaddset(&set, SIGINT) == -1) {
perror("sigaddset SIGINT 失败");
return 1;
}
if (sigaddset(&set, SIGQUIT) == -1) {
perror("sigaddset SIGQUIT 失败");
return 1;
}
// 此时set中仅包含 SIGINT 和 SIGQUIT
4. sigdelset:从信号集中删除指定信号
作用
从信号集中移除单个信号(对应位设为 0),常用于 "阻塞所有信号,但排除某个信号" 的场景。
典型用法
cpp
sigset_t set;
// 第一步:填满信号集(包含所有信号)
sigfillset(&set);
// 第二步:删除 SIGUSR1(10)(即不阻塞该信号)
if (sigdelset(&set, SIGUSR1) == -1) {
perror("sigdelset SIGUSR1 失败");
return 1;
}
// 此时set中包含除 SIGUSR1 外的所有信号
5. sigismember:判断信号是否在信号集中
作用
检查指定信号是否存在于信号集中(位是否为 1),常用于 "验证信号是否添加成功""查询未决信号" 等场景。
典型用法
cpp
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
// 判断 SIGINT 是否在信号集中
int ret = sigismember(&set, SIGINT);
if (ret == 1) {
printf("SIGINT 在信号集中\n"); // 输出此内容
} else if (ret == 0) {
printf("SIGINT 不在信号集中\n");
} else {
perror("sigismember 失败");
}
// 判断 SIGQUIT 是否在信号集中
ret = sigismember(&set, SIGQUIT);
if (ret == 0) {
printf("SIGQUIT 不在信号集中\n"); // 输出此内容
}
四、综合实战示例
场景:阻塞 SIGINT 和 SIGQUIT,查询未决信号
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
sigset_t block_set, pending_set;
// ========== 步骤1:初始化并配置阻塞信号集 ==========
// 清空信号集
if (sigemptyset(&block_set) == -1) {
perror("sigemptyset 失败");
exit(1);
}
// 添加要阻塞的信号:SIGINT(2) 和 SIGQUIT(3)
if (sigaddset(&block_set, SIGINT) == -1 || sigaddset(&block_set, SIGQUIT) == -1) {
perror("sigaddset 失败");
exit(1);
}
// ========== 步骤2:设置进程阻塞信号集 ==========
if (sigprocmask(SIG_BLOCK, &block_set, NULL) == -1) {
perror("sigprocmask 失败");
exit(1);
}
printf("已阻塞 SIGINT 和 SIGQUIT,请按 Ctrl+C/Ctrl+\\ 测试\n");
// ========== 步骤3:循环查询未决信号 ==========
for (int i = 0; i < 10; i++) {
sleep(1);
// 清空未决信号集
sigemptyset(&pending_set);
// 获取当前进程的未决信号集
if (sigpending(&pending_set) == -1) {
perror("sigpending 失败");
exit(1);
}
// 判断 SIGINT 是否未决
if (sigismember(&pending_set, SIGINT)) {
printf("检测到未决信号:SIGINT\n");
}
// 判断 SIGQUIT 是否未决
if (sigismember(&pending_set, SIGQUIT)) {
printf("检测到未决信号:SIGQUIT\n");
}
}
// ========== 步骤4:解除阻塞 ==========
if (sigprocmask(SIG_UNBLOCK, &block_set, NULL) == -1) {
perror("sigprocmask 解除阻塞失败");
exit(1);
}
printf("已解除阻塞,未决信号会立即处理\n");
return 0;
}
运行结果说明
- 运行程序后,按
Ctrl+C(SIGINT)或Ctrl+\(SIGQUIT),信号会被阻塞并标记为 "未决"; - 程序每秒查询一次未决信号集,通过
sigismember检测到未决信号并打印; - 10 秒后解除阻塞,未决的 SIGINT/SIGQUIT 会立即递达(默认行为是终止进程)。
五、常见错误与避坑指南
-
未初始化信号集直接操作 ❌ 错误:
sigset_t set; sigaddset(&set, SIGINT);(set 未初始化,内容随机);✅ 正确:先调用sigemptyset(&set),再调用sigaddset。 -
传入无效信号编号 ❌ 错误:
sigaddset(&set, 100);(100 不是合法信号编号);✅ 正确:仅使用系统定义的信号宏(如SIGINT、SIGUSR1)或确认有效的编号(1~64)。 -
混淆 "阻塞集" 和 "未决集" 的操作
sigprocmask操作的是阻塞集(block);sigpending获取的是未决集(pending);- 两者都用
sigset_t表示,但含义不同,需区分。
-
忽略函数返回值 ❌ 错误:直接调用
sigaddset(&set, SIGINT);不检查返回值;✅ 正确:检查返回值,通过perror打印错误原因(如权限问题、无效信号)。
六、总结
- 这 5 个函数是操作
sigset_t信号集的唯一标准方式,保证跨系统兼容性; - 核心流程:
sigemptyset(初始化)→sigaddset/sigdelset(添加 / 删除信号)→sigismember(验证); sigfillset用于 "全包含" 场景,sigemptyset是所有操作的前置步骤;- 结合
sigprocmask(修改阻塞集)、sigpending(查询未决集)是最典型的使用场景。
sigprocmask函数
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
sigprocmask() 是 Linux 中修改进程信号掩码(阻塞集) 的核心接口,也是控制信号递达时机的关键 ------ 通过它可以阻塞 / 解除阻塞指定信号,让信号暂时处于 "未决状态",直到解除阻塞后才会被处理。它是信号生命周期中 "递达" 环节的核心控制手段,结合之前讲的 sigset_t 操作函数(sigemptyset/sigaddset 等)使用。
一、核心定义与基础概念
1. 函数原型
cpp
#include <signal.h>
// 返回值:成功返回 0,失败返回 -1(设置 errno)
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
2. 核心背景:信号掩码(阻塞集)
信号掩码(也叫 "阻塞集"/"屏蔽字")是进程的核心属性,本质是 sigset_t 类型的位图:
- 每一位对应一个信号,
1表示 "阻塞该信号",0表示 "不阻塞"; - 被阻塞的信号不会立即递达,而是进入 "未决状态"(
pending); - 解除阻塞后,内核会立即处理该信号(若未决);
- 注意 :
SIGKILL(9)和SIGSTOP(19)无法被阻塞(内核强制忽略对这两个信号的阻塞操作)。
二、参数深度解析
1. how:修改阻塞集的方式(核心参数)
how 决定了如何将 set 中的信号集应用到进程的当前阻塞集,共 3 种取值:
how 取值 |
含义 | 典型场景 |
|---|---|---|
SIG_BLOCK |
将 set 中的信号添加到当前阻塞集(阻塞集 = 原阻塞集 ∪ set) |
新增需要阻塞的信号 |
SIG_UNBLOCK |
将 set 中的信号从当前阻塞集移除(阻塞集 = 原阻塞集 ∩ ~set) |
解除指定信号的阻塞 |
SIG_SETMASK |
用 set 完全替换当前阻塞集(阻塞集 = set) |
重置阻塞集为指定状态 |
关键说明:
SIG_BLOCK:是 "叠加阻塞",不会解除原有阻塞的信号;SIG_UNBLOCK:仅解除set中的信号,不影响其他信号的阻塞状态;SIG_SETMASK:是 "覆盖",会完全改变阻塞集(慎用,避免误解除关键信号的阻塞)。
2. set:待操作的信号集
- 指向一个
sigset_t类型的信号集,包含要 "阻塞 / 解除阻塞 / 替换" 的信号; - 若
set = NULL:表示 "不修改阻塞集",仅通过oldset获取当前阻塞集(常用作查询操作)。
3. oldset:保存旧的阻塞集
- 输出参数,指向一个
sigset_t变量,用于保存修改前的阻塞集; - 若
oldset = NULL:表示 "不需要保存旧的阻塞集"; - 用途:修改阻塞集后,可通过
oldset恢复原来的阻塞状态(比如临时阻塞信号,处理完后还原)。
三、典型使用场景与示例
场景 1:查询当前进程的阻塞集(仅获取,不修改)
核心:how 任意(因 set=NULL),set=NULL,oldset 保存当前阻塞集。
cpp
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
sigset_t oldset;
// 查询当前阻塞集(set=NULL 表示不修改,how 无意义,通常传 0)
if (sigprocmask(0, NULL, &oldset) == -1) {
perror("sigprocmask 查询失败");
exit(1);
}
// 检查常见信号是否被阻塞
printf("当前阻塞集状态:\n");
if (sigismember(&oldset, SIGINT)) {
printf("SIGINT(2) 被阻塞\n");
} else {
printf("SIGINT(2) 未被阻塞\n");
}
if (sigismember(&oldset, SIGQUIT)) {
printf("SIGQUIT(3) 被阻塞\n");
} else {
printf("SIGQUIT(3) 未被阻塞\n");
}
return 0;
}
场景 2:阻塞指定信号(SIGINT + SIGQUIT)
核心:how=SIG_BLOCK,set 包含要阻塞的信号。
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
sigset_t block_set, oldset;
// 1. 初始化并配置要阻塞的信号集
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT); // Ctrl+C
sigaddset(&block_set, SIGQUIT); // Ctrl+\
// 2. 阻塞 SIGINT 和 SIGQUIT,保存旧的阻塞集(以便后续还原)
if (sigprocmask(SIG_BLOCK, &block_set, &oldset) == -1) {
perror("sigprocmask 阻塞失败");
exit(1);
}
printf("已阻塞 SIGINT 和 SIGQUIT,按 Ctrl+C/Ctrl+\\ 无反应(5秒后解除)\n");
// 3. 模拟业务逻辑:5秒内即使按 Ctrl+C/Ctrl+\,信号也会被阻塞(未决)
sleep(5);
// 4. 恢复原来的阻塞集(解除 SIGINT/SIGQUIT 的阻塞)
if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {
perror("sigprocmask 恢复失败");
exit(1);
}
printf("已恢复阻塞集,未决的 SIGINT/SIGQUIT 会立即处理\n");
// 等待信号处理(若5秒内按了 Ctrl+C,此时会触发)
sleep(2);
return 0;
}
运行结果:
- 运行后按
Ctrl+C/Ctrl+\,程序无反应(信号被阻塞); - 5 秒后恢复阻塞集,若之前按过
Ctrl+C,程序会立即处理 SIGINT(默认终止)。
场景 3:解除指定信号的阻塞
核心:how=SIG_UNBLOCK,set 包含要解除阻塞的信号。
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
sigset_t block_all, unblock_set;
// 1. 填满信号集(包含所有信号)
sigfillset(&block_all);
// 2. 阻塞所有信号(SIGKILL/SIGSTOP 除外)
sigprocmask(SIG_SETMASK, &block_all, NULL);
printf("已阻塞所有信号,仅保留 SIGUSR1 可递达\n");
// 3. 配置要解除阻塞的信号:SIGUSR1(10)
sigemptyset(&unblock_set);
sigaddset(&unblock_set, SIGUSR1);
// 4. 解除 SIGUSR1 的阻塞
sigprocmask(SIG_UNBLOCK, &unblock_set, NULL);
// 5. 循环等待 SIGUSR1(其他信号仍被阻塞)
while (1) {
sleep(1);
printf("等待 SIGUSR1 信号(可通过 kill -10 %d 发送)\n", getpid());
}
return 0;
}
测试方式:
- 另一个终端执行
kill -10 进程PID,程序会处理 SIGUSR1(默认终止); - 若发送其他信号(如
kill -2 进程PID),程序无反应(仍被阻塞)。
场景 4:临时阻塞信号,处理完后还原
核心:先保存旧阻塞集,修改后再通过 SIG_SETMASK 还原。
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void sig_handler(int signo) {
printf("捕获到信号 %d\n", signo);
}
int main() {
sigset_t block_set, oldset;
// 注册 SIGINT 处理函数
signal(SIGINT, sig_handler);
// 1. 配置要临时阻塞的信号:SIGINT
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
// 2. 临时阻塞 SIGINT,保存旧阻塞集
sigprocmask(SIG_BLOCK, &block_set, &oldset);
printf("临时阻塞 SIGINT,3秒内按 Ctrl+C 无反应\n");
sleep(3);
// 3. 还原旧阻塞集(解除 SIGINT 阻塞)
sigprocmask(SIG_SETMASK, &oldset, NULL);
printf("已还原阻塞集,SIGINT 可正常递达\n");
// 等待信号
while (1) {
sleep(1);
}
return 0;
}
四、关键注意事项
1. 无法阻塞的信号
sigprocmask() 对 SIGKILL(9) 和 SIGSTOP(19) 无效:
- 即使将这两个信号加入
set,内核也会忽略该操作; - 目的是保证系统能强制终止 / 暂停进程,避免进程 "卡死"。
2. 未决信号的处理
- 解除信号阻塞时,若该信号处于 "未决状态"(
pending=1),内核会立即处理该信号(递达); - 非实时信号(1~31):未决状态仅保留 1 次,解除阻塞后只处理 1 次;
- 实时信号(34~64):未决状态会排队,解除阻塞后逐个处理。
3. 线程场景的限制
sigprocmask()是进程级 接口,仅影响调用线程所在的进程(在多线程程序中,建议用pthread_sigmask()替代,可指定线程的信号掩码);- 多线程中,主线程的
sigprocmask()不会影响子线程的信号掩码(子线程默认继承主线程的掩码,但可独立修改)。
4. 错误码说明
| errno 值 | 含义 | 常见原因 |
|---|---|---|
EINVAL |
how 参数无效 |
传入非 SIG_BLOCK/SIG_UNBLOCK/SIG_SETMASK 的值 |
EFAULT |
set 或 oldset 指针无效 |
指向非法内存(如空指针、只读内存) |
5. 与 sigpending 的配合使用
sigprocmask() 控制阻塞集,sigpending() 查询未决集,两者结合是调试信号状态的常用组合:
cpp
sigset_t pending_set;
// 查询未决信号集
sigpending(&pending_set);
// 判断 SIGINT 是否未决
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT 处于未决状态\n");
}
五、与其他信号接口的关联
| 接口 | 与 sigprocmask 的关系 |
|---|---|
sigemptyset/sigaddset |
用于构造 sigprocmask 需要的 set 信号集 |
sigpending |
查询 sigprocmask 阻塞后产生的未决信号 |
sigaction |
注册信号处理函数,sigprocmask 控制信号是否递达 |
pthread_sigmask |
多线程版本的 sigprocmask,控制单个线程的信号掩码 |
六、总结
sigprocmask()是修改进程信号掩码(阻塞集)的唯一标准接口,核心是通过how参数控制阻塞集的修改方式;- 核心流程:构造信号集(
sigemptyset/sigaddset)→ 调用sigprocmask修改阻塞集 → (可选)通过oldset还原; - 关键特性:无法阻塞
SIGKILL/SIGSTOP,解除阻塞时会立即处理未决信号; - 适用场景:临时阻塞信号(避免关键逻辑被打断)、选择性放行信号、查询当前阻塞状态。
sigpending函数
sigpending() 是 Linux 中查询进程当前未决信号集 的核心接口,它能精准获取 "哪些信号已产生、但因被阻塞而未递达" 的状态,是分析信号生命周期中 "未决阶段" 的关键工具。结合之前讲的 sigprocmask(控制阻塞集)和信号集操作函数(sigismember 等),可完整还原进程的信号状态。
一、核心定义与基础概念
1. 函数原型
cpp
#include <signal.h>
// 返回值:成功返回 0,失败返回 -1(设置 errno)
int sigpending(sigset_t *set);
2. 核心背景:未决信号集
未决信号集(Pending Set)是进程的核心属性,与阻塞集(Block Set)强关联:
- 未决:信号已由内核 / 进程 / 硬件产生,但因进程阻塞该信号,暂时无法递达(处理);
- 未决信号集 :
sigset_t类型的位图,每一位对应一个信号,1表示 "该信号未决",0表示 "无未决"; - 核心逻辑 :
- 信号产生 → 内核将未决集对应位设为
1; - 若信号未被阻塞 → 立即递达(处理),未决集对应位设为
0; - 若信号被阻塞 → 保持未决状态,直到解除阻塞(递达后未决位清零);
- 信号产生 → 内核将未决集对应位设为
- 区别 :非实时信号(1~31)的未决位仅记录 "有无"(不排队),实时信号(34~64)的未决集是队列(可记录多个相同信号),但
sigpending()仅能检测 "是否未决",无法获取实时信号的数量。
二、参数深度解析
唯一参数:set(输出参数)
- 指向一个
sigset_t类型的变量,sigpending()会将当前进程的未决信号集写入该变量; - 调用前需确保
set指向有效的内存空间(可先通过sigemptyset初始化,也可直接传入,函数会覆盖其内容); - 若
set为 NULL:函数会失败(errno=EFAULT),无任何输出。
返回值与错误码
| 返回值 | 含义 | 错误码(errno) | 常见原因 |
|---|---|---|---|
| 0 | 成功,set 已填充未决信号集 |
- | - |
| -1 | 失败 | EFAULT |
set 指针指向非法内存(如空指针、只读内存) |
三、典型使用场景与示例
场景 1:基础用法 ------ 查询未决信号
核心:先阻塞信号→触发信号→用 sigpending() 查询未决状态→验证未决信号。
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
sigset_t block_set, pending_set;
// ========== 步骤1:配置阻塞集(阻塞 SIGINT(2)) ==========
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT); // Ctrl+C 对应的信号
// 阻塞 SIGINT
if (sigprocmask(SIG_BLOCK, &block_set, NULL) == -1) {
perror("sigprocmask 失败");
exit(1);
}
printf("已阻塞 SIGINT,请按 Ctrl+C 触发信号(3秒内)\n");
// ========== 步骤2:等待用户触发 SIGINT ==========
sleep(3);
// ========== 步骤3:查询未决信号集 ==========
// 初始化 pending_set(可选,函数会覆盖内容,但初始化是好习惯)
sigemptyset(&pending_set);
if (sigpending(&pending_set) == -1) {
perror("sigpending 失败");
exit(1);
}
// ========== 步骤4:检测 SIGINT 是否未决 ==========
if (sigismember(&pending_set, SIGINT)) {
printf("检测到 SIGINT 处于未决状态(已产生但未递达)\n");
} else {
printf("未检测到 SIGINT 未决\n");
}
// ========== 步骤5:解除阻塞,未决信号会立即递达 ==========
printf("解除 SIGINT 阻塞,未决信号将被处理\n");
sigprocmask(SIG_UNBLOCK, &block_set, NULL);
return 0;
}
运行结果:
- 运行程序后,按
Ctrl+C(触发 SIGINT); - 程序输出 "检测到 SIGINT 处于未决状态";
- 解除阻塞后,SIGINT 立即递达(默认行为是终止程序)。
场景 2:循环监控未决信号
核心:持续查询未决信号集,实时反馈信号状态(适合调试信号阻塞逻辑)。
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
sigset_t block_set, pending_set;
// 阻塞 SIGINT(2) 和 SIGQUIT(3)(Ctrl+\)
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigaddset(&block_set, SIGQUIT);
sigprocmask(SIG_BLOCK, &block_set, NULL);
printf("已阻塞 SIGINT 和 SIGQUIT,PID:%d\n", getpid());
printf("可通过 kill -2/%d 或 kill -3/%d 发送信号,程序会监控未决状态\n", getpid(), getpid());
// 循环查询未决信号
while (1) {
sleep(1);
// 查询未决信号集
sigpending(&pending_set);
// 检测 SIGINT
if (sigismember(&pending_set, SIGINT)) {
printf("[未决信号] SIGINT(2)\n");
}
// 检测 SIGQUIT
if (sigismember(&pending_set, SIGQUIT)) {
printf("[未决信号] SIGQUIT(3)\n");
// 检测到 SIGQUIT 未决,退出循环
break;
}
}
// 解除阻塞,处理 SIGQUIT
sigprocmask(SIG_UNBLOCK, &block_set, NULL);
printf("解除阻塞,程序退出\n");
return 0;
}
测试方式:
- 另一个终端执行
kill -2 进程PID:程序输出[未决信号] SIGINT(2); - 执行
kill -3 进程PID:程序输出[未决信号] SIGQUIT(3)并退出。
场景 3:验证 "非实时信号不排队" 特性
核心:多次发送同一非实时信号,未决集仅标记 "有",解除阻塞后仅处理 1 次。
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
// 信号处理函数:统计 SIGINT 被处理的次数
volatile sig_atomic_t sigint_count = 0;
void sig_handler(int signo) {
sigint_count++;
printf("处理 SIGINT,累计次数:%d\n", sigint_count);
}
int main() {
sigset_t block_set, pending_set;
// 注册 SIGINT 处理函数
signal(SIGINT, sig_handler);
// 阻塞 SIGINT
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigprocmask(SIG_BLOCK, &block_set, NULL);
printf("已阻塞 SIGINT,PID:%d\n", getpid());
printf("请在另一个终端执行:kill -2 %d(执行3次)\n", getpid());
sleep(5); // 等待用户发送3次 SIGINT
// 查询未决信号集
sigpending(&pending_set);
if (sigismember(&pending_set, SIGINT)) {
printf("检测到 SIGINT 未决(即使发送3次,未决集仅标记1次)\n");
}
// 解除阻塞,处理未决信号
sigprocmask(SIG_UNBLOCK, &block_set, NULL);
sleep(1); // 等待信号处理完成
printf("最终 SIGINT 处理次数:%d(验证非实时信号不排队)\n", sigint_count);
return 0;
}
运行结果:
- 即使发送 3 次 SIGINT,未决集仅标记 1 次,解除阻塞后处理函数仅执行 1 次,输出 "最终 SIGINT 处理次数:1"。
四、关键注意事项
1. 仅能检测 "是否未决",无法获取数量
- 对于非实时信号(1~31):无论发送多少次,未决集仅标记 "有"(位为 1),
sigpending()无法知道发送次数; - 对于实时信号(34~64):内核会用队列保存未决信号,但
sigpending()仍只能检测 "是否未决",需通过sigqueue()+ 自定义逻辑获取数量。
2. 未决信号的清零时机
- 信号递达(处理)后,内核会将未决集对应位清零;
- 若进程忽略该信号(
handler=SIG_IGN),解除阻塞后未决位会清零,但无处理逻辑; - 若进程终止,未决信号集也会被清空。
3. 线程场景的限制
sigpending()是进程级 接口,在多线程中调用时,返回的是调用线程所在进程的未决信号集;- 多线程中,信号的未决状态是进程级的(所有线程共享),但阻塞状态是线程级的(可通过
pthread_sigmask()单独设置)。
4. 与 sigprocmask 的强关联
sigpending() 的结果完全依赖 sigprocmask() 设置的阻塞集:
- 若信号未被阻塞 → 产生后立即递达,
sigpending()检测不到; - 若信号被阻塞 → 产生后进入未决状态,
sigpending()可检测到。
5. 常见错误避坑
| 错误操作 | 后果 | 正确做法 |
|---|---|---|
未初始化 set 直接调用 |
无实质影响(函数会覆盖 set),但不符合编程规范 |
调用前执行 sigemptyset(&set) 初始化 |
传入 set=NULL |
函数失败,errno=EFAULT |
确保 set 指向有效的栈 / 堆内存 |
| 检测无效信号编号 | sigismember 返回 -1,errno=EINVAL |
仅检测合法信号(1~64),使用系统宏(如 SIGINT) |
五、与其他信号接口的配合关系
| 接口 | 与 sigpending 的配合场景 |
|---|---|
sigprocmask |
阻塞信号,制造未决状态,供 sigpending() 查询 |
sigismember |
解析 sigpending() 返回的未决信号集,判断指定信号是否未决 |
sigaction |
注册信号处理函数,决定未决信号解除阻塞后的处理逻辑 |
kill/raise |
发送信号,触发未决状态 |
六、总结
sigpending()是查询进程未决信号集的唯一标准接口,核心作用是 "查看哪些信号已产生但被阻塞";- 核心流程:阻塞信号(
sigprocmask)→ 产生信号(kill/ 手动触发)→ 查询未决集(sigpending)→ 解析未决状态(sigismember); - 关键特性:仅能检测 "是否未决",无法获取信号数量;非实时信号未决集不排队,实时信号排队但无法通过该函数感知数量;
- 适用场景:调试信号阻塞逻辑、验证信号未决状态、实现 "信号触发后延迟处理" 的逻辑。
四.捕捉信号

1. 进入内核态:信号处理的触发前提
- 触发场景 :进程在用户态执行时,因 *中断(如时钟中断)、异常(如除零错误)或系统调用(如
read)*主动进入内核态。 - 核心作用:这是信号处理的唯一时机 ------ 内核只会在 "从内核态返回用户态" 前检查并处理信号,不会主动打断用户态执行。
2. 内核检查并准备处理信号
- 核心函数 :内核执行
do_signal(),检查当前进程的未决信号集 和信号掩码,判断哪些信号可以递达(未被阻塞且未决)。 - 关键逻辑 :
- 遍历未决信号集,找到第一个可以递达的信号。
- 根据信号的
handler确定处理方式(默认、忽略、自定义)。
3. 切换回用户态执行信号处理函数
- 核心逻辑 :若信号绑定了自定义处理函数 ,内核会修改进程的用户态上下文(如栈、程序计数器),让进程返回用户态时,直接跳转到信号处理函数
sighandler执行,而非回到原主控制流程。 - 为什么不直接在内核态执行:用户态的处理函数属于进程代码,内核无法直接执行用户空间的指令,必须切换回用户态才能安全执行。
4. 信号处理函数返回,触发 sigreturn 系统调用
- 核心动作 :信号处理函数执行完成后,会自动调用特殊的系统调用
sigreturn(),再次进入内核态。 - 作用 :
sigreturn()是内核提供的 "收尾接口",用于恢复进程在进入信号处理函数前的用户态上下文(栈、寄存器、程序计数器等)。
5. 内核恢复上下文,回到原主控制流程
- 核心函数 :内核执行
sys_sigreturn(),恢复之前保存的主控制流程上下文。 - 最终结果 :进程从内核态返回用户态时,会从上次被中断的指令处继续执行,实现 "信号处理完成后无缝恢复主流程" 的效果。
关键技术细节总结
- 异步性与时机控制:信号处理不是实时打断,而是延迟到 "内核态返回用户态" 时处理,避免频繁切换态导致性能损耗。
- 上下文切换的完整性:内核会保存两次上下文 ------ 第一次是主流程被中断时的上下文,第二次是信号处理函数执行时的上下文,确保最终能完全恢复。
- 自定义处理的安全性 :信号处理函数运行在用户态,避免了内核直接执行用户代码的风险,同时通过
sigreturn保证了上下文的正确恢复。 - 默认 / 忽略处理的简化:若信号处理方式为默认或忽略,内核会直接在内核态完成处理(如终止进程、丢弃信号),无需切换回用户态执行额外逻辑。
sigaction函数
sigaction() 是 Linux 中注册 / 修改信号处理方式 的标准接口(POSIX 规范),也是替代老旧 signal() 函数的工业级方案。它不仅能实现信号处理函数的绑定,还能精细控制信号处理的行为(如阻塞嵌套信号、保留信号元数据),是编写健壮信号处理程序的核心工具。
一、核心定义与基础优势
1. 函数原型
cpp
#include <signal.h>
// 返回值:成功返回 0,失败返回 -1(设置 errno)
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
2. 核心优势(对比 signal())
| 特性 | signal () 函数 | sigaction () 函数 |
|---|---|---|
| 可移植性 | 不同系统行为不一致(如是否重置处理方式) | 遵循 POSIX 标准,跨系统兼容 |
| 信号处理行为控制 | 无(仅能绑定处理函数) | 可设置阻塞掩码、保留信号信息等 |
| 实时信号支持 | 差(部分系统不支持) | 完美支持(可获取实时信号携带的数据) |
| 处理函数重置 | 部分系统会重置为默认方式 | 不会自动重置,行为稳定 |
二、核心结构体:struct sigaction
sigaction() 的核心是 struct sigaction 结构体,它定义了信号的处理方式和附加属性,原型如下:
cpp
struct sigaction {
// 方式1:普通信号处理函数(不接收信号附加数据)
void (*sa_handler)(int);
// 方式2:扩展信号处理函数(可接收信号元数据,如实时信号的附加数据)
void (*sa_sigaction)(int, siginfo_t *, void *);
// 信号处理期间的阻塞掩码(临时阻塞指定信号)
sigset_t sa_mask;
// 控制信号处理行为的标志位(如 SA_SIGINFO、SA_RESTART 等)
int sa_flags;
// 废弃字段(无实际作用)
void (*sa_restorer)(void);
};
结构体成员深度解析
1. sa_handler:普通处理函数
- 取值:
SIG_DFL:使用信号默认处理方式(如终止进程、忽略等);SIG_IGN:忽略该信号;- 自定义函数指针:
void (*handler)(int),参数为信号编号。
- 适用场景:无需获取信号附加信息的普通场景(如处理
SIGINT、SIGALRM)。
2. sa_sigaction:扩展处理函数
-
原型:
void (*sa_sigaction)(int signo, siginfo_t *info, void *context); -
启用条件:
sa_flags必须设置SA_SIGINFO; -
参数说明:
参数 含义 signo触发的信号编号(同 sa_handler的参数)info信号元数据结构体(包含信号发送者 PID、实时信号附加数据、信号产生原因等) context指向 ucontext_t结构体,保存信号触发时的进程上下文(寄存器、栈等) -
适用场景:需要获取信号详细信息的场景(如实时信号、追踪信号发送者)。
3. sa_mask:处理期间的阻塞掩码
- 作用:信号处理函数执行期间,内核会临时将
sa_mask中的信号加入进程阻塞集,避免嵌套触发同类 / 其他信号,导致处理函数重入; - 特性:
- 仅在处理函数执行期间生效,处理完成后自动恢复原阻塞集;
- 触发当前信号的信号会被自动加入
sa_mask(无需手动添加),避免嵌套触发。
4. sa_flags:行为控制标志位
常用标志位(可通过 | 组合):
| 标志位 | 含义 |
|---|---|
SA_SIGINFO |
启用 sa_sigaction 扩展处理函数(替代 sa_handler) |
SA_RESTART |
信号打断的系统调用(如 read、write)自动重启,避免返回 -1(errno=EINTR) |
SA_NODEFER |
不自动阻塞当前信号(允许嵌套触发) |
SA_RESETHAND |
信号处理完成后,自动将处理方式重置为 SIG_DFL(模拟 signal() 的旧行为) |
SA_NOCLDSTOP |
仅对 SIGCHLD 有效:子进程暂停 / 继续时不发送 SIGCHLD,仅退出时发送 |
SA_NOCLDWAIT |
仅对 SIGCHLD 有效:子进程退出时不产生僵尸进程,内核自动回收资源 |
三、参数深度解析
1. signo:目标信号编号
- 合法值:1~64(支持所有信号,包括实时信号);
- 例外:
SIGKILL(9)和SIGSTOP(19)无法注册处理函数(设置会失败)。
2. act:新的信号处理配置
- 指向
struct sigaction结构体,包含要设置的处理方式、掩码、标志位; - 若
act = NULL:表示 "不修改信号处理方式",仅通过oldact获取当前配置。
3. oldact:保存旧的处理配置
- 输出参数,指向
struct sigaction结构体; - 若
oldact = NULL:表示 "不需要保存旧配置"; - 用途:修改处理方式后,可通过
oldact恢复原有配置(如临时修改信号处理)。
返回值与错误码
| 返回值 | 含义 | 错误码(errno) | 常见原因 |
|---|---|---|---|
| 0 | 成功 | - | - |
| -1 | 失败 | EINVAL |
signo 无效(如 9/19)或 sa_flags 非法 |
EFAULT |
act/oldact 指向非法内存 |
四、典型使用场景与示例
场景 1:基础用法 ------ 绑定普通处理函数
核心:使用 sa_handler 绑定简单处理函数,设置 sa_mask 阻塞嵌套信号。
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
// 普通信号处理函数
void sigint_handler(int signo) {
printf("捕获到 SIGINT(%d),处理期间阻塞 SIGQUIT\n", signo);
sleep(3); // 模拟处理耗时操作
printf("SIGINT 处理完成\n");
}
int main() {
struct sigaction act;
// 1. 初始化并配置 act 结构体
// 清空 sa_mask(后续添加要阻塞的信号)
sigemptyset(&act.sa_mask);
// 处理 SIGINT 期间,临时阻塞 SIGQUIT(3)
sigaddset(&act.sa_mask, SIGQUIT);
// 绑定普通处理函数
act.sa_handler = sigint_handler;
// 无特殊行为标志
act.sa_flags = 0;
// 2. 注册 SIGINT 信号的处理方式
if (sigaction(SIGINT, &act, NULL) == -1) {
perror("sigaction 失败");
exit(1);
}
printf("已注册 SIGINT 处理函数,按 Ctrl+C 触发(3秒内按 Ctrl+\\ 无反应)\n");
while (1) {
sleep(1);
}
return 0;
}
运行结果:
- 按
Ctrl+C触发 SIGINT,处理函数执行期间(3 秒),按Ctrl+\(SIGQUIT)无反应(被sa_mask阻塞); - 3 秒后处理完成,SIGQUIT 才会递达(若期间按过)。
场景 2:扩展用法 ------ 获取信号元数据
核心:使用 sa_sigaction 结合 SA_SIGINFO,获取信号发送者 PID 和附加数据。
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
// 扩展信号处理函数
void sigusr1_handler(int signo, siginfo_t *info, void *context) {
printf("===== 信号详情 =====\n");
printf("信号编号:%d\n", signo);
printf("发送者 PID:%d\n", info->si_pid);
printf("信号产生原因:%d\n", info->si_code);
// 若为实时信号,可获取附加数据(si_value)
if (signo >= SIGRTMIN && signo <= SIGRTMAX) {
printf("实时信号附加数据:%d\n", info->si_value.sival_int);
}
}
int main() {
struct sigaction act;
// 1. 配置 act 结构体
sigemptyset(&act.sa_mask);
// 绑定扩展处理函数
act.sa_sigaction = sigusr1_handler;
// 启用 SA_SIGINFO 标志(必须)
act.sa_flags = SA_SIGINFO;
// 2. 注册 SIGUSR1(10) 信号
if (sigaction(SIGUSR1, &act, NULL) == -1) {
perror("sigaction 失败");
exit(1);
}
printf("进程 PID:%d\n", getpid());
printf("请执行:kill -10 %d 或 kill -%d %d(发送实时信号)\n", getpid(), SIGRTMIN+1, getpid());
while (1) {
sleep(1);
}
return 0;
}
测试方式:
- 执行
kill -10 进程PID:输出 SIGUSR1 的发送者 PID 和产生原因; - 执行
kill -35 进程PID(SIGRTMIN+1):输出实时信号的附加数据(默认 0)。
场景 3:自动重启被打断的系统调用
核心:设置 SA_RESTART 标志,让 read/write 等系统调用被信号打断后自动重启。
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
void alarm_handler(int signo) {
printf("捕获到 SIGALRM(%d),但 read 会自动重启\n", signo);
}
int main() {
struct sigaction act;
char buf[10];
// 配置 act 结构体
sigemptyset(&act.sa_mask);
act.sa_handler = alarm_handler;
// 关键:设置 SA_RESTART 标志
act.sa_flags = SA_RESTART;
// 注册 SIGALRM 信号
sigaction(SIGALRM, &act, NULL);
// 设置 3 秒定时器(打断 read 调用)
alarm(3);
printf("请输入任意字符(3秒后触发 SIGALRM,但 read 不会中断):");
// read 会被 SIGALRM 打断,但 SA_RESTART 使其自动重启
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
if (n == -1) {
perror("read 失败");
exit(1);
}
printf("输入成功:%s\n", buf);
return 0;
}
运行结果:
- 3 秒后触发 SIGALRM,处理函数执行完成后,
read不会返回-1,而是继续等待用户输入(自动重启)。
场景 4:恢复原有信号处理配置
核心:通过 oldact 保存旧配置,修改后再还原。
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void temp_handler(int signo) {
printf("临时处理 SIGINT,5秒后恢复默认行为\n");
}
int main() {
struct sigaction act, oldact;
// 1. 配置临时处理方式
sigemptyset(&act.sa_mask);
act.sa_handler = temp_handler;
act.sa_flags = 0;
// 2. 注册 SIGINT,保存旧配置到 oldact
if (sigaction(SIGINT, &act, &oldact) == -1) {
perror("sigaction 失败");
exit(1);
}
printf("5秒内按 Ctrl+C 触发临时处理函数\n");
sleep(5);
// 3. 恢复原有 SIGINT 处理配置
if (sigaction(SIGINT, &oldact, NULL) == -1) {
perror("sigaction 恢复失败");
exit(1);
}
printf("已恢复 SIGINT 默认行为,按 Ctrl+C 会终止程序\n");
while (1) {
sleep(1);
}
return 0;
}
五、关键注意事项
1. SIGKILL/SIGSTOP 不可处理
无论如何配置 sigaction(),这两个信号都无法注册自定义处理函数(内核强制忽略),确保系统能强制管控进程。
2. 处理函数的可重入性
- 信号处理函数会异步打断进程执行,需遵循 "可重入原则":
- 仅使用
volatile sig_atomic_t类型的变量; - 不调用非可重入函数(如
printf、malloc、strcpy); - 简化处理逻辑(仅做标记,主进程轮询标记处理业务)。
- 仅使用
3. sa_handler 与 sa_sigaction 互斥
- 若
sa_flags未设置SA_SIGINFO,内核使用sa_handler; - 若设置了
SA_SIGINFO,内核使用sa_sigaction(sa_handler失效)。
4. 实时信号的特殊处理
- 实时信号(34~64)的附加数据需通过
sigqueue()发送,sigaction()的sa_sigaction接收; - 实时信号支持排队,
sa_mask可避免嵌套处理导致的队列混乱。
5. 与 signal() 的兼容
signal() 可视为 sigaction() 的简化版,等价于:
cpp
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = handler;
act.sa_flags = SA_RESETHAND | SA_NODEFER; // 不同系统略有差异
sigaction(signo, &act, NULL);
六、总结
sigaction()是注册信号处理方式的标准接口 ,功能远超signal(),是工业级代码的首选;- 核心能力:绑定普通 / 扩展处理函数、控制处理期间的阻塞掩码、设置信号处理行为(如自动重启系统调用);
- 核心流程:初始化
struct sigaction→ 配置处理函数 / 掩码 / 标志位 → 调用sigaction()注册 → (可选)通过oldact恢复旧配置; - 关键坑点:处理函数需保证可重入,
SIGKILL/SIGSTOP不可处理,SA_SIGINFO需配合sa_sigaction使用。
五.可重入函数

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile
该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下
cpp
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
[hb@localhost code_test]$ cat Makefile
sig:sig.c
gcc -o sig sig.c #-O2
.PHONY:clean
clean:
rm -f sig
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
process quit normal
标准情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 , while 条件不满足,退出循
环,进程退出
cpp
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
[hb@localhost code_test]$ cat Makefile
sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
^Cchage flag 0 to 1
^Cchage flag 0 to 1
优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。 while 检测的flag其实已经因为优化,被放在了 CPU寄存器当中。如何解决呢?很明显需要 volatile
cpp
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
[hb@localhost code_test]$ cat Makefile
sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
process quit normal
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作