Linux_进程信号

本节重点:

  1. 掌握Linux信号的基本概念
  2. 掌握信号产生的一般方式
  3. 理解信号递达和阻塞的概念,原理。
  4. 掌握信号捕捉的一般方式。
  5. 重新了解可重入函数的概念。
  6. 了解竞态条件的情景和处理方式
  7. 了解SIGCHLD信号, 重新编写信号处理函数的一般处理机制

一.信号入门

**1.**生活角度的信号

你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能"识别快递"
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。
那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成"在合适的时候去取"。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你"记住了有一个快递要去取"
当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话.

**2.**技术应用角度的信号

  1. 用户输入命令,在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.**注意

  1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步
    (Asynchronous)的。

**4.**信号概念

信号是Linux 进程间通信(IPC)的一种异步通信机制 ,也是内核向进程传递事件通知的核心方式,本质是内核给进程发送的软中断------ 进程无需主动轮询,内核会在合适的时机打断进程的正常执行流程,触发信号相关的处理逻辑。

简单来说,信号是进程之间事件异步通知的一种方式,属于软中断。

5.kill -l****命令可以察看系统定义的信号列表


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

**6.**信号处理常见方式概览

(sigaction函数稍后详细介绍),可选的处理动作有以下三种:

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

二.产生信号

**1.**通过终端按键产生信号

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。

Core Dump

Core Dump(核心转储)是 Linux/Unix 系统中进程异常终止时,内核将进程的内存镜像、寄存器状态、调用栈等核心数据保存到磁盘文件的机制,本质是进程 "崩溃现场" 的完整快照。它是定位程序崩溃(如段错误、非法指令、内存越界)的 "终极工具",能让开发者在事后还原崩溃瞬间的进程状态,而非仅依赖运行时的错误提示。

一、Core Dump 的核心本质

当进程因未捕获的信号 (如段错误SIGSEGV、除零错误SIGFPE、非法内存访问SIGBUS等)异常终止时,内核会触发 Core Dump:

  1. 内核暂停进程的所有执行流,冻结当前的内存布局(堆、栈、数据段、代码段);
  2. 将进程的核心数据(内存、寄存器、程序计数器、调用栈、文件描述符等)写入磁盘文件(默认文件名为corecore.<pid>);
  3. 进程按信号的默认行为终止(如退出)。

简单来说: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 的常见应用场景
  1. 定位生产环境崩溃问题:生产服务器程序崩溃时,无法实时调试,Core Dump 是唯一能还原现场的手段;
  2. 复现偶发崩溃:部分崩溃(如多线程竞态、内存越界)难以复现,Core Dump 可保存崩溃瞬间的所有状态;
  3. 分析内存泄漏 / 越界 :结合 gdb 的x(查看内存)、p(打印变量)命令,定位非法内存操作;
  4. 调试第三方程序:无源码时,可通过 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查看该线程的调用栈。

七、总结
  1. Core Dump 是进程崩溃时的 "内存快照",由内核生成,需先通过ulimit -c unlimited开启系统限制;
  2. 仅特定信号(如SIGSEGVSIGFPE)会触发 Core Dump,SIGKILL永远不会;
  3. 调试 Core Dump 需编译程序时加-g,通过gdb <可执行文件> <core文件>定位崩溃位置;
  4. 生产环境建议自定义 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),而是有兜底逻辑,确保进程必然终止:

  1. 首先调用 raise(SIGABRT) 向自身发送 SIGABRT 信号;
  2. 若进程捕获了 SIGABRT 且处理函数返回(未终止进程),则 abort()重置 SIGABRT 为默认处理方式
  3. 再次发送 SIGABRT,此时内核强制执行默认行为:终止进程 + 生成 Core Dump(若开启)。
3. 关键特点
  • 必终止进程 :即使捕获了 SIGABRT 信号,abort() 最终也会终止进程(无法忽略);
  • 生成 Core DumpSIGABRT 是带 "核心转储" 属性的信号,开启 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

五、三者的使用场景选择

  1. 进程间通信 / 管控 → 用 kill()比如主进程向子进程发送 "停止 / 继续" 信号、监控进程向业务进程发送告警信号。
  2. 进程自我通知 / 中断 → 用 raise()比如进程检测到错误后,向自身发送 SIGUSR1 信号触发自定义处理逻辑。
  3. 进程主动崩溃 / 调试 → 用 abort()比如程序检测到严重错误(如内存越界、参数非法),调用 abort() 生成 Core Dump,方便后续调试。

六、总结

  1. kill() 是通用信号发送接口,支持跨进程,参数 pid 决定目标范围,是最灵活的选择;
  2. raise()kill(getpid(), sig) 的封装,仅用于进程向自身发信号,使用更简单;
  3. abort() 是专用崩溃接口,固定发送 SIGABRT,且保证进程必然终止,适合主动触发核心转储;
  4. 优先级:跨进程用 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 定时器,重复调用会覆盖之前的设置(返回剩余时间)。

二、工作原理

  1. 进程调用 alarm(n) 时,向内核注册一个 "n 秒后触发" 的定时器,内核记录该定时器的到期时间;
  2. 进程继续执行正常逻辑,内核在后台计时;
  3. 定时器到期后,内核向该进程发送 SIGALRM(14) 信号;
  4. 进程处理 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 类型的变量(避免编译器优化);
  • 不调用非可重入函数(如 printfmalloc,建议仅做 "标记更新",主进程轮询标记处理业务)。

5. 多线程场景的限制

alarm()进程级 定时器,仅向进程的主线程发送 SIGALRM 信号(多线程中无法指定某线程接收信号)。若需线程级定时,需用 pthread_kill() 结合 sleep(),或 timer_create()(POSIX 定时器)。

五、进阶替代方案

alarm() 无法满足需求,可选择更强大的接口:

  1. setitimer():支持微秒级精度,可设置周期性定时器(ITIMER_REAL 模式等价于增强版 alarm());
  2. timerfd_create():将定时器封装为文件描述符,可结合 epoll 监听,适合事件驱动模型;
  3. clock_nanosleep():高精度睡眠,支持绝对时间 / 相对时间,适合同步定时。

六、总结

  1. alarm() 是轻量级的一次性秒级定时器,核心是通过 SIGALRM 信号实现异步定时;
  2. 核心用法:注册 SIGALRM 处理函数 → 调用 alarm(seconds) 设置定时 → 处理信号完成业务逻辑;
  3. 关键坑点:未注册处理函数会导致进程终止,重复调用会覆盖旧定时器;
  4. 适用场景:简单定时通知、限时任务、异步超时检测(无需高精度的场景)

**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. 背后的信号处理逻辑

  1. 信号产生与未决 当用户按 Ctrl+C 时,内核会将 SIGINTpending 位设为 1。但因为 block 位为 1,信号不会立即递达,处于 "未决" 状态。

  2. 信号阻塞与解除 进程可以通过 sigprocmask 修改 block 集:

    • 若将 SIGINTblock 位设为 0(解除阻塞),内核会检查 pending
    • 此时 pending=1handler=SIG_IGN,所以内核会直接丢弃该信号,不会触发任何处理
  3. 信号处理方式的优先级 信号处理的优先级为:自定义处理 > 忽略处理 > 默认处理 。例如 SIGQUIT 绑定了自定义函数,当它解除阻塞且有未决信号时,内核会切换到用户态执行 sighandler


4. 关键结论

  • 内核通过 blockpendinghandler 三个集合,完整管理每个信号的生命周期
  • 信号的 "阻塞" 与 "未决" 是独立状态:阻塞仅影响递达时机,不影响信号的产生
  • 处理方式的配置决定了信号递达后的行为,而用户态的自定义函数是实现业务逻辑的核心

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
通用注意事项
  1. 参数 signo :必须是合法的信号编号(1~64),不能是 0 或无效值(否则函数失败,设置 errno=EINVAL);
  2. 线程安全 :这些函数本身是线程安全的,但操作同一个 sigset_t 变量时需加锁;
  3. 兼容性:遵循 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;
}

运行结果说明

  1. 运行程序后,按 Ctrl+C(SIGINT)或 Ctrl+\(SIGQUIT),信号会被阻塞并标记为 "未决";
  2. 程序每秒查询一次未决信号集,通过 sigismember 检测到未决信号并打印;
  3. 10 秒后解除阻塞,未决的 SIGINT/SIGQUIT 会立即递达(默认行为是终止进程)。

五、常见错误与避坑指南

  1. 未初始化信号集直接操作 ❌ 错误:sigset_t set; sigaddset(&set, SIGINT);(set 未初始化,内容随机);✅ 正确:先调用 sigemptyset(&set),再调用 sigaddset

  2. 传入无效信号编号 ❌ 错误:sigaddset(&set, 100);(100 不是合法信号编号);✅ 正确:仅使用系统定义的信号宏(如 SIGINTSIGUSR1)或确认有效的编号(1~64)。

  3. 混淆 "阻塞集" 和 "未决集" 的操作

    • sigprocmask 操作的是阻塞集(block);
    • sigpending 获取的是未决集(pending);
    • 两者都用 sigset_t 表示,但含义不同,需区分。
  4. 忽略函数返回值 ❌ 错误:直接调用 sigaddset(&set, SIGINT); 不检查返回值;✅ 正确:检查返回值,通过 perror 打印错误原因(如权限问题、无效信号)。

六、总结

  1. 这 5 个函数是操作 sigset_t 信号集的唯一标准方式,保证跨系统兼容性;
  2. 核心流程:sigemptyset(初始化)→ sigaddset/sigdelset(添加 / 删除信号)→ sigismember(验证);
  3. sigfillset 用于 "全包含" 场景,sigemptyset 是所有操作的前置步骤;
  4. 结合 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=NULLoldset 保存当前阻塞集。

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_BLOCKset 包含要阻塞的信号。

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_UNBLOCKset 包含要解除阻塞的信号。

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 setoldset 指针无效 指向非法内存(如空指针、只读内存)

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,控制单个线程的信号掩码

六、总结

  1. sigprocmask() 是修改进程信号掩码(阻塞集)的唯一标准接口,核心是通过 how 参数控制阻塞集的修改方式;
  2. 核心流程:构造信号集(sigemptyset/sigaddset)→ 调用 sigprocmask 修改阻塞集 → (可选)通过 oldset 还原;
  3. 关键特性:无法阻塞 SIGKILL/SIGSTOP,解除阻塞时会立即处理未决信号;
  4. 适用场景:临时阻塞信号(避免关键逻辑被打断)、选择性放行信号、查询当前阻塞状态。

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. 信号产生 → 内核将未决集对应位设为 1
    2. 若信号未被阻塞 → 立即递达(处理),未决集对应位设为 0
    3. 若信号被阻塞 → 保持未决状态,直到解除阻塞(递达后未决位清零);
  • 区别 :非实时信号(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;
}

运行结果

  1. 运行程序后,按 Ctrl+C(触发 SIGINT);
  2. 程序输出 "检测到 SIGINT 处于未决状态";
  3. 解除阻塞后,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 发送信号,触发未决状态

六、总结

  1. sigpending() 是查询进程未决信号集的唯一标准接口,核心作用是 "查看哪些信号已产生但被阻塞";
  2. 核心流程:阻塞信号(sigprocmask)→ 产生信号(kill/ 手动触发)→ 查询未决集(sigpending)→ 解析未决状态(sigismember);
  3. 关键特性:仅能检测 "是否未决",无法获取信号数量;非实时信号未决集不排队,实时信号排队但无法通过该函数感知数量;
  4. 适用场景:调试信号阻塞逻辑、验证信号未决状态、实现 "信号触发后延迟处理" 的逻辑。

四.捕捉信号

1. 进入内核态:信号处理的触发前提

  • 触发场景 :进程在用户态执行时,因 *中断(如时钟中断)、异常(如除零错误)或系统调用(如 read)*主动进入内核态。
  • 核心作用:这是信号处理的唯一时机 ------ 内核只会在 "从内核态返回用户态" 前检查并处理信号,不会主动打断用户态执行。

2. 内核检查并准备处理信号

  • 核心函数 :内核执行 do_signal(),检查当前进程的未决信号集信号掩码,判断哪些信号可以递达(未被阻塞且未决)。
  • 关键逻辑
    1. 遍历未决信号集,找到第一个可以递达的信号。
    2. 根据信号的 handler 确定处理方式(默认、忽略、自定义)。

3. 切换回用户态执行信号处理函数

  • 核心逻辑 :若信号绑定了自定义处理函数 ,内核会修改进程的用户态上下文(如栈、程序计数器),让进程返回用户态时,直接跳转到信号处理函数 sighandler 执行,而非回到原主控制流程。
  • 为什么不直接在内核态执行:用户态的处理函数属于进程代码,内核无法直接执行用户空间的指令,必须切换回用户态才能安全执行。

4. 信号处理函数返回,触发 sigreturn 系统调用

  • 核心动作 :信号处理函数执行完成后,会自动调用特殊的系统调用 sigreturn(),再次进入内核态。
  • 作用sigreturn() 是内核提供的 "收尾接口",用于恢复进程在进入信号处理函数前的用户态上下文(栈、寄存器、程序计数器等)。

5. 内核恢复上下文,回到原主控制流程

  • 核心函数 :内核执行 sys_sigreturn(),恢复之前保存的主控制流程上下文。
  • 最终结果 :进程从内核态返回用户态时,会从上次被中断的指令处继续执行,实现 "信号处理完成后无缝恢复主流程" 的效果。

关键技术细节总结

  1. 异步性与时机控制:信号处理不是实时打断,而是延迟到 "内核态返回用户态" 时处理,避免频繁切换态导致性能损耗。
  2. 上下文切换的完整性:内核会保存两次上下文 ------ 第一次是主流程被中断时的上下文,第二次是信号处理函数执行时的上下文,确保最终能完全恢复。
  3. 自定义处理的安全性 :信号处理函数运行在用户态,避免了内核直接执行用户代码的风险,同时通过 sigreturn 保证了上下文的正确恢复。
  4. 默认 / 忽略处理的简化:若信号处理方式为默认或忽略,内核会直接在内核态完成处理(如终止进程、丢弃信号),无需切换回用户态执行额外逻辑。

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),参数为信号编号。
  • 适用场景:无需获取信号附加信息的普通场景(如处理 SIGINTSIGALRM)。
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 信号打断的系统调用(如 readwrite)自动重启,避免返回 -1errno=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. 处理函数的可重入性

  • 信号处理函数会异步打断进程执行,需遵循 "可重入原则":
    1. 仅使用 volatile sig_atomic_t 类型的变量;
    2. 不调用非可重入函数(如 printfmallocstrcpy);
    3. 简化处理逻辑(仅做标记,主进程轮询标记处理业务)。

3. sa_handlersa_sigaction 互斥

  • sa_flags 未设置 SA_SIGINFO,内核使用 sa_handler
  • 若设置了 SA_SIGINFO,内核使用 sa_sigactionsa_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);

六、总结

  1. sigaction() 是注册信号处理方式的标准接口 ,功能远超 signal(),是工业级代码的首选;
  2. 核心能力:绑定普通 / 扩展处理函数、控制处理期间的阻塞掩码、设置信号处理行为(如自动重启系统调用);
  3. 核心流程:初始化 struct sigaction → 配置处理函数 / 掩码 / 标志位 → 调用 sigaction() 注册 → (可选)通过 oldact 恢复旧配置;
  4. 关键坑点:处理函数需保证可重入,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 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

相关推荐
强风7942 小时前
Linux—应用层自定义协议与序列化
运维·服务器·网络
白日梦想家6812 小时前
第三篇:Node.js 性能优化实战:提升服务并发与稳定性
linux·编辑器·vim
晚风吹长发2 小时前
初步了解Linux中的线程概率及线程控制
linux·运维·服务器·开发语言·c++·centos·线程
i建模2 小时前
在 Ubuntu 中为 npm 切换国内镜像源
linux·ubuntu·npm
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [drivers][gpio]gpio
linux·笔记·学习
Art&Code2 小时前
M系列Mac保姆级教程:Clawdbot安装+API配置,30分钟解锁AI自动化!
运维·macos·自动化
玉梅小洋2 小时前
GitHub SSH配置教程
运维·ssh·github
等什么君!2 小时前
Docker 数据卷:MySQL 数据同步实战
运维·docker·容器
礼拜天没时间.2 小时前
《Docker实战入门与部署指南:从核心概念到网络与数据管理》:环境准备与Docker安装
运维·网络·docker·容器·centos