Linux:信号捕捉下(信号四)

上一篇我们讲了 Linux 信号捕捉的底层机制,其中有个关键约束:信号处理函数必须使用可重入函数 。如果在处理函数中调用printf()malloc()等不可重入函数,可能导致数据错乱、内存泄漏甚至程序崩溃。本文将从定义、原理到实操,帮你彻底搞懂可重入函数,避开信号处理的 "致命陷阱"

一、什么是可重入函数?核心定义与形象类比

1. 官方定义

可重入函数(Reentrant Function)指:函数在执行过程中被异步打断(如信号、中断、多线程调度),再次调用(重入)后,原执行流程和新执行流程都能正确完成,结果不受打断影响

2. 形象类比

  • 可重入函数:像自动售货机 ------ 你投币买水到一半被人打断去买零食,回来继续投币,售货机仍能正确给你水(逻辑独立,不依赖 "半完成" 状态)
  • 不可重入函数:像手工记账本 ------ 你记到一半(写了 "收入 100" 但没写 "元")被打断去记另一笔账,回来忘了之前的进度,账本直接混乱(依赖全局状态,操作非原子)

3. 与线程安全的区别

很多人会混淆 "可重入" 与 "线程安全",但二者是完全不同的概念:

对比维度 可重入函数 线程安全函数
核心场景 单线程中被异步打断后重入 多线程并发调用
依赖资源 仅使用局部变量 / 函数参数(私有) 可能使用共享资源(需同步机制)
实现方式 无状态设计,不依赖全局资源 可能通过锁、原子操作保护共享资源
关系逻辑 可重入未必线程安全,线程安全未必可重入 ------

示例:fprintf()是线程安全的(内部有锁保护),但不可重入 ------ 若信号处理函数打断主程序的fprintf(),再调用fprintf()会导致全局输出缓冲区错乱

二、不可重入函数的 "致命陷阱":为什么不能在信号处理中使用?

不可重入函数的核心问题是 "依赖非私有资源"(全局变量、静态变量、共享内存等)或 "执行非原子操作"。在信号处理场景中,这种依赖会被无限放大,因为信号的触发时机完全不可预测

典型案例:strtok () 函数的重入灾难

strtok()是 C 标准库中的字符串分割函数,其不可重入的根源是使用静态变量保存 "上次分割的位置":

复制代码
#include <stdio.h>
#include <string.h>
#include <signal.h>

void handle_sigint(int signum) {
    char str[] = "x,y,z";
    // 信号处理函数中调用strtok(),覆盖静态变量
    char *p = strtok(str, ",");
    printf("信号处理中分割结果:%s\n", p);
}

int main() {
    signal(SIGINT, handle_sigint);
    char str[] = "a,b,c,d";
    // 主程序第一次分割,静态变量保存"b,c,d"的起始地址
    char *p = strtok(str, ",");
    printf("主程序第一次分割结果:%s\n", p);
    
    printf("按Ctrl+C触发信号...\n");
    pause(); // 等待信号
    
    // 主程序继续分割,静态变量已被信号处理函数修改
    p = strtok(NULL, ",");
    printf("主程序第二次分割结果:%s\n", p); // 预期输出"b",实际输出"y"
    return 0;
}

运行结果 :主程序第二次分割的结果完全错误,因为strtok()的静态变量被信号处理函数覆盖 ------ 这就是不可重入函数在信号处理中的典型灾难

不可重入函数的常见特征

只要满足以下任一条件,大概率是不可重入函数:

  1. 使用全局变量、静态变量或静态缓冲区(如strtok()asctime()
  2. 调用动态内存分配 / 释放函数(malloc()free(),操作全局内存池)
  3. 操作共享资源(如文件、硬件寄存器,无同步机制)
  4. 调用其他不可重入函数(如printf()调用malloc(),间接依赖全局资源)

三、常见可重入 / 不可重入函数清单(开发必备)

1. 安全可用:可重入函数(信号处理中放心用)

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

  • 内存操作:memcpy()memset()strcpy()strcmp()(仅操作传入参数)
  • 系统调用:write()read()_exit()close()(内核级原子操作,不依赖用户态全局资源)
  • 基础运算:abs()sqrt()atoi()(仅处理参数,无副作用)
  • 可重入变体:strtok_r()strtok()的可重入版)、localtime_r()localtime()的可重入版)

2. 绝对禁用:不可重入函数(信号处理中严禁使用)

这类函数依赖全局 / 静态资源,或有分步操作,易导致错乱:

  • 标准 IO:printf()fprintf()puts()scanf()(依赖全局输出缓冲区)
  • 内存管理:malloc()free()calloc()realloc()(操作全局内存池链表)
  • 字符串处理:strtok()asctime()ctime()(使用静态变量 / 缓冲区)
  • 其他:rand()(静态随机数种子)、getenv()(全局环境变量表)、sleep()(修改全局定时器状态)

四、如何编写安全的可重入函数?5 个核心原则

编写可重入函数的核心是 "无状态化" 和 "数据隔离",具体遵循以下原则

  1. 禁用全局 / 静态变量:所有数据通过参数传入(传值而非传指针共享),或使用函数内局部变量(栈上分配,每个调用有独立栈帧)
  2. 避免动态内存操作 :若需内存分配,改用栈上缓冲区或内存池(提前分配固定大小内存),不调用malloc()/free()
  3. 仅调用可重入函数:函数内部依赖的所有函数必须是可重入的,形成 "安全调用链"
  4. 原子操作处理共享资源 :若必须操作共享资源(如全局标志位),使用volatile sig_atomic_t类型(保证读写原子性),或通过信号屏蔽避免并发访问
  5. 拒绝递归依赖:避免函数递归调用自身(除非能保证栈空间充足,且无共享状态)

实操示例:将不可重入函数改为可重入

以 "统计字符串长度" 为例,对比不可重入与可重入实现:

复制代码
// 不可重入版本:使用静态变量保存结果
static int g_len = 0;
int unsafe_strlen(const char *str) {
    g_len = 0;
    while (str[g_len] != '\0') {
        g_len++;
    }
    return g_len;
}

// 可重入版本:使用局部变量,无全局依赖
int safe_strlen(const char *str) {
    int len = 0;
    while (str[len] != '\0') {
        len++;
    }
    return len;
}

五、信号处理函数的安全编写规范

结合可重入原则,信号处理函数需满足以下规范,避免踩坑:

  1. 仅执行必要逻辑:优先设置全局标志位(volatile sig_atomic_t类型),复杂逻辑交给主程序处理
  2. 严格使用可重入函数:仅调用write()memcpy()等安全函数,禁用printf()malloc()
  3. 控制函数长度:处理函数应短小精悍,避免长时间阻塞(如循环等待)
  4. 保护共享资源:若需修改全局变量,使用volatile sig_atomic_t保证原子性,或在处理前屏蔽相关信号

规范示例:安全的信号处理函数

复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// 全局标志位:使用volatile sig_atomic_t保证原子读写
volatile sig_atomic_t g_quit = 0;

// 信号处理函数:仅设置标志位,无复杂操作
void handle_sigterm(int signum) {
    g_quit = 1;
    // 安全调用可重入函数write()
    const char *msg = "收到SIGTERM信号,准备退出...\n";
    write(STDOUT_FILENO, msg, strlen(msg));
}

int main() {
    struct sigaction act;
    sigemptyset(&act.sa_mask);
    act.sa_handler = handle_sigterm;
    act.sa_flags = 0;
    sigaction(SIGTERM, &act, NULL);

    printf("进程运行中,PID:%d,等待SIGTERM信号(kill PID)...\n", getpid());
    while (!g_quit) {
        sleep(1); // 主程序循环,检测标志位
    }

    // 主程序处理退出逻辑(释放资源等)
    printf("开始释放资源,优雅退出...\n");
    return 0;
}

可重入函数是 Linux 信号处理的 "安全底线",其核心设计思想是 "不依赖外部状态,仅靠输入参数保证逻辑正确"。记住三个关键要点:信号处理函数必须用可重入函数,禁用printf()/malloc()等危险函数,通过volatile sig_atomic_t处理全局标志位,就可以啦>w<

相关推荐
AlfredZhao4 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
ClouGence9 小时前
Oracle CDC 架构优化:从主库直连到 DataGuard 备库同步
数据库·后端·oracle
用户97183563346610 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪11 小时前
linux 拷贝文件或目录到指定的位置
linux
无响应de神11 小时前
三、用户与权限管理
数据库·mysql
大树881 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠1 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质1 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush41 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 天前
Linux 11 动态监控指令top
linux