Linux进程通信---6---信号

一. 信号的核心认知

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/SIGRTMAX

4. 开发必记的核心信号(高频使用)

无需记全 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 无法修改(强制默认动作)

通俗关联逻辑(一句话讲清三者配合)

内核处理信号时:

  1. 先查 block 表 → 若信号被屏蔽 → 存入 Pending 表 暂存;
  2. 若未被屏蔽 → 从 Pending 表 移除该信号 → 查 handler 表 → 按规则(默认 / 忽略 / 捕捉)处理。

命令行方式发送信号

适用于终端手动给进程发信号,核心工具是 killkillallpkill,其中 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 12345

2. 补充: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 秒后内核发 SIGALRM
sigqueue()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,设置errno

1.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 使用场景

  1. 异常处理中终止自身

  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 特性

  1. 单一定时器:每个进程只能有一个alarm定时器

  2. 非阻塞:设置后立即返回,不阻塞进程

  3. 精度为秒:最小单位是1秒

  4. 信号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 使用条件

  1. 发送者和接收者都必须使用sigqueue()

  2. 接收者需要使用sigaction()设置SA_SIGINFO标志

  3. 接收者处理函数原型: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. 可重入函数(放心用,尤其是信号处理 / 多线程)

这些函数仅依赖参数和局部变量,无全局状态,操作原子:

  • 内存操作:memcpymemsetstrcpystrcmp(仅操作传入的参数);
  • 系统调用:writeread_exitclose(内核级原子操作,不依赖用户态全局资源);
  • 基础运算:abssqrt(仅处理参数,无副作用)。

2. 不可重入函数(绝对不能在信号处理函数里用!)

这些函数依赖全局 / 静态资源,或有分步操作:

  • 标准 IO:printffprintfputs(用全局输出缓冲区,分步写入);
  • 内存管理:mallocfreecalloc(操作全局内存池,分步修改链表);
  • 定时器:sleepalarm(修改全局定时器状态);
  • 字符串处理:strtok(静态变量)、asctime(静态缓冲区);
  • 其他:rand(静态随机数种子)、getenv(全局环境变量表)。

关键应用场景:为什么你必须关心?

这部分和“信号捕获” 强相关 ——信号处理函数必须用可重入函数,否则必出问题!

场景 1:信号处理打断不可重入函数

主程序正在执行malloc(修改全局内存池链表):

主程序:malloc → 拆链表节点(只拆了一半)
↓ 信号触发(比如SIGINT)
信号处理函数:又调用malloc → 继续改同一个内存池链表
↓ 信号处理完,主程序继续
主程序:malloc的链表已经乱了 → 内存泄漏/程序崩溃

场景 2:多线程调用不可重入函数

多线程同时调用printf

线程1:printf("hello") → 写了“he”到全局缓冲区,被调度走
线程2:printf("world") → 覆盖缓冲区为“world”,输出
线程1:继续执行 → 缓冲区剩下的“llo”被输出
最终结果:worldllo(完全错乱)

核心原因:信号处理是 “异步打断”,多线程是 “并发执行”,二者都会触发函数的 “重入”—— 不可重入函数扛不住这种场景,可重入函数则完全没问题。

如何编写可重入函数?(实战规则)

只要遵守以下规则,就能写出安全的可重入函数:

  1. 绝不使用全局 / 静态变量:所有数据都通过参数传入(传值,而非传指针共享),或用函数内局部变量(栈上分配,每个调用独立);
  2. 绝不调用不可重入函数:比如信号处理函数里,不能用printf/malloc/sleep,改用write(可重入)输出、_exit退出;
  3. 绝不操作共享资源:不写全局文件、不修改全局配置,仅操作函数内创建的私有资源;
  4. 只用原子操作:避免 “读 - 改 - 写” 分步操作(比如count++是三步:读 count→+1→写回,非原子),改用原子指令(如__sync_fetch_and_add);
  5. 不依赖函数执行顺序:函数执行结果仅由输入参数决定,不受 “是否被打断” 影响(纯函数思想)。

正面例子(可重入函数):

// 计算两数之和,仅用参数和局部变量,无全局依赖
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;
}

核心总结

  1. 核心判断:可重入函数 =“自给自足”(仅用参数 / 局部变量),不可重入函数 =“依赖外部状态”(全局 / 静态 / 共享资源);
  2. 关键风险:不可重入函数被异步打断(信号)/ 并发调用(多线程)会导致数据错乱,可重入函数则安全;
  3. 实战要求:信号处理函数、多线程核心逻辑必须用可重入函数,禁用printf/malloc等不可重入函数;
  4. 编写规则:不碰全局、不调不可重入函数、只用原子操作、结果仅由参数决定。

简单说:可重入函数是 “不怕打断的函数”,不可重入函数是 “一打断就乱的函数”—— 在异步 / 并发场景下,选可重入函数是唯一安全的选择。

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 并报错(因为子进程资源已被内核自动回收)。

关键注意点

  1. 与默认忽略的本质区别

    • 默认忽略:内核不通知父进程,但保留子进程终止状态 → 产生僵尸;
    • 主动 SIG_IGN:父进程明确告知内核 “不关心子进程状态” → 内核直接回收子进程资源 → 无僵尸。
  2. 适用场景:适合父进程完全不关心子进程终止状态的场景(只需避免僵尸);若需要获取子进程的退出码 / 终止信号,不能用这种方式(需捕获 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 是核心转储过程生成的最终文件,是内核保存的「进程崩溃瞬间的内存快照文件」,包含了核心转储过程中记录的所有状态数据,通常命名为 corecore.<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 内核对「已产生但尚未递达处理」的信号的临时存储行为—— 简单来说,当信号被触发(产生)但进程暂时无法处理(比如信号被屏蔽、进程正在处理同类型信号)时,内核会把这个信号 “存起来”,直到进程具备处理条件后,再把保存的信号递达给进程处理。

信号保存是信号生命周期中「注册→未决」阶段的核心行为,也是保证信号不被无故丢失的关键机制,下面我会从「保存的原因→保存的位置→保存的规则→实战示例」层层讲清楚。

  1. 信号保存的本质:内核将 “已产生但未递达” 的信号,临时存储在进程 PCB 的 pending 位图(所有信号)和实时信号队列(实时信号)中;
  2. 保存规则:非实时信号仅保存一次(可能丢失),实时信号逐个保存(不丢失);
  3. 核心作用:适配信号的异步特性,保证进程不会错过关键信号,同时让进程可自主控制信号处理时机。

为什么需要 “保存” 信号?

信号是异步的(进程无法预测信号何时到达),但进程可能处于 “暂时无法处理信号” 的状态,此时内核必须先保存信号,否则信号会直接丢失:

  1. 信号被屏蔽:进程通过 sigprocmask() 设置了信号屏蔽字(block),被屏蔽的信号无法递达,只能先保存;
  2. 进程正在处理同类型信号:比如进程正在执行 SIGINT 的自定义处理函数,此时又收到 SIGINT,新的信号会先保存,等当前处理函数执行完再递达;
  3. 进程处于不可调度状态:比如进程被 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_tsigset_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_SETMASK
set要操作的信号集(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/SIGTERM
sigdelset(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_tsigprocmask 也会直接忽略该操作。

原因:这两个信号是内核的 “终极控制手段”,保证管理员能随时终止 / 暂停任意进程(哪怕进程屏蔽了所有其他信号)。

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;
}
运行结果分析:
  1. 运行程序后,快速按 3 次 Ctrl+C;
  2. 5 秒后,程序检测到 pending 中保存了 SIGINT(位图位为 1);
  3. 解除屏蔽后,进程仅收到 1 次 SIGINT(非实时信号仅保存一次),处理函数打印一次 “收到 SIGINT 信号”;
  4. 若将示例中的 SIGINT 换成实时信号 SIGRTMIN,并多次用 sigqueue() 发送,解除屏蔽后会收到所有发送的信号(逐个保存)。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值