【第16期】可重入性(Reentrancy):编写线程安全函数

当一个函数正在被主程序执行时,如果突然发生中断,且中断服务程序(ISR)也调用了这个函数,会发生什么?系统会崩溃吗?数据会乱套吗?

如果一个函数在上述情况下能安全运行,结果正确,我们就称它是可重入的 (Reentrant)。反之,就是不可重入的

在裸机开发中,"线程安全"通常指的就是"中断安全"。

1. 事故现场:函数的“双重人格”

假设我们有一个看似人畜无害的字符串处理函数,或者一个 LCD 绘图函数:

int g_temp; // 一个全局临时变量

void functional_math(int x) {
    g_temp = x * 2;
    // ... 假设这里经过了一些耗时的计算 ...
    g_temp = g_temp + 5;
    return g_temp;
}

推演过程:

  1. 主程序调用 functional_math(10)

  2. 函数执行 g_temp = 10 * 2 (即 20)。

  3. 中断触发!CPU 暂停主程序,跳入 ISR。

  4. ISR 居然也调用了 functional_math(100)

  5. ISR 中的函数执行 g_temp = 100 * 2 (即 200)。注意:全局变量 g_temp 被改写了!

  6. ISR 执行完毕,g_temp 最终结果是 205,ISR 返回。

  7. 回到主程序。主程序继续执行 g_temp = g_temp + 5

  8. 主程序原本以为 g_temp 是 20,结果拿到的是 ISR 改写后的 205。

  9. 最终主程序返回 210,而不是预期的 25。逻辑错误产生。

在这个例子中,functional_math 就是典型的不可重入函数


2. 判决标准:谁是“不可重入”的罪魁祸首?

要编写一个可重入的函数,必须死守以下三条红线。只要踩了其中一条,这个函数在中断里调用就是不安全的。

红线一:使用了全局变量或静态局部变量

这是最常见的原因。函数如果不纯粹,依赖了函数栈以外的存储空间(Global 或 Static),那么这些空间就是所有调用者的“共享资源”。

  • 例外:如果对全局变量的使用进行了严格的原子操作关中断保护,那么可以视为可重入。

红线二:调用了其他“不可重入”的函数

这是一条传递性规则。如果你的函数里调用了一个坏函数,那你的函数也变坏了。

  • 典型案例printfmallocfree

红线三:使用了硬件资源(主要指未被保护的外设寄存器)

如果在函数里操作了特定的硬件(比如向 LCD 控制器的 FIFO 寄存器写数据),而没有加锁。当重入发生时,硬件的时序或 FIFO 顺序会被打乱。


3. 为什么 ISR 里严禁调用 printf?

这是新手最爱犯的错误:为了调试方便,在中断里打印 log。

printf 是标准库函数,它的底层实现通常包含极其复杂的逻辑:

  1. 缓冲区管理:标准库维护着一个全局的输出缓冲区。

  2. 资源锁:为了支持多线程,标准库往往会使用信号量或锁机制。

  3. 硬件操作:最终调用 fputc 发送 UART。

风险推演

  • 主程序调用 printf,刚刚获取了缓冲区的“锁”,准备填数据。

  • 中断来了,ISR 也调用 printf

  • ISR 试图去获取同一个“锁”。

  • 因为锁已经被主程序拿走了,ISR 只能等待。

  • 死锁 (Deadlock):主程序在等中断结束才能释放锁,中断在等主程序释放锁才能结束。系统彻底卡死。

工程建议

  1. 永远不要在 ISR 里调用标准库的 printf

  2. 如果非要打印,使用简易版的、不带缓冲、直接操作寄存器的自定义打印函数(前提是该 UART 端口只在中断里用,或者加了关中断保护)。

  3. 更好的做法:ISR 只置标志位或记录数据,打印工作交给主循环去做。

4. 为什么 ISR 里严禁调用 malloc/free?

malloc 管理着堆(Heap)内存链表。这是一个巨大的全局数据结构。 如果在调整链表指针的过程中(比如刚断开前一个节点,还没接上后一个节点)被中断打断,且中断里又调用了 malloc,堆内存链表就会崩溃,导致内存泄漏或野指针访问。


5. 如何编写“可重入”函数?

要让函数变得纯净、安全,可以采取以下策略:

  1. 只使用局部变量: 局部变量分配在栈 (Stack) 上。 每次调用函数,CPU 都会在栈上新开辟一块区域。主程序调用的上下文,和中断调用的上下文,它们的栈空间是完全独立的,互不干扰。这是实现可重入最自然的方法。

  2. 数据隔离: 如果函数必须访问全局数据,通过参数指针传入,而不是直接在函数内部引用固定的全局变量。

  3. 临界区保护: 如果非要操作全局变量或硬件,请务必使用关中断(上一期讲的内容)来包裹那部分代码。

修改后的安全版本:

// 纯函数:只操作栈上的局部变量 x 和 temp
// 无论被重入多少次,每个上下文都有自己的 x 和 temp,互不影响
int functional_math_safe(int x) {
    int temp; 
    temp = x * 2;
    temp = temp + 5;
    return temp;
}

关键点归纳:

  1. 可重入性:指一个函数可以安全地被“打断并再次调用”。

  2. 三大杀手:全局/静态变量、调用了不可重入函数(如 printf/malloc)、硬件资源冲突。

  3. 黄金法则:中断服务程序越简单越好,尽量只做纯计算或状态标记,涉及复杂逻辑和标准库调用的操作,尽量扔回主循环处理。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值