一. 信号的核心认知
1. 信号的本质:进程的 “异步中断短信”
可以把信号理解为:内核 / 其他进程给目标进程发的 “紧急短信” ——
- 「发送方」:内核(硬件异常、终端操作)、其他进程(
kill命令)、进程自身(abort());- 「接收方」:目标进程(PID 标识);
- 「核心特性」:
- 异步性:信号到达不可预测(进程可能在执行任意代码时被信号打断);
- 轻量性:仅传递 “事件类型”(如 “终端中断”“子进程退出”),无复杂数据(实时信号可传少量附加数据);
- 强制性:部分信号(
SIGKILL/SIGSTOP)无法被忽略 / 捕获,内核强制执行默认动作。2. 信号 vs 信号量
两者名称相似但完全无关,核心区别如下:
特性 信号(Signal) 信号量(Semaphore) 核心作用 异步通知进程 “某事件发生” 同步 / 互斥,保护临界资源(如共享内存) 数据传递 仅传递 “事件类型”(无数据) 无数据(仅通过计数器控制资源) 触发方式 异步(不可预测) 同步(进程主动调用 semop)处理方式 忽略 / 默认动作 / 自定义函数 P 操作(减 1)/V 操作(加 1) 典型场景 进程终止、异常通知、异步控制 多进程互斥访问共享资源 3. 信号的分类(实时 / 非实时,可靠 / 不可靠)
Linux 定义了 64 种信号(编号 1-64),分为两类,核心差异是 “是否排队、是否丢失”:
类型 编号范围 别名 核心特点 示例 非实时信号 1-31 不可靠信号 重复发送的同名信号会合并(丢失) SIGINT/SIGTERM实时信号 34-64 可靠信号 重复发送的同名信号会排队(不丢失) SIGRTMIN/SIGRTMAX4. 开发必记的核心信号(高频使用)
无需记全 64 种,重点掌握以下 8 种即可覆盖 90% 的开发场景:
信号名 编号 触发场景 默认动作 能否捕获 / 忽略 SIGINT2 终端中断(Ctrl+C) 终止进程 ✅ 可以 SIGTERM15 正常终止( kill命令默认)终止进程 ✅ 可以 SIGKILL9 强制终止( kill -9)终止进程 ❌ 绝对不能 SIGSTOP19 暂停进程( kill -19)暂停进程 ❌ 绝对不能 SIGCONT18 恢复暂停的进程( kill -18)(变成后台进程)恢复进程 ✅ 可以 SIGCHLD17 子进程状态变化(退出 / 暂停 / 恢复) 忽略 ✅ 可以 SIGSEGV11 段错误(访问非法内存) 终止 + 生成 core 转储文件 ✅ 可以(无意义) SIGALRM14 定时器到期( alarm()触发)终止进程 ✅ 可以 核心重点:
SIGKILL(9)和SIGSTOP(19)是内核的 “终极控制手段”—— 无法被捕获、无法被忽略、无法自定义处理,目的是保证管理员能强制控制任何进程(避免进程 “劫持” 所有信号导致无法终止)。
二. 核心作用:
信号的核心作用可以一句话概括:作为 Linux 内核 / 进程向目标进程发送的「异步事件通知」,实现对进程的 “远程控制” 和 “异常告知”,是进程间极简但关键的通信方式。
下面列出信号的具体作用 + 实战场景,每个作用都对应你能直接感知 / 使用的实际案例:
1. 最核心:进程的 “强制 / 优雅控制”(日常运维 / 开发高频用)
这是信号最基础、最常用的作用 —— 控制进程的启动 / 暂停 / 终止,是运维和开发中接触最多的场景:
信号 具体作用 实战例子 SIGINT(2)终端中断进程(用户主动终止前台进程)
按 Ctrl+C终止正在运行的python test.py进程SIGTERM(15)优雅终止进程(默认 kill 命令) kill 12345给 PID=12345 的进程发终止信号,进程可先保存数据再退出SIGKILL(9)强制终止进程(无法拦截,终极手段) kill -9 12345强制杀死 “卡死” 的进程(如死循环进程)SIGSTOP(19)暂停进程(无法拦截) kill -19 12345暂停后台运行的ffmpeg转码进程SIGCONT(18)恢复暂停的进程(变成后台进程) kill -18 12345恢复上述暂停的ffmpeg进程核心价值:无需侵入进程代码,通过命令 / 系统调用就能远程控制进程的生命周期(比如运维杀进程、调试时暂停进程)。
2. 异常告知:通知进程 “出错了”(内核→进程)
当进程触发系统异常时,内核会给进程发信号,告知 “你出问题了,需要处理”:
信号 具体作用 实战场景 SIGSEGV(11)告知进程 “访问了非法内存”(段错误) 进程代码中 int *p = NULL; *p = 10;会触发该信号,默认终止并生成 core 转储文件(用于调试)SIGPIPE(13)告知进程 “向无读端的管道 / 套接字写数据” 进程 A 往管道写数据,但进程 B(读端)已退出,内核发该信号,默认终止进程 SIGFPE(8)告知进程 “算术异常”(如除 0、溢出) 代码中 int a = 1/0;触发该信号,默认终止进程核心价值:进程可捕获这类信号(如
SIGSEGV),在终止前做 “最后补救”(比如保存日志、释放资源),或生成调试文件定位问题。3. 异步事件响应:处理 “不可预测的事件”(进程→进程 / 内核→进程)
进程需要响应 “异步发生的事件”(无法提前知道何时发生),信号是最直接的方式:
1. 回收子进程(避免僵尸进程)
SIGCHLD(17):子进程退出 / 暂停 / 恢复时,内核给父进程发该信号 —— 父进程捕获后调用waitpid()回收子进程,避免僵尸进程占用 PID 资源。
- 实战场景:多进程程序中,父进程无需轮询 “子进程是否退出”,只需捕获
SIGCHLD异步处理。2. 实现定时器
SIGALRM(14):进程调用alarm(5)后,内核会在 5 秒后给进程发该信号 —— 实现 “定时任务”,无需手动循环等待。
- 实战场景:给网络请求设置超时(比如 3 秒没响应就终止请求)。
3. 进程间 “极简指令通信”
进程 A 给进程 B 发自定义信号(如
SIGUSR1(10)/SIGUSR2(12)),B 捕获后执行特定逻辑 —— 无需复杂的消息队列 / 共享内存,仅传递 “指令”。
- 实战场景:
- 后台服务进程收到
SIGUSR1后,自动刷新配置文件(无需重启进程);- 爬虫程序收到
SIGUSR2后,暂停爬取并保存进度。4. 进阶:自定义业务逻辑触发(开发场景)
进程可捕获特定信号,覆盖默认动作,执行自定义逻辑 —— 实现 “优雅退出”“紧急处理” 等需求:
- 场景 1:优雅退出进程捕获
SIGTERM(15),不是直接退出,而是先:① 关闭打开的文件句柄;② 保存内存中的数据到磁盘;③ 释放网络连接;④ 再退出。
- 实战:数据库服务、Web 服务(如 Nginx)收到
SIGTERM时,会先停止接收新请求,处理完已有请求后再退出。- 场景 2:紧急备份进程捕获
SIGINT(2)(Ctrl+C),执行 “紧急备份” 逻辑(比如把临时数据写入文件),再终止。- 场景 3:实时信号传少量数据用实时信号(
SIGRTMIN-SIGRTMAX)给进程传递少量数据(如数字、指针),实现 “带指令 + 参数” 的极简通信。
三. 信号的生命周期(核心机制,必须理解)
信号从 “产生” 到 “被处理” 分为 5 个阶段,这是理解信号行为的关键(进程的 PCB 是信号管理的核心载体):
1. 产生(Generation):信号的 “触发来源”
信号由以下 3 类主体触发:
- 内核触发:硬件异常(如
SIGSEGV)、终端操作(如SIGINT)、定时器到期(SIGALRM)、子进程退出(SIGCHLD);- 进程触发:其他进程通过
kill()/sigqueue()发送(如kill -15 12345给 PID=12345 的进程发SIGTERM);- 自身触发:进程调用
raise()/abort()给自己发信号(如abort()强制发SIGABRT终止自身)。alarm(); 闹钟产生信号2. 注册(Registration):内核记录信号
内核将信号 “登记” 到目标进程的 PCB(进程控制块)中,核心是修改两个关键集合:
- 未决信号集(pending):标记 “已产生但未处理” 的信号(位图结构,1 表示未决);
- 信号屏蔽字(block):标记 “暂时屏蔽” 的信号(屏蔽的信号不会递达,仅保留在未决集)。
注册规则:
- 非实时信号(1-31):重复注册同名信号,仅标记一次(可能丢失,如连续发 3 次
SIGINT,未决集仅记 1 次);- 实时信号(34+):重复注册同名信号,会排队记录(不丢失,每发一次都记)。
3. 未决(Pending):信号 “待处理”
信号已注册到未决集,但暂时不处理,原因包括:
- 信号被屏蔽字(block)屏蔽;
- 进程正在处理同类型的信号(自定义处理函数未执行完)。
pending 表(也叫「未决信号集」)是 Linux 内核为每个进程维护的一张位图清单(存在进程的 PCB 里),本质是标记「已经产生、但进程暂时还没处理(递达)」的信号 —— 简单说,就是进程的 “信号待办清单”:
- 位图的每一位对应一个信号(比如第 2 位对应
SIGINT、第 15 位对应SIGTERM);- 位值为
1→ 该信号 “已产生但未处理”(未决);- 位值为
0→ 该信号 “无待办”(没产生,或已处理)。4. 递达(Delivery):信号 “交付处理”
内核将信号从未决集中移除,交付给进程处理,触发条件:
- 信号未被屏蔽;
- 进程无同类型信号正在处理;
- 进程处于可调度状态(未被暂停)。
5. 处理(Handling):进程响应信号
进程收到递达的信号后,有 3 种处理方式(优先级:自定义 > 忽略 > 默认):
- 默认动作:内核预设的行为(如终止、暂停、忽略、生成 core 文件);
- 忽略(Ignore):进程收到信号后不做任何操作(如
SIGCHLD默认忽略);- 自定义处理函数(Handler):进程提前注册函数,信号递达时异步执行该函数。
三个特殊的表:
block 表、Pending 表、handler 表是 Linux 进程 PCB 中管理信号的三大核心表,分别管 “是否暂不处理”“是否已产生未处理”“处理时该做什么”,三者配合完成信号的全生命周期管控:
维度 block 表(信号屏蔽字) Pending 表 (未决信号集)
handler 表(信号处理动作表) 核心定义 进程主动设置的 “暂时不想处理的信号清单” 已产生但未递达的信号 “暂存清单” 每个信号对应的 “处理规则清单” 本质 控制信号递达时机(是否暂时拦住) 记录信号未决状态(是否已产生待处理) 定义信号处理方式(递达后该怎么做) 存储结构 64 位位图(1 位对应 1 个信号,1 = 屏蔽) 64 位位图 + 实时信号队列(1 = 未决;实时信号排队) 数组(下标 = 信号编号,值 = 处理动作:默认 / 忽略 / 捕捉函数地址) 核心作用 决定信号是否能递达(屏蔽则暂存 pending) 暂存被屏蔽 / 暂未处理的信号 决定信号递达后执行什么逻辑 操作方式 用 sigprocmask+sigset_t修改内核自动维护(用户仅能通过 sigpending查询)用 signal/sigaction修改关键特点 SIGKILL/SIGSTOP 无法屏蔽 非实时信号合并,实时信号排队 SIGKILL/SIGSTOP 无法修改(强制默认动作) 通俗关联逻辑(一句话讲清三者配合)
内核处理信号时:
- 先查 block 表 → 若信号被屏蔽 → 存入 Pending 表 暂存;
- 若未被屏蔽 → 从 Pending 表 移除该信号 → 查 handler 表 → 按规则(默认 / 忽略 / 捕捉)处理。
命令行方式发送信号
适用于终端手动给进程发信号,核心工具是
kill、killall、pkill,其中kill是最基础、最精准的(按 PID 定位进程)。1. 核心:kill 命令(按 PID 发送,精准)
这是向单个进程发信号的首选方式,语法:
# 格式1:用信号编号(推荐,简洁) kill -<信号编号> <进程PID> # 格式2:用信号名(更易读,可省略SIG前缀) kill -<信号名> <进程PID> kill -SIG<信号名> <进程PID> # 格式3:默认发送 SIGTERM(15)(优雅终止) kill <进程PID>关键前提:先通过
ps/top/pgrep找到目标进程的 PID(进程唯一标识):# 示例:查找 python test.py 进程的 PID pgrep -f "python test.py" # 输出:12345(假设PID是12345) ps aux | grep "python test.py" # 也能查到PID实战示例(以 PID=12345 为例):
# 1. 发送默认信号 SIGTERM(15)(优雅终止进程) kill 12345 # 2. 发送 SIGKILL(9)(强制终止,终极手段) kill -9 12345 # 等价于 kill -SIGKILL 12345 kill -KILL 12345 # 3. 发送 SIGINT(2)(模拟 Ctrl+C 中断) kill -2 12345 # 等价于 kill -SIGINT 12345 # 4. 发送自定义信号 SIGUSR1(10)(让进程执行自定义逻辑,如刷新配置) kill -10 12345 kill -SIGUSR1 123452. 补充:killall 命令(按进程名发送,给所有同名进程发)
如果不想查 PID,可按进程名发送,但会给所有同名进程发信号(适合单实例进程):
# 格式:killall [信号] 进程名 killall -9 python # 强制终止所有 python 进程 killall -SIGUSR1 nginx # 给所有 nginx 进程发 SIGUSR1(刷新配置)3. 补充:pkill 命令(按关键词匹配进程发送)
更灵活,可按进程名 / 命令行关键词匹配,适合复杂场景:
# 格式:pkill [信号] -f 进程关键词 pkill -9 -f "python test.py" # 强制终止所有命令行包含 "python test.py" 的进程 pkill -SIGINT -f "myapp" # 给所有含 "myapp" 的进程发 SIGINT注意: 普通用户只能给「自己启动的进程」发信号;root 用户可以给任意进程发信号(包括系统进程)。如果普通用户给 root 进程发信号,会报错
Operation not permitted。
代码方式(开发场景,程序内发送)
信号的操作函数
Linux 提供了一系列函数用于「发送信号」「设置处理方式」「管理信号集」「等待信号」,下面按使用场景分类讲解,所有示例均可直接运行。
1. 发送信号(给进程发 “短信”)
核心函数:
kill()(通用)、raise()(给自己发)、alarm()(定时器)、sigqueue()(实时信号带数据)。
函数 原型 功能描述 kill()int kill(pid_t pid, int sig);给指定 PID 的进程 / 进程组发信号 raise()int raise(int sig);给自己发信号(等价于 kill(getpid(), sig))alarm()unsigned int alarm(unsigned int seconds);设置定时器,n 秒后内核发 SIGALRMsigqueue()int sigqueue(pid_t pid, int sig, const union sigval value);发送实时信号,可附带少量数据
kill()函数 - 向指定进程发送信号1.1 函数原型
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);1.2 参数详解
pid参数的含义:
pid值 含义 示例 pid > 0发送给进程ID为pid的进程 kill(1234, SIGTERM)pid = 0发送给同一进程组的所有进程 kill(0, SIGHUP)pid = -1发送给所有有权限的进程(除init进程) kill(-1, SIGKILL)pid < -1发送给进程组ID为 -pid的所有进程kill(-5678, SIGTERM)
sig参数:
信号编号(1-64)
如果
sig=0,不发送信号,只检查目标进程是否存在和权限1.3 返回值
成功:返回0
失败:返回-1,设置
errno1.4 常见错误码
EPERM // 没有权限向目标进程发送信号 ESRCH // 目标进程不存在 EINVAL // 无效的信号编号1.5 使用示例
#include <stdio.h> #include <signal.h> #include <stdlib.h> #include <unistd.h> int main() { pid_t child_pid; // 创建子进程 child_pid = fork(); if (child_pid == 0) { // 子进程 printf("Child process (PID=%d) started\n", getpid()); while(1) { printf("Child is running...\n"); sleep(1); } } else { // 父进程 sleep(3); // 让子进程运行3秒 printf("Parent sending SIGTERM to child (PID=%d)\n", child_pid); // 发送终止信号 if (kill(child_pid, SIGTERM) == -1) { perror("kill failed"); exit(1); } // 等待子进程结束 wait(NULL); printf("Child process terminated\n"); } return 0; }二、
raise()函数 - 向自身发送信号2.1 函数原型
#include <signal.h> int raise(int sig);2.2 与
kill()的关系// 完全等价于 raise(sig); ⇔ kill(getpid(), sig);2.3 使用场景
异常处理中终止自身
触发信号处理函数的测试
多线程程序中的信号处理
2.4 使用示例
#include <stdio.h> #include <signal.h> #include <stdlib.h> // 信号处理函数 void signal_handler(int sig) { printf("Caught signal %d\n", sig); switch(sig) { case SIGTERM: printf("Graceful shutdown requested\n"); // 清理资源 exit(0); break; case SIGINT: printf("Interrupt received\n"); // 不退出,继续运行 break; } } int main() { // 设置信号处理函数 signal(SIGTERM, signal_handler); signal(SIGINT, signal_handler); printf("Process PID: %d\n", getpid()); // 示例1:手动触发SIGINT printf("\nSending SIGINT to self...\n"); raise(SIGINT); // 示例2:模拟异常情况,发送SIGABRT printf("\nSimulating error condition...\n"); int* ptr = NULL; if (ptr == NULL) { fprintf(stderr, "Null pointer detected!\n"); raise(SIGABRT); // 引发SIGABRT信号 } // 这行不会执行,因为SIGABRT会终止进程 printf("This line won't be printed\n"); return 0; }三、
alarm()函数 - 设置定时器信号3.1 函数原型
#include <unistd.h> unsigned int alarm(unsigned int seconds);3.2 特性
单一定时器:每个进程只能有一个
alarm定时器非阻塞:设置后立即返回,不阻塞进程
精度为秒:最小单位是1秒
信号SIGALRM:定时器到期后发送
SIGALRM信号(默认动作是终止进程)3.3 参数和返回值
seconds:定时器秒数
0:取消之前的定时器
>0:设置新的定时器,覆盖之前的返回值:之前设置的定时器剩余秒数
3.4 使用示例
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <stdlib.h> // 定时器处理函数 void alarm_handler(int sig) { if (sig == SIGALRM) { printf("ALARM! Time's up!\n"); exit(1); } } int main() { signal(SIGALRM, alarm_handler); printf("Starting 5-second alarm...\n"); // 设置5秒定时器 unsigned int remaining = alarm(5); printf("Previous alarm had %u seconds remaining\n", remaining); // 模拟长时间操作 for(int i = 1; i <= 10; i++) { printf("Working... %d seconds passed\n", i); // 每2秒检查一次 sleep(2); // 获取剩余时间 remaining = alarm(0); // 0表示不设置新定时器,只获取剩余时间 printf("Alarm remaining: %u seconds\n", remaining); // 重新设置定时器(续期) alarm(remaining > 0 ? remaining : 5); } printf("Work completed before alarm\n"); alarm(0); // 取消定时器 return 0; }四、
sigqueue()函数 - 发送实时信号并携带数据4.1 函数原型
#include <signal.h> int sigqueue(pid_t pid, int sig, const union sigval value);4.2 参数详解
pid参数:
必须是具体的进程ID(不能是进程组)
sig参数:
优先使用实时信号(
SIGRTMIN到SIGRTMAX)也可使用普通信号,但可能丢失附加数据
value参数(union sigval):union sigval { int sival_int; // 传递整型数据 void *sival_ptr; // 传递指针(需确保有效性) };4.3 使用条件
发送者和接收者都必须使用
sigqueue()接收者需要使用
sigaction()设置SA_SIGINFO标志接收者处理函数原型:
void handler(int sig, siginfo_t *info, void *context)4.4 使用示例
发送端代码:
#include <stdio.h> #include <signal.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <target_pid>\n", argv[0]); exit(1); } pid_t target_pid = atoi(argv[1]); printf("Sending signals to PID %d\n", target_pid); union sigval value; // 示例1:发送整型数据 printf("\nSending integer data...\n"); value.sival_int = 12345; if (sigqueue(target_pid, SIGRTMIN, value) == -1) { perror("sigqueue failed"); exit(1); } // 示例2:发送指针数据 printf("\nSending string data...\n"); char *message = "Hello from sender!"; value.sival_ptr = message; if (sigqueue(target_pid, SIGRTMIN + 1, value) == -1) { perror("sigqueue failed"); exit(1); } // 示例3:批量发送带序号的信号 printf("\nSending multiple signals...\n"); for (int i = 0; i < 5; i++) { value.sival_int = i * 100; if (sigqueue(target_pid, SIGRTMIN + 2, value) == -1) { perror("sigqueue failed"); } usleep(100000); // 延迟0.1秒 } printf("All signals sent\n"); return 0; }接收端代码:
#include <stdio.h> #include <signal.h> #include <stdlib.h> #include <string.h> #include <unistd.h> // 信号处理函数(带附加信息) void signal_handler(int sig, siginfo_t *info, void *context) { printf("\nReceived signal: %d\n", sig); printf(" Sender PID: %d\n", info->si_pid); printf(" Sender UID: %d\n", info->si_uid); // 检查是否来自sigqueue if (info->si_code == SI_QUEUE) { printf(" Signal was queued with data\n"); // 根据信号类型处理数据 if (sig == SIGRTMIN) { printf(" Integer data: %d\n", info->si_value.sival_int); } else if (sig == SIGRTMIN + 1) { printf(" String data: %s\n", (char*)info->si_value.sival_ptr); } else if (sig == SIGRTMIN + 2) { printf(" Batch data: %d\n", info->si_value.sival_int); } } else { printf(" Signal was not queued (no data)\n"); } } int main() { printf("Receiver process PID: %d\n", getpid()); printf("Waiting for queued signals...\n"); struct sigaction sa; sigset_t newmask; // 设置信号处理 sa.sa_sigaction = signal_handler; // 使用三参数处理函数 sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO; // 重要:启用附加信息 // 注册实时信号处理 sigaction(SIGRTMIN, &sa, NULL); sigaction(SIGRTMIN + 1, &sa, NULL); sigaction(SIGRTMIN + 2, &sa, NULL); // 阻塞普通信号,只处理实时信号 sigemptyset(&newmask); sigaddset(&newmask, SIGINT); sigaddset(&newmask, SIGTERM); sigprocmask(SIG_BLOCK, &newmask, NULL); // 等待信号 while(1) { pause(); // 等待任何信号 } return 0; }2. 设置信号处理方式(捕获信号Signal Catching)
捕获信号是 Linux 进程主动定制信号处理逻辑的机制:进程为指定信号注册一个自定义处理函数(Handler),当该信号递达时,内核不再执行默认动作(如终止、暂停、忽略),而是调用这个自定义函数来处理信号。
简单说:捕获信号 = 给信号 “绑定自己的处理逻辑”,让信号按你的代码执行,而非内核预设行为。
进程通过两个核心函数注册捕获逻辑:
函数 特点 适用场景 signal()入门级,接口简单 测试 / 简单场景 sigaction()生产级,功能完整(支持信号掩码、信号携带数据) 所有实战场景 (1)
signal():入门级(简单但有缺陷)// 函数原型:sighandler_t 是函数指针,参数为信号编号,无返回值 typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
handler可选值:
SIG_DFL:执行默认动作;SIG_IGN:忽略信号;- 自定义函数指针:执行自定义逻辑。
示例:捕获 Ctrl+C(SIGINT),拒绝终止进程
#include <stdio.h> #include <signal.h> #include <unistd.h> // 自定义信号处理函数(捕获 SIGINT 后执行) void sigint_handler(int sig) { printf("\n捕获到信号 %d(SIGINT),正在优雅退出...\n", sig); // 执行自定义逻辑:保存数据、释放资源 printf("数据已保存,资源已释放\n"); _exit(0); // 异步安全的退出函数 } int main() { // 注册信号捕获:将 SIGINT 的处理方式替换为 sigint_handler signal(SIGINT, sigint_handler); // 入门级用法 // 生产级推荐用 sigaction,支持更多配置 // struct sigaction sa; // sa.sa_handler = sigint_handler; // sigemptyset(&sa.sa_mask); // sa.sa_flags = 0; // sigaction(SIGINT, &sa, NULL); printf("程序运行中,按 Ctrl+C 触发捕获逻辑\n"); while (1) { sleep(1); // 模拟业务逻辑 } return 0; }运行后按 Ctrl+C,进程不会终止,而是打印自定义提示 —— 因为我们覆盖了
SIGINT的默认 “终止” 动作。实例: 信号忽略(忽略 SIGINT,按 Ctrl+C 完全无反应)
#include <stdio.h> #include <signal.h> #include <unistd.h> int main() { // 注册:忽略 SIGINT(Ctrl+C),SIG_IGN 表示“忽略” signal(SIGINT, SIG_IGN); printf("进程运行中,按 Ctrl+C 无反应(已忽略)...\n"); while (1) sleep(1); return 0; }“屏蔽(block)” 和 “忽略(ignore)” 是不同的,“忽略” 是信号的处理方式(默认 / 忽略 / 捕捉),“屏蔽” 是信号是否能递达的控制。如果设置了 SIG_IGN,即使信号产生了,也不会进 pending 表
(2)
sigaction():生产级(推荐,功能更全)
signal()存在跨平台兼容性问题,且无法设置高级属性(如屏蔽嵌套信号),生产环境优先用sigaction():// 函数原型 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); // 核心结构体:定义信号处理规则 struct sigaction { void (*sa_handler)(int); // 普通处理函数(同 signal()) void (*sa_sigaction)(int, siginfo_t *, void *); // 带数据的处理函数(实时信号用) sigset_t sa_mask; // 处理该信号时,屏蔽的信号集(避免嵌套) int sa_flags; // 标志位(如 SA_RESTART 重启被中断的系统调用) };示例:用
sigaction捕获 SIGINT,处理时屏蔽其他信号#include <stdio.h> #include <signal.h> #include <unistd.h> void sigint_handler(int sig) { const char *msg1 = "\n处理 SIGINT 中,屏蔽 SIGTERM...\n"; write(STDOUT_FILENO, msg1, __builtin_strlen(msg1)); sleep(3); // 模拟耗时处理(期间发 SIGTERM 会被屏蔽) const char *msg2 = "SIGINT 处理完成\n"; write(STDOUT_FILENO, msg2, __builtin_strlen(msg2)); } int main() { struct sigaction act; // 1. 设置自定义处理函数 act.sa_handler = sigint_handler; // 2. 清空屏蔽集 → 添加要屏蔽的信号(处理 SIGINT 时,屏蔽 SIGTERM) sigemptyset(&act.sa_mask); sigaddset(&act.sa_mask, SIGTERM); // 3. 标志位:SA_RESTART 让被信号中断的系统调用(如 read)自动重启 act.sa_flags = SA_RESTART; // 注册信号处理规则 if (sigaction(SIGINT, &act, NULL) == -1) { perror("sigaction"); return 1; } printf("进程运行中(PID:%d),按 Ctrl+C 测试...\n", getpid()); while (1) { sleep(1); } return 0; }运行后:
- 按 Ctrl+C 进入处理函数,此时执行
kill -15 进程ID发SIGTERM,会被屏蔽;- 处理函数执行完后,
SIGTERM才会递达(若未屏蔽则执行默认终止动作)。
特性 signal()sigaction()跨平台兼容性 极差(System V/BSD 行为不一致) 符合 POSIX 标准,所有 Linux 一致 处理函数自动重置 是(System V)/ 否(BSD) 永不重置,行为可控 信号屏蔽集(sa_mask) 不支持 支持(避免信号重入) 被中断系统调用控制 无(需手动重启) 支持 SA_RESTART(自动重启) 实时信号附加数据 不支持 支持(sa_sigaction) 扩展属性配置 无(如 SA_NOCLDSTOP) 支持所有信号属性 如果你写的 SIGINT 处理函数正在往文件里写数据(保存进度),刚写了一半,又来一个 SIGINT 信号,又触发这个处理函数,两个函数同时写同一个文件,最后文件内容就乱了。
sa_mask 为啥能避免嵌套?(核心:临时 “屏蔽名单”)
sa_mask就是给信号处理函数加了一个「临时免打扰模式」——当你执行当前信号的处理函数时,内核会自动把sa_mask里的信号加入 “屏蔽名单”,这些信号就算来了,也会被暂时 “扣下”,等当前处理函数执行完,再释放这些被扣下的信号。内核的自动屏蔽是 “保底操作”,只防最基础的同信号打断;手动设置
sa_mask是 “精准防护”,根据你的业务逻辑,把所有可能干扰处理函数的信号都挡在外面,保证处理函数能完整、安全地执行完。3. 信号集操作(管理屏蔽 / 未决信号)
进程的
pending(未决)和block(屏蔽)集合都是sigset_t类型(位图),需通过专用函数操作:
函数 功能 sigemptyset()清空信号集 sigfillset()填满信号集(包含所有信号) sigaddset()向信号集添加指定信号 sigdelset()从信号集删除指定信号 sigismember()判断信号是否在信号集中(返回 1/0) sigprocmask()设置 / 查询进程的屏蔽字(block) sigpending()查询进程的未决信号集(pending) 后续会详细讲解...
4. 等待信号(暂停进程直到收到信号)
函数 原型 特点 pause()int pause(void);简单但有竞态(信号可能在 pause 前到达,导致进程永久暂停) sigsuspend()int sigsuspend(const sigset_t *mask);原子操作:设置屏蔽字 + 暂停进程,无竞态,推荐使用 示例:用
sigsuspend()安全等待信号#include <stdio.h> #include <signal.h> #include <unistd.h> void sigusr1_handler(int sig) { const char *msg = "收到 SIGUSR1 信号\n"; write(STDOUT_FILENO, msg, __builtin_strlen(msg)); } int main() { // 注册 SIGUSR1 处理函数 signal(SIGUSR1, sigusr1_handler); sigset_t mask, old_mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); // 仅屏蔽 SIGINT,其他信号正常接收 printf("进程暂停,等待 SIGUSR1(执行:kill -10 %d)\n", getpid()); // 原子操作:1. 保存当前屏蔽字 2. 设置新屏蔽字 3. 暂停进程 sigsuspend(&mask); // 恢复原屏蔽字 sigprocmask(SIG_SETMASK, &old_mask, NULL); printf("进程恢复运行\n"); return 0; }
不可重入函数(Non-Reentrant)和可重入函数(Reentrant)
用最通俗的生活例子理解:
- 可重入函数:像自动售货机 —— 你投币买水到一半,有人打断你去买零食,回来你继续投币,售货机仍能正确给你水(逻辑独立、不依赖 “半完成” 的状态);
- 不可重入函数:像手工记账本 —— 你记到一半(写了 “收入 100” 但没写 “元”),有人打断你去记另一笔账,回来你忘了之前写到哪,账本就乱了(依赖全局状态、操作不原子)。
核心定义
函数类型 核心定义 通俗理解 可重入函数 函数执行过程中被异步打断(如信号、中断、多线程调度),再次调用(重入)后,原流程和新流程都能正确执行,结果不受影响 多个人同时用、中途被打断再用,都不会乱,逻辑完全 “自给自足” 不可重入函数 函数执行过程中被异步打断后重入,会导致数据错乱、逻辑异常、结果错误 只能 “一次性干完”,中途被打断就乱套,依赖全局状态 / 共享资源 关键前提:“重入” 的核心是异步打断 + 再次调用—— 比如信号处理函数打断主程序的
malloc,又在信号处理函数里调用malloc,就是典型的 “重入不可重入函数”,必然出问题。本质区别:为什么可重入函数 “不乱”?
可重入与不可重入的核心差异,在于是否依赖 “非私有资源” 和 “非原子操作”,用表格清晰对比:
对比维度 可重入函数 不可重入函数 依赖全局 / 静态变量 ❌ 完全不用(仅用参数 / 局部变量) ✅ 依赖(比如 strtok用静态变量存分割位置)操作共享资源(文件 / 内存) ❌ 仅操作函数内私有资源 ✅ 操作全局共享资源(比如 printf用全局输出缓冲区)调用其他不可重入函数 ❌ 只调用可重入函数 ✅ 调用 malloc/printf等不可重入函数非原子操作 ❌ 仅用原子操作(一步完成) ✅ 有分步操作(比如 “读全局变量→修改→写回”) 内存分配 / 释放 ❌ 不调用 malloc/free(改内存池)✅ 调用 malloc/free不可重入函数的 “坑”:具体怎么乱的?
举个经典例子 ——
strtok(字符串分割函数,不可重入):// 不可重入的根源:strtok用静态变量保存“上次分割的位置” char *strtok(char *str, const char *delim); // 主程序执行: char str[] = "a,b,c,d"; strtok(str, ","); // 第一次调用,静态变量存“b,c,d”的起始地址 // 此时信号触发,信号处理函数里也调用strtok: strtok("x,y,z", ","); // 静态变量被覆盖为“y,z”的起始地址 // 信号处理完,主程序继续调用strtok: strtok(NULL, ","); // 本应取“b”,但静态变量被改,实际取到“y”,结果完全错了而可重入版本
strtok_r(r=reentrant)就解决了这个问题 —— 把 “分割位置” 从静态变量改成参数传入(私有资源),就算重入也不会乱:// 可重入版本:用saveptr(局部变量)保存分割位置,不依赖全局 char *strtok_r(char *str, const char *delim, char **saveptr);常见的可重入 / 不可重入函数举例
1. 可重入函数(放心用,尤其是信号处理 / 多线程)
这些函数仅依赖参数和局部变量,无全局状态,操作原子:
- 内存操作:
memcpy、memset、strcpy、strcmp(仅操作传入的参数);- 系统调用:
write、read、_exit、close(内核级原子操作,不依赖用户态全局资源);- 基础运算:
abs、sqrt(仅处理参数,无副作用)。2. 不可重入函数(绝对不能在信号处理函数里用!)
这些函数依赖全局 / 静态资源,或有分步操作:
- 标准 IO:
printf、fprintf、puts(用全局输出缓冲区,分步写入);- 内存管理:
malloc、free、calloc(操作全局内存池,分步修改链表);- 定时器:
sleep、alarm(修改全局定时器状态);- 字符串处理:
strtok(静态变量)、asctime(静态缓冲区);- 其他:
rand(静态随机数种子)、getenv(全局环境变量表)。关键应用场景:为什么你必须关心?
这部分和“信号捕获” 强相关 ——信号处理函数必须用可重入函数,否则必出问题!
场景 1:信号处理打断不可重入函数
主程序正在执行
malloc(修改全局内存池链表):主程序:malloc → 拆链表节点(只拆了一半) ↓ 信号触发(比如SIGINT) 信号处理函数:又调用malloc → 继续改同一个内存池链表 ↓ 信号处理完,主程序继续 主程序:malloc的链表已经乱了 → 内存泄漏/程序崩溃场景 2:多线程调用不可重入函数
多线程同时调用
printf:线程1:printf("hello") → 写了“he”到全局缓冲区,被调度走 线程2:printf("world") → 覆盖缓冲区为“world”,输出 线程1:继续执行 → 缓冲区剩下的“llo”被输出 最终结果:worldllo(完全错乱)核心原因:信号处理是 “异步打断”,多线程是 “并发执行”,二者都会触发函数的 “重入”—— 不可重入函数扛不住这种场景,可重入函数则完全没问题。
如何编写可重入函数?(实战规则)
只要遵守以下规则,就能写出安全的可重入函数:
- 绝不使用全局 / 静态变量:所有数据都通过参数传入(传值,而非传指针共享),或用函数内局部变量(栈上分配,每个调用独立);
- 绝不调用不可重入函数:比如信号处理函数里,不能用
printf/malloc/sleep,改用write(可重入)输出、_exit退出;- 绝不操作共享资源:不写全局文件、不修改全局配置,仅操作函数内创建的私有资源;
- 只用原子操作:避免 “读 - 改 - 写” 分步操作(比如
count++是三步:读 count→+1→写回,非原子),改用原子指令(如__sync_fetch_and_add);- 不依赖函数执行顺序:函数执行结果仅由输入参数决定,不受 “是否被打断” 影响(纯函数思想)。
正面例子(可重入函数):
// 计算两数之和,仅用参数和局部变量,无全局依赖 int add(int a, int b) { int temp = a + b; // 局部变量,栈上分配,每个调用独立 return temp; } // 信号处理函数里的可重入输出(用write替代printf) void sig_handler(int sig) { char msg[] = "signal caught\n"; write(1, msg, sizeof(msg)-1); // write是可重入的系统调用 _exit(0); // _exit是可重入的退出函数(exit不可重入) }反面例子(不可重入函数):
int count = 0; // 全局变量 // 不可重入:依赖全局变量,count++是非原子操作 int increment() { count++; // 读count→+1→写回,中途被打断会乱 return count; }核心总结
- 核心判断:可重入函数 =“自给自足”(仅用参数 / 局部变量),不可重入函数 =“依赖外部状态”(全局 / 静态 / 共享资源);
- 关键风险:不可重入函数被异步打断(信号)/ 并发调用(多线程)会导致数据错乱,可重入函数则安全;
- 实战要求:信号处理函数、多线程核心逻辑必须用可重入函数,禁用
printf/malloc等不可重入函数;- 编写规则:不碰全局、不调不可重入函数、只用原子操作、结果仅由参数决定。
简单说:可重入函数是 “不怕打断的函数”,不可重入函数是 “一打断就乱的函数”—— 在异步 / 并发场景下,选可重入函数是唯一安全的选择。
SIGCHLD: 一种新的回收子进程的思路
在没学过信号之前,我们回收子进程使用父进程不断的轮询方式,这样会导致CPU的空转,虽然我们可以通过在轮询循环中添加其他task的方式,一边执行task一边工作,这样代码非常不优雅而且CPU依然空转只是频率小了, 如果能让子进程结束后自主通知父进程 , 且把这个通知信号捕获后为其附加辅助函数, 那么就可以非常方便地回收子进程, 且避免CPU空转.------这个通知信号就是SIGCHLD
SIGCHLD 是 Linux 内核为父进程管理子进程设计的核心异步信号,子进程的状态发生改变时,内核会主动向其父进程发送 SIGCHLD 信号,实现 “子进程状态变化→父进程异步感知” 的通信,无需父进程轮询(轮询会持续占用 CPU,效率极低)。
触发 “状态变化” 的场景(信号发送的前提)
内核仅在子进程出现以下 3 类状态变化时发送 SIGCHLD:
状态变化类型 具体场景 优先级 / 常见度 终止类(核心) 1. 子进程正常终止:调用 exit()/_exit()主动退出;2. 子进程异常终止:被 SIGKILL/SIGSEGV 等信号杀死最高 / 最常见 暂停类 子进程被 SIGSTOP/SIGTSTP 等信号暂停执行 中 / 较少见 恢复类 被暂停的子进程被 SIGCONT 信号恢复运行 中 / 较少见 关键特性:信号的 “定向性”——SIGCHLD 只会发送给子进程的直接父进程,祖父进程 / 其他进程不会收到;若父进程已退出,子进程被 init/PID1 接管,信号则发送给 init 进程。
核心价值作用:解决僵尸进程问题(最关键)
这是 SIGCHLD 信号存在的核心意义,也是工作中处理该信号的首要目标。
1. 先理解:僵尸进程的成因(为什么需要 SIGCHLD 解决)
子进程终止后,内核不会立刻释放其所有资源(比如 PID、进程表项、终止状态),而是会保留这些信息,直到父进程通过
wait()/waitpid()读取 ——如果父进程不读取这些信息,子进程就会变成 “僵尸进程(Z 状态)”,持续占用 PID 资源(系统 PID 数量有限,僵尸累积会导致无法创建新进程)。2. SIGCHLD 的核心解决逻辑
SIGCHLD 是内核给父进程的 “提醒信号”:“你的子进程终止了,快来读取它的终止信息,释放资源!”父进程通过处理 SIGCHLD 信号,在信号处理函数中调用
waitpid()回收子进程资源,从根本上避免僵尸进程:
- 若父进程不处理 SIGCHLD:内核默认忽略该信号,父进程不会主动调用
waitpid(),子进程永久变成僵尸;- 若父进程处理 SIGCHLD:在信号处理函数中循环调用
waitpid(-1, NULL, WNOHANG),一次性回收所有终止的子进程,释放 PID 和进程表资源3. 实战落地方式(两种核心处理逻辑)
处理方式 核心逻辑 优点 缺点 适用场景 捕获 SIGCHLD + 循环 waitpid 注册 SIGCHLD 处理函数,在函数内用 waitpid(-1, &status, WNOHANG)循环回收所有终止子进程可控,能获取子进程终止详情(退出码、终止信号) 需编写处理函数,注意可重入性 需要知道子进程终止原因(如排查崩溃) 设置 SA_NOCLDWAIT 标志 通过 sigaction设置sa_flags = SA_NOCLDWAIT,内核自动回收子进程资源,无需处理函数代码简单,无需手动回收 无法获取子进程终止详情 无需关注子进程终止原因,仅需避免僵尸 SIGCHLD 信号的 “非排队特性”
多个子进程同时终止时,内核只会向父进程发送一个 SIGCHLD 信号(而非每个子进程发一个)—— 这意味着,父进程的
handler处理函数只会被调用一次。
让操作系统自动回收子进程:
有两种方法可以让操作系统自动回收子进程
1. 主动执行 signal(SIGCHLD, SIG_IGN);
2. 设置 SA_NOCLDWAIT 标志
忽略 SIGCHLD 信号:
当主动执行
signal(SIGCHLD, SIG_IGN);(用户明确忽略 SIGCHLD 信号)时,内核会触发特殊逻辑:子进程终止后,内核会自动回收其资源,不会产生僵尸进程—— 这是 POSIX 标准定义的行为(Linux 完全遵循),与「默认忽略 SIGCHLD」的效果有本质区别。核心效果拆解
1. 直接解决僵尸进程问题(无需手动
waitpid)
- 若父进程主动设置 SIGCHLD 为 SIG_IGN:子进程终止后,内核会直接释放子进程的所有资源(PID、进程表项等),不会保留终止状态,因此不会产生僵尸进程;
- 对比「默认忽略 SIGCHLD」:默认忽略只是内核不通知父进程,但仍会保留子进程的终止状态,导致子进程变成僵尸(必须手动
waitpid回收)。2. 父进程无法获取子进程的终止状态
由于内核直接回收了子进程资源,父进程无法通过
wait()/waitpid()获取子进程的退出码、终止信号(调用这些函数会返回错误,因为子进程资源已被释放)。典型场景示例
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <sys/wait.h> int main() { // 用户主动忽略 SIGCHLD signal(SIGCHLD, SIG_IGN); // 创建子进程 pid_t pid = fork(); if (pid == 0) { // 子进程运行后退出 printf("子进程 PID:%d 退出\n", getpid()); _exit(0); } // 父进程等待 2 秒(确保子进程已终止) sleep(2); // 尝试调用 waitpid 获取子进程状态(会失败) int status; pid_t ret = waitpid(pid, &status, WNOHANG); if (ret == -1) { perror("waitpid 失败(子进程资源已被内核回收)"); } printf("父进程退出,子进程无僵尸残留\n"); return 0; }运行后用
ps -ef | grep Z查看,不会出现僵尸进程;同时waitpid会返回-1并报错(因为子进程资源已被内核自动回收)。关键注意点
与默认忽略的本质区别:
- 默认忽略:内核不通知父进程,但保留子进程终止状态 → 产生僵尸;
- 主动
SIG_IGN:父进程明确告知内核 “不关心子进程状态” → 内核直接回收子进程资源 → 无僵尸。适用场景:适合父进程完全不关心子进程终止状态的场景(只需避免僵尸);若需要获取子进程的退出码 / 终止信号,不能用这种方式(需捕获 SIGCHLD +
waitpid)。
设置 SA_NOCLDWAIT 标志
SA_NOCLDWAIT标志(通过sigaction设置)与主动执行signal(SIGCHLD, SIG_IGN),核心目标都是 “避免子进程变成僵尸”,但实现方式、信号行为、功能兼容性有本质区别,具体对比如下:1. 实现方式 & 信号处理逻辑的差异
维度 signal(SIGCHLD, SIG_IGN)SA_NOCLDWAIT(通过sigaction设置)操作本质 将 SIGCHLD 的处理动作直接设为 “忽略”,告知内核 “父进程不关心该信号” 保持 SIGCHLD 的处理动作(默认 / 自定义),仅通过标志位告知内核 “自动回收子进程资源” 信号的发送行为 内核会停止向父进程发送 SIGCHLD 信号(因为父进程已明确忽略,发信号无意义) 内核仍会正常向父进程发送 SIGCHLD 信号(子进程状态变化时依然触发信号) 2. 与 “信号处理函数” 的兼容性差异
这是两者最关键的区别:
signal(SIGCHLD, SIG_IGN):会直接覆盖 SIGCHLD 的处理动作,无法同时注册自定义处理函数—— 即使之前注册了处理函数,也会被 “忽略” 动作替代,处理函数不会被执行。
SA_NOCLDWAIT:可以同时注册 SIGCHLD 的自定义处理函数—— 子进程终止时,内核会发送 SIGCHLD 信号,触发处理函数执行;同时内核自动回收子进程资源(无僵尸)。示例(SA_NOCLDWAIT + 处理函数):
struct sigaction sa; sa.sa_handler = my_sigchld_handler; // 自定义处理函数 sigemptyset(&sa.sa_mask); sa.sa_flags = SA_NOCLDWAIT; // 自动回收资源 sigaction(SIGCHLD, &sa, NULL); // 子进程终止时,my_sigchld_handler 会被调用(可做日志记录),但 waitpid 无法获取状态3. 子进程状态的可获取性(两者一致)
无论是
signal(SIGCHLD, SIG_IGN)还是SA_NOCLDWAIT,父进程都无法通过wait()/waitpid()获取子进程的终止状态(退出码、终止信号)—— 因为内核会直接回收子进程资源,不会保留终止状态,调用waitpid会返回-1并报错。4. 功能覆盖的场景差异
signal(SIGCHLD, SIG_IGN):仅影响 “子进程终止” 的场景(内核不发信号 + 自动回收),对子进程 “暂停 / 恢复” 触发的 SIGCHLD 信号无特殊处理(但暂停不会产生僵尸,所以实际影响不大)。
SA_NOCLDWAIT:仅针对 “子进程终止” 的资源回收,对子进程 “暂停 / 恢复” 触发的 SIGCHLD 信号无影响(信号仍会发送,处理函数会执行)。5. 行为的明确性 & 兼容性
signal(SIGCHLD, SIG_IGN):子进程自动回收是 POSIX 标准的 “隐含行为”(早期 Unix 可能不支持,但现代 Linux/macOS 已统一),意图相对模糊(“忽略信号” 的同时附带了 “回收资源” 的副作用)
SA_NOCLDWAIT:是 POSIX 明确定义的 **“资源自动回收” 标志 **,意图更清晰(专门用于控制子进程资源回收),兼容性更稳定核心总结表
对比项 signal(SIGCHLD, SIG_IGN)SA_NOCLDWAIT内核是否发送 SIGCHLD ❌ 不发送 ✅ 正常发送 能否注册处理函数 ❌ 不能 ✅ 可以 子进程资源回收 ✅ 自动回收 ✅ 自动回收 能否获取子进程状态 ❌ 不能 ❌ 不能 行为意图 模糊(忽略信号 + 回收) 清晰(仅回收资源) 适用场景选择
- 若你完全不关心子进程的任何状态变化(不需要感知终止):用
signal(SIGCHLD, SIG_IGN)更简单- 若你需要感知子进程终止(如记录日志),但不需要回收资源 / 获取状态:用
SA_NOCLDWAIT + 自定义处理函数更灵活- 若你需要获取子进程终止状态(退出码):两者都不适用,必须用 “捕获 SIGCHLD +
waitpid循环回收”
信号的实战核心场景
1. 处理 SIGCHLD:避免僵尸进程
子进程退出后,若父进程未回收,会变成「僵尸进程」(占用 PID 资源)。父进程可捕获
SIGCHLD,在处理函数中调用waitpid()回收子进程:#include <stdio.h> #include <signal.h> #include <unistd.h> #include <sys/wait.h> // 循环回收所有退出的子进程(避免漏收) void sigchld_handler(int sig) { pid_t pid; // WNOHANG:非阻塞回收,有子进程退出则返回 PID,否则返回 0 while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) { printf("回收子进程:%d\n", pid); } } int main() { struct sigaction act; act.sa_handler = sigchld_handler; sigemptyset(&act.sa_mask); // SA_NOCLDSTOP:子进程暂停时不触发 SIGCHLD(仅退出时触发) act.sa_flags = SA_RESTART | SA_NOCLDSTOP; sigaction(SIGCHLD, &act, NULL); // 创建3个子进程 for (int i = 0; i < 3; i++) { pid_t pid = fork(); if (pid == 0) { printf("子进程 %d 退出\n", getpid()); exit(0); } } printf("父进程运行中,等待子进程退出...\n"); pause(); // 暂停直到收到信号 return 0; }
2. 实时信号传递数据(sigqueue + sa_sigaction)
实时信号(34+)可通过
sigqueue()传递少量数据,处理函数需用sa_sigaction:#include <stdio.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> // 带数据的信号处理函数 void sigrt_handler(int sig, siginfo_t *info, void *context) { printf("收到实时信号:%d,附加数据:%d\n", sig, info->si_value.sival_int); } int main() { pid_t pid = fork(); if (pid == 0) { // 子进程:注册实时信号处理函数 struct sigaction act; act.sa_sigaction = sigrt_handler; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO; // 启用 sa_sigaction(必须) sigaction(SIGRTMIN, &act, NULL); pause(); // 等待信号 exit(0); } else { // 父进程:发送实时信号并传递数据 union sigval value; value.sival_int = 12345; // 要传递的数据 sigqueue(pid, SIGRTMIN, value); // 发送实时信号 wait(NULL); } return 0; }
实战避坑指南(核心注意事项)
1. 处理函数必须用「异步安全函数」
信号处理函数是异步执行的(可能打断主进程任意代码),因此只能调用「异步安全函数」(不会被信号打断、不修改全局数据):
异步安全函数(推荐) 非异步安全函数(禁止) write()、_exit()printf()、sprintf()kill()、sigemptyset()malloc()、free()read()、close()exit()、fprintf()2. 避免「信号重入」问题
「信号重入」:进程正在执行某函数(如
malloc),被信号打断,处理函数中又调用该函数,导致数据结构错乱(如malloc的内存链表被同时修改)。规避方法:
- 处理函数仅调用异步安全函数;
- 用
sa_mask屏蔽同类型信号(避免嵌套处理)。3. 注意「竞态条件」
pause()存在竞态:信号可能在pause()调用前到达,导致进程永久暂停 ——必须用sigsuspend()替代(原子操作,无竞态)。4. 子进程的信号继承规则
fork()后:子进程继承父进程的信号处理方式(自定义函数、忽略 / 默认);exec()后:子进程重置所有「自定义处理」的信号为默认动作,仅保留「忽略」的信号(如SIGCHLD)。
核心转储和core-file
这两个东西是个冷门的调试工具, 服务器默认是关闭的, 了解就行;
1. 核心转储(Core Dump):「过程」
核心转储是 Linux 内核的一个应急记录行为—— 当进程因致命异常(如段错误、除 0、非法指令)崩溃时,内核会将该进程崩溃瞬间的所有关键运行状态(内存数据、寄存器值、函数调用栈、打开的文件句柄、程序计数器等),完整地「导出并保存」到磁盘的过程。
可以通俗理解为:程序崩溃时,内核给程序的 “死亡现场” 拍了一张高清全景照的动作。
2. core-file(核心转储文件):「产物」
core-file 是核心转储过程生成的最终文件,是内核保存的「进程崩溃瞬间的内存快照文件」,包含了核心转储过程中记录的所有状态数据,通常命名为
core、core.<pid>(pid 是崩溃进程的 ID),或自定义格式(如core.%e.%p,% e 是程序名)。
Linux 信号处理时机
信号不会 “一收到就立刻处理”,信号会被存放在pending表中, 内核只会在进程从内核态切换回用户态的 “安全点” 检查并处理未决信号(pending 表中未屏蔽的信号),这是唯一的核心时机。
1. 触发 “安全点” 的典型场景(内核态→用户态切换)
- 进程调用的系统调用(如
read/write/sleep/printf底层的 write)执行完成;- 硬件中断(如键盘、网卡)处理完毕,内核交还 CPU 控制权给进程;
- 进程被调度器唤醒(比如 sleep 结束、时间片到期后重新获得 CPU)。
2. 例外情况
SIGKILL(9)/SIGSTOP(19)不遵循这个时机 —— 内核会立即处理(终止 / 暂停进程),这是内核的终极控制手段,无延迟。
![]()
一个疑问:
如果执行主控制流程的过程中没有出现任何异常和中断,也没有任何系统调用,那么发送给该进程的信号是不是无法被处理?
在 “完全不进入内核态” 的理想假设下,信号会一直处于未决状态、无法被处理;但在实际 Linux 系统中,信号最终会被处理(只是可能延迟),原因如下:
1. 理想假设下:信号无法被处理
信号的处理时机是进程从内核态返回用户态的 “安全点”,而内核态的进入依赖:
- 系统调用(如
read/sleep);- 中断(如键盘中断、时钟中断);
- 异常(如除 0、段错误)。
如果进程全程在用户态执行,没有任何进入内核态的行为(无系统调用、无中断、无异常),内核就没有机会检查进程的 pending 表,信号会一直存放在 pending 表中,无法被处理。
2. 实际 Linux 系统中:信号最终会被处理
实际中,Linux 是分时操作系统,存在时钟中断(硬件中断):内核会定期触发时钟中断(默认每 10ms 左右),强制进程进入内核态进行 “时间片调度”。
此时,进程会从用户态切换到内核态(处理时钟中断 + 调度),内核会在 “切回用户态前” 检查 pending 表,处理未决信号。
信号保存
「信号保存」(也常被称为「信号注册 / 信号未决存储」),本质是 Linux 内核对「已产生但尚未递达处理」的信号的临时存储行为—— 简单来说,当信号被触发(产生)但进程暂时无法处理(比如信号被屏蔽、进程正在处理同类型信号)时,内核会把这个信号 “存起来”,直到进程具备处理条件后,再把保存的信号递达给进程处理。
信号保存是信号生命周期中「注册→未决」阶段的核心行为,也是保证信号不被无故丢失的关键机制,下面我会从「保存的原因→保存的位置→保存的规则→实战示例」层层讲清楚。
- 信号保存的本质:内核将 “已产生但未递达” 的信号,临时存储在进程 PCB 的
pending位图(所有信号)和实时信号队列(实时信号)中;- 保存规则:非实时信号仅保存一次(可能丢失),实时信号逐个保存(不丢失);
- 核心作用:适配信号的异步特性,保证进程不会错过关键信号,同时让进程可自主控制信号处理时机。
为什么需要 “保存” 信号?
信号是异步的(进程无法预测信号何时到达),但进程可能处于 “暂时无法处理信号” 的状态,此时内核必须先保存信号,否则信号会直接丢失:
- 信号被屏蔽:进程通过
sigprocmask()设置了信号屏蔽字(block),被屏蔽的信号无法递达,只能先保存;- 进程正在处理同类型信号:比如进程正在执行
SIGINT的自定义处理函数,此时又收到SIGINT,新的信号会先保存,等当前处理函数执行完再递达;- 进程处于不可调度状态:比如进程被
SIGSTOP暂停,所有信号都会被保存,直到进程被SIGCONT恢复。
信号保存在哪里?
信号的保存完全依赖进程的 PCB(进程控制块)(内核中描述进程的核心数据结构),PCB 里有两个关键结构负责保存信号:
结构类型 作用 适用信号类型 未决信号集( sigset_t pending)位图结构(每一位对应一个信号),用 1标记 “已保存(未决)”、0标记 “未保存 / 已处理”所有信号(基础标记) 实时信号队列( sigqueue)链表结构,保存实时信号的每个实例(包括附加数据),仅针对实时信号 实时信号(34-64) 核心逻辑:
- 对非实时信号(1-31):仅用
pending位图保存 —— 只要位图中对应位是1,就表示该信号已保存(未决);- 对实时信号(34-64):除了
pending位图标记 “有未决信号”,还会在队列中保存每个信号的具体实例(包括附加数据),保证重复信号不丢失。
信号保存的核心规则(关键!决定信号是否丢失)
信号保存的规则直接决定了信号是否 “可靠”(是否丢失),分为两类信号:
1. 非实时信号(1-31,不可靠信号):重复信号仅保存一次
- 规则:如果同一个非实时信号(如
SIGINT)被多次触发,内核只保存一次(pending位图中对应位仅置1一次),后续重复的信号会被合并,导致丢失;- 示例:进程屏蔽
SIGINT时,快速按 3 次 Ctrl+C(触发 3 次SIGINT),内核仅在pending中保存 1 次SIGINT—— 解除屏蔽后,进程仅收到 1 次SIGINT,另外 2 次丢失。2. 实时信号(34-64,可靠信号):重复信号逐个保存(排队)
- 规则:同一个实时信号被多次触发,内核会在「实时信号队列」中逐个保存每个信号实例(包括附加数据),
pending位图仅标记 “有未决的该类信号”;- 示例:进程屏蔽
SIGRTMIN时,用sigqueue()发送 3 次SIGRTMIN(每次带不同数据),内核会在队列中保存 3 个信号实例 —— 解除屏蔽后,进程会依次收到 3 次SIGRTMIN,无丢失。通用规则:保存的信号会一直存在,直到 “递达” 或 “进程退出”
- 递达:当信号不再被屏蔽、进程可处理时,内核会从
pending中移除该信号(位图置0),并递达给进程处理;- 进程退出:如果进程在信号递达前退出,内核会清空保存的所有信号,不会再处理。
sigset_t(信号屏蔽)是导致信号进入 pending 表的最常见原因,但绝非唯一原因。信号进入 pending 表的核心逻辑是:信号已产生,但暂时无法递达给进程处理—— 只要满足这个条件,无论是否手动操作
sigset_t,信号都会进入 pending 表。
导致信号进入 pending 表的 4 种场景(仅 1 种和 sigset_t 直接相关)
1. 手动设置信号屏蔽(通过
sigset_t)—— 最常见这是你提到的场景:通过
sigprocmask()结合sigset_t屏蔽某类信号,此时该信号产生后无法递达,会进入 pending 表。
- 示例:用
sigaddset(&set, SIGINT)将SIGINT加入屏蔽集,按 Ctrl+C 后,SIGINT进入 pending 表,直到解除屏蔽。- 关联
sigset_t:sigset_t是设置信号屏蔽的 “工具”,屏蔽是主动操作,导致信号无法递达。2. 进程正在处理同类型信号 —— 内核自动屏蔽,无需手动设置
sigset_t当进程正在执行某信号的自定义处理函数时,内核会自动屏蔽同类型信号(除非设置了
SA_NODEFER标志),此时新的同类型信号会进入 pending 表,直到当前处理函数执行完毕。
- 示例:进程正在处理
SIGINT(执行自定义 handler),此时再次按 Ctrl+C,新的SIGINT会进入 pending 表,等 handler 执行完后才会处理。- 和
sigset_t的关系:无需手动操作sigset_t,是内核自动临时屏蔽的结果。3. 进程处于「不可中断状态」—— 和
sigset_t完全无关如果进程处于不可中断睡眠状态(如等待磁盘 I/O、网络硬中断,内核状态为
TASK_UNINTERRUPTIBLE),此时收到的信号无法递达,会进入 pending 表,直到进程退出该状态。
- 示例:进程执行
read()读取磁盘大文件,此时收到SIGINT,信号会进入 pending 表,直到磁盘 I/O 完成、进程恢复可调度状态后,才会处理该信号。- 和
sigset_t的关系:完全无关,是进程的内核状态导致信号无法递达。4. 实时信号排队 —— 无需
sigset_t,内核自动保存实时信号(34-64)的设计是 “可靠信号”,即使没有被屏蔽,当多个同类型实时信号连续到达时,内核会将它们排队存入 pending 表的实时信号队列,按发送顺序逐个递达处理,不会丢失。
- 示例:连续用
sigqueue()给进程发 3 次SIGRTMIN,内核会在 pending 表中保存 3 个信号实例,逐个递达处理。- 和
sigset_t的关系:无需手动设置屏蔽,是实时信号的默认排队机制。
信号屏蔽(Signal Mask)
信号屏蔽是 Linux 进程主动掌控信号处理时机的核心机制,也是进程信号知识点中最易混淆、最贴近实战的部分。以下从「本质→实现→操作→规则→场景→避坑」层层拆解,覆盖所有核心细节:
信号屏蔽的本质与核心价值
1. 核心定义
信号屏蔽(也叫 “信号阻塞”)是进程通过设置「信号屏蔽字(Signal Mask)」,告诉内核:
“以下这些信号,我暂时不想处理;如果它们产生了,先帮我存在 pending 表(未决信号集)里,等我解除屏蔽后再处理。”
2. 关键误区纠正(新手必看)
错误认知 正确事实 屏蔽信号 = 阻止信号产生 屏蔽不阻止信号产生(内核仍会接收信号),仅阻止信号递达处理(执行默认 / 捕捉 / 忽略逻辑);信号产生后会暂存到 pending 表 屏蔽 = 忽略 屏蔽是 “暂存”(解除后仍会处理),忽略(SIG_IGN)是 “直接丢弃”(信号产生后不进 pending 表) 所有信号都能屏蔽 SIGKILL (9)、SIGSTOP (19) 是内核 “终极控制信号”,无法屏蔽 / 捕捉 / 忽略,保证管理员能随时控制进程 3. 核心价值
信号是异步的(进程无法预测信号何时到达),而进程执行「临界区代码」(如操作全局数据、调用非异步安全函数)时,被信号打断会导致数据错乱 / 程序崩溃。信号屏蔽的核心价值就是:
把 “随机时机到达的信号”,推迟到 “进程安全的时机” 处理,保证程序稳定性。
信号屏蔽的底层实现(PCB 中的核心结构)
信号屏蔽的实现依赖进程
PCB(进程控制块)中的两个核心结构,二者通过sigset_t(信号集,64 位位图)关联:
结构名称 类型 核心作用 与信号屏蔽的关系 信号屏蔽字(signal mask) sigset_t位图存储 “进程当前要屏蔽的信号清单” 进程通过 sigprocmask主动修改该结构,是 “屏蔽规则”未决信号集(pending 表) sigset_t位图 + 实时信号队列存储 “已产生但未递达的信号” 信号产生后,若在屏蔽字中,则存入该结构,是 “屏蔽的结果” 信号屏蔽的核心流程(可视化)
信号屏蔽的核心操作(函数 + 标准流程)
函数原型 核心用途 关键说明 int sigprocmask(
int how,
const sigset_t *set, sigset_t *oldset);设置 / 查询进程的信号屏蔽字 how=SIG_BLOCK(添加屏蔽)/SIG_UNBLOCK(解除屏蔽)/SIG_SETMASK(替换屏蔽集);oldset 保存旧屏蔽集
int sigpending(
sigset_t *set);查询进程的未决信号集(pending 表) 拿到的是 pending 表的用户态镜像,可查哪些信号已产生但未处理
int sigsuspend(
const sigset_t *mask);原子操作:切换屏蔽集 + 暂停进程等待信号 替代 pause(无竞态);收到信号后恢复原屏蔽集 所有信号屏蔽的操作都围绕
sigprocmask函数展开(进程级),结合sigset_t完成 “编辑屏蔽清单→应用屏蔽规则→恢复旧规则” 的完整流程。1. 核心函数:sigprocmask
函数原型
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);参数详解(重点!)
参数 取值 / 说明 实战场景 how控制 “如何修改屏蔽字”,仅 3 种合法取值:
①
SIG_BLOCK:将set中的信号添加到当前屏蔽字(新增屏蔽,保留原有规则)②SIG_UNBLOCK:将set中的信号从当前屏蔽字删除(解除屏蔽)③SIG_SETMASK:用set替换整个屏蔽字(全量覆盖,慎用)- 临时屏蔽少数信号:用 SIG_BLOCK- 解除特定信号屏蔽:用SIG_UNBLOCK- 恢复旧屏蔽字:用SIG_SETMASKset要操作的信号集( sigset_t类型),需先通过sigemptyset/sigaddset编辑必选,除非
how为
SIG_SETMASK且set为 NULL(仅查询当前屏蔽字)oldset保存修改前的旧屏蔽字(用于后续恢复),传 NULL则不保存建议必传!避免修改后无法恢复原有屏蔽规则 返回值
- 成功:返回
0;- 失败:返回
-1(仅参数无效时失败,如传入非法信号编号)。2. 信号集操作函数(编辑屏蔽清单)
sigset_t是 “信号清单容器”(64 位位图),必须通过以下函数操作(禁止手动位运算):
函数 作用 备注 sigemptyset(sigset_t *set)清空信号集(所有位设 0) 初始化必备!未初始化的 sigset_t位值随机sigaddset(sigset_t *set, int sig)向信号集添加指定信号(对应位设 1) 最常用,如添加 SIGINT/SIGTERMsigdelset(sigset_t *set, int sig)从信号集删除指定信号(对应位设 0) 解除部分信号屏蔽时用 sigismember(const sigset_t *set, int sig)判断信号是否在信号集中 返回 1 = 存在,0 = 不存在,-1 = 错误 sigfillset(sigset_t *set)填满信号集(所有位设 1) 临时屏蔽所有可屏蔽信号时用 3. 信号屏蔽的标准操作流程(必背!)
#include <stdio.h> #include <signal.h> #include <unistd.h> // 临界区函数:模拟操作全局数据(不能被信号打断) void critical_work() { static int global_count = 0; printf("开始执行临界区代码,操作全局变量...\n"); for (int i = 0; i < 5; i++) { global_count++; sleep(1); // 模拟耗时操作 } printf("临界区代码执行完成,global_count = %d\n", global_count); } int main() { // ========== 步骤1:初始化信号集,编辑要屏蔽的信号 ========== sigset_t mask, old_mask; sigemptyset(&mask); // 初始化(必须!) sigaddset(&mask, SIGINT); // 添加要屏蔽的信号:SIGINT(Ctrl+C) sigaddset(&mask, SIGTERM); // 可选:添加更多信号 // ========== 步骤2:保存旧屏蔽字,应用新屏蔽规则 ========== // SIG_BLOCK:新增屏蔽(保留原有屏蔽字,仅添加 SIGINT/SIGTERM) int ret = sigprocmask(SIG_BLOCK, &mask, &old_mask); if (ret == -1) { perror("sigprocmask failed"); return -1; } printf("已屏蔽 SIGINT/SIGTERM,按 Ctrl+C 无反应(5秒内)\n"); // ========== 步骤3:执行临界区代码(核心保护逻辑) ========== critical_work(); // ========== 步骤4:恢复旧屏蔽字(避免永久屏蔽) ========== sigprocmask(SIG_SETMASK, &old_mask, NULL); printf("已恢复原有屏蔽规则,现在按 Ctrl+C 可终止进程\n"); sleep(3); return 0; }运行效果
- 执行临界区代码时,按
Ctrl+C无反应(SIGINT 被屏蔽,存入 pending 表);- 临界区代码执行完成后,恢复屏蔽规则,pending 表中的 SIGINT 会立即处理(默认终止进程)。
信号屏蔽的关键规则(避坑核心)
1. 仅可屏蔽 “可屏蔽信号”
Linux 中仅
SIGKILL(9)、SIGSTOP(19)无法屏蔽 —— 即使将它们加入sigset_t,sigprocmask也会直接忽略该操作。原因:这两个信号是内核的 “终极控制手段”,保证管理员能随时终止 / 暂停任意进程(哪怕进程屏蔽了所有其他信号)。
2. 屏蔽是 “进程级 / 线程级” 行为
- 单进程:
sigprocmask作用于整个进程,所有线程共享同一套屏蔽字(实际是 “主线程的屏蔽字”);- 多线程:
sigprocmask仅影响调用线程的屏蔽字(线程级屏蔽),若要设置进程级屏蔽,需用pthread_sigmask(POSIX 线程函数)。3. 执行信号处理函数时,内核自动屏蔽同类型信号
进程执行某信号的自定义处理函数时,内核会临时屏蔽该类型信号(除非设置
sigaction的SA_NODEFER标志)。目的:避免 “信号重入”—— 比如处理 SIGINT 时,再次收到 SIGINT 打断处理函数,导致逻辑错乱。
4. 屏蔽不改变信号的处理方式
信号屏蔽仅控制 “何时处理”,不改变 “如何处理”:
- 若进程设置
signal(SIGINT, SIG_IGN)(忽略 SIGINT),即使先屏蔽 SIGINT,解除屏蔽后 pending 表中的 SIGINT 也会被直接丢弃;- 若进程设置了自定义处理函数,解除屏蔽后会执行该函数。
5. pending 表中的信号仅保存 “未处理状态”
- 非实时信号(1-31):重复产生的信号在 pending 表中仅保存 1 次(合并丢失);
- 实时信号(34-64):重复产生的信号在 pending 表中排队保存(每个实例 + 附加数据,不丢失)。
信号屏蔽与易混概念的区分
1. 屏蔽(block)vs 忽略(SIG_IGN)
维度 屏蔽(block) 忽略(SIG_IGN) 核心逻辑 信号暂存 pending 表,解除屏蔽后处理 信号产生后直接丢弃,不进 pending 表 操作方式 通过 sigprocmask设置通过 signal/sigaction设置信号状态 未决(pending) 无状态(直接丢弃) 示例 sigaddset(&mask, SIGINT); sigprocmask(SIG_BLOCK, &mask, NULL);signal(SIGINT, SIG_IGN);2. 屏蔽(block)vs 捕捉(自定义 handler)
维度 屏蔽(block) 捕捉(自定义 handler) 核心逻辑 控制信号处理时机 定义信号的处理逻辑 操作方式 通过 sigprocmask设置通过 signal/sigaction设置关联关系 捕捉不影响屏蔽,屏蔽仅推迟捕捉的执行 解除屏蔽后,pending 表中的信号会执行捕捉函数 信号屏蔽的典型实战场景
1. 保护临界区代码(最常用)
场景:进程操作全局数据、共享内存、文件句柄等 “不能被打断” 的逻辑时,屏蔽信号避免数据错乱。
// 示例:修改全局配置时屏蔽信号 void update_global_config() { sigset_t mask, old_mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); sigprocmask(SIG_BLOCK, &mask, &old_mask); // 屏蔽信号 // 临界区:修改全局配置(不能被打断) global_config.flag = 1; global_config.time = time(NULL); sigprocmask(SIG_SETMASK, &old_mask, NULL); // 恢复屏蔽字 }2. 避免非异步安全函数重入
场景:信号处理函数中只能调用「异步安全函数」(如
write、_exit),若进程正在执行malloc/printf(非异步安全),屏蔽信号可避免重入崩溃。// 示例:调用 malloc 时屏蔽信号 void alloc_large_memory() { sigset_t mask, old_mask; sigfillset(&mask); // 屏蔽所有可屏蔽信号 sigprocmask(SIG_BLOCK, &mask, &old_mask); // 调用非异步安全函数:malloc char *buf = (char*)malloc(1024 * 1024); if (buf == NULL) perror("malloc failed"); sigprocmask(SIG_SETMASK, &old_mask, NULL); // 恢复屏蔽字 }3. 批量处理信号
场景:进程需要完成批量任务(如文件写入、网络发包)后,统一处理所有待决信号,保证任务完整性。
// 示例:批量写入文件后处理信号 void write_file_batch() { sigset_t mask, old_mask; sigfillset(&mask); sigprocmask(SIG_BLOCK, &mask, &old_mask); // 屏蔽所有可屏蔽信号 // 批量写入文件(不会被信号打断) for (int i = 0; i < 100; i++) { write(fd, &data[i], sizeof(data[i])); } sigprocmask(SIG_UNBLOCK, &mask, NULL); // 解除屏蔽,统一处理信号 }4. 实现优雅退出
场景:捕获 SIGTERM 后,屏蔽该信号,先释放资源(关闭文件、断开连接),再解除屏蔽处理退出逻辑。
void sigterm_handler(int sig) { sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGTERM); sigprocmask(SIG_BLOCK, &mask, NULL); // 屏蔽 SIGTERM,避免重复触发 // 释放资源 close(fd); disconnect_network(); printf("资源已释放,准备退出...\n"); _exit(0); // 异步安全的退出函数 } int main() { signal(SIGTERM, sigterm_handler); while (1) sleep(1); return 0; }5. 实战示例:直观看到 “信号保存” 的效果
下面的代码演示:屏蔽
SIGINT后触发多次SIGINT,观察内核如何保存信号,解除屏蔽后看进程收到几次信号。#include <stdio.h> #include <signal.h> #include <unistd.h> // SIGINT 处理函数 void sigint_handler(int sig) { printf("收到 SIGINT 信号(已处理)\n"); } int main() { sigset_t block_set, pending_set; // 1. 注册 SIGINT 处理函数(避免默认终止进程) signal(SIGINT, sigint_handler); // 2. 初始化屏蔽集,添加 SIGINT(屏蔽该信号) sigemptyset(&block_set); sigaddset(&block_set, SIGINT); sigprocmask(SIG_BLOCK, &block_set, NULL); printf("已屏蔽 SIGINT,接下来 5 秒内按多次 Ctrl+C...\n"); // 3. 等待 5 秒,期间可多次按 Ctrl+C(触发 SIGINT,内核会保存) sleep(5); // 4. 查看当前保存的未决信号 sigpending(&pending_set); if (sigismember(&pending_set, SIGINT)) { printf("检测到内核保存的未决信号:SIGINT\n"); } else { printf("无未决信号\n"); } // 5. 解除 SIGINT 屏蔽(保存的信号会立即递达处理) printf("解除 SIGINT 屏蔽,处理保存的信号...\n"); sigprocmask(SIG_UNBLOCK, &block_set, NULL); printf("程序继续运行\n"); return 0; }运行结果分析:
- 运行程序后,快速按 3 次 Ctrl+C;
- 5 秒后,程序检测到
pending中保存了SIGINT(位图位为1);- 解除屏蔽后,进程仅收到 1 次
SIGINT(非实时信号仅保存一次),处理函数打印一次 “收到 SIGINT 信号”;- 若将示例中的
SIGINT换成实时信号SIGRTMIN,并多次用sigqueue()发送,解除屏蔽后会收到所有发送的信号(逐个保存)。



1899

被折叠的 条评论
为什么被折叠?



