不可重入函数Non-Reentrant & 可重入函数Reentrant

用最通俗的生活例子理解:

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

核心定义

函数类型 核心定义 通俗理解
可重入函数 函数执行过程中被异步打断(如信号、中断、多线程调度),再次调用(重入)后,原流程和新流程都能正确执行,结果不受影响 多个人同时用、中途被打断再用,都不会乱,逻辑完全 "自给自足"
不可重入函数 函数执行过程中被异步打断后重入,会导致数据错乱、逻辑异常、结果错误 只能 "一次性干完",中途被打断就乱套,依赖全局状态 / 共享资源

关键前提:"重入" 的核心是异步打断 + 再次调用 ------ 比如信号处理函数打断主程序的malloc,又在信号处理函数里调用malloc,就是典型的 "重入不可重入函数",必然出问题。

本质区别:为什么可重入函数 "不乱"?

可重入与不可重入的核心差异,在于是否依赖 "非私有资源" 和 "非原子操作",用表格清晰对比:

对比维度 可重入函数 不可重入函数
依赖全局 / 静态变量 ❌ 完全不用(仅用参数 / 局部变量) ✅ 依赖(比如strtok用静态变量存分割位置)
操作共享资源(文件 / 内存) ❌ 仅操作函数内私有资源 ✅ 操作全局共享资源(比如printf用全局输出缓冲区)
调用其他不可重入函数 ❌ 只调用可重入函数 ✅ 调用malloc/printf等不可重入函数
非原子操作 ❌ 仅用原子操作(一步完成) ✅ 有分步操作(比如 "读全局变量→修改→写回")
内存分配 / 释放 ❌ 不调用malloc/free(改内存池) ✅ 调用malloc/free

不可重入函数的 "坑":具体怎么乱的?

举个经典例子 ------strtok(字符串分割函数,不可重入):

cpp 复制代码
// 不可重入的根源: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)就解决了这个问题 ------ 把 "分割位置" 从静态变量改成参数传入(私有资源),就算重入也不会乱:

cpp 复制代码
// 可重入版本:用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. 不依赖函数执行顺序:函数执行结果仅由输入参数决定,不受 "是否被打断" 影响(纯函数思想)。

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

cpp 复制代码
// 计算两数之和,仅用参数和局部变量,无全局依赖
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不可重入)
}

反面例子(不可重入函数):

cpp 复制代码
int count = 0; // 全局变量
// 不可重入:依赖全局变量,count++是非原子操作
int increment() {
    count++; // 读count→+1→写回,中途被打断会乱
    return count;
}

核心总结

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

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

相关推荐
Thera7772 小时前
Linux 核心绑定(CPU Affinity)详解:原理、方法与优缺点分析
linux·运维·服务器
小鹏linux2 小时前
【linux】进程与服务管理命令 - setup
linux·运维·服务器
倔强的石头1062 小时前
【Linux指南】进程控制系列(二)进程终止 —— 退出场景、方法与退出码详解
linux·运维·服务器
爱吃生蚝的于勒2 小时前
【Linux】零基础深入学习动静态库+深入学习地址
linux·运维·服务器·c语言·数据结构·c++·学习
不甘平凡的小鸟2 小时前
libcurl+vs2017+openssl编译
linux·运维·服务器
知识分享小能手2 小时前
Ubuntu入门学习教程,从入门到精通, Ubuntu 22.04中的任务计划详解(16)
linux·学习·ubuntu
oMcLin3 小时前
CentOS 7.9 使用 SELinux 时无法访问特定目录:如何配置 SELinux 策略允许访问
linux·运维·centos
geniuscrh3 小时前
自建Tailscale的Derp服务器
运维·服务器
QT 小鲜肉3 小时前
【Linux命令大全】001.文件管理之whereis命令(实操篇)
linux·运维·服务器·网络·笔记