当一个函数正在被主程序执行时,如果突然发生中断,且中断服务程序(ISR)也调用了这个函数,会发生什么?系统会崩溃吗?数据会乱套吗?
如果一个函数在上述情况下能安全运行,结果正确,我们就称它是可重入的 (Reentrant)。反之,就是不可重入的。
在裸机开发中,"线程安全"通常指的就是"中断安全"。
1. 事故现场:函数的“双重人格”
假设我们有一个看似人畜无害的字符串处理函数,或者一个 LCD 绘图函数:
int g_temp; // 一个全局临时变量
void functional_math(int x) {
g_temp = x * 2;
// ... 假设这里经过了一些耗时的计算 ...
g_temp = g_temp + 5;
return g_temp;
}
推演过程:
-
主程序调用
functional_math(10)。 -
函数执行
g_temp = 10 * 2(即 20)。 -
中断触发!CPU 暂停主程序,跳入 ISR。
-
ISR 居然也调用了
functional_math(100)。 -
ISR 中的函数执行
g_temp = 100 * 2(即 200)。注意:全局变量 g_temp 被改写了! -
ISR 执行完毕,
g_temp最终结果是 205,ISR 返回。 -
回到主程序。主程序继续执行
g_temp = g_temp + 5。 -
主程序原本以为
g_temp是 20,结果拿到的是 ISR 改写后的 205。 -
最终主程序返回 210,而不是预期的 25。逻辑错误产生。
在这个例子中,functional_math 就是典型的不可重入函数。
2. 判决标准:谁是“不可重入”的罪魁祸首?
要编写一个可重入的函数,必须死守以下三条红线。只要踩了其中一条,这个函数在中断里调用就是不安全的。
红线一:使用了全局变量或静态局部变量
这是最常见的原因。函数如果不纯粹,依赖了函数栈以外的存储空间(Global 或 Static),那么这些空间就是所有调用者的“共享资源”。
-
例外:如果对全局变量的使用进行了严格的原子操作或关中断保护,那么可以视为可重入。
红线二:调用了其他“不可重入”的函数
这是一条传递性规则。如果你的函数里调用了一个坏函数,那你的函数也变坏了。
-
典型案例:
printf、malloc、free。
红线三:使用了硬件资源(主要指未被保护的外设寄存器)
如果在函数里操作了特定的硬件(比如向 LCD 控制器的 FIFO 寄存器写数据),而没有加锁。当重入发生时,硬件的时序或 FIFO 顺序会被打乱。
3. 为什么 ISR 里严禁调用 printf?
这是新手最爱犯的错误:为了调试方便,在中断里打印 log。
printf 是标准库函数,它的底层实现通常包含极其复杂的逻辑:
-
缓冲区管理:标准库维护着一个全局的输出缓冲区。
-
资源锁:为了支持多线程,标准库往往会使用信号量或锁机制。
-
硬件操作:最终调用
fputc发送 UART。
风险推演:
-
主程序调用
printf,刚刚获取了缓冲区的“锁”,准备填数据。 -
中断来了,ISR 也调用
printf。 -
ISR 试图去获取同一个“锁”。
-
因为锁已经被主程序拿走了,ISR 只能等待。
-
死锁 (Deadlock):主程序在等中断结束才能释放锁,中断在等主程序释放锁才能结束。系统彻底卡死。
工程建议:
永远不要在 ISR 里调用标准库的
printf。如果非要打印,使用简易版的、不带缓冲、直接操作寄存器的自定义打印函数(前提是该 UART 端口只在中断里用,或者加了关中断保护)。
更好的做法:ISR 只置标志位或记录数据,打印工作交给主循环去做。
4. 为什么 ISR 里严禁调用 malloc/free?
malloc 管理着堆(Heap)内存链表。这是一个巨大的全局数据结构。 如果在调整链表指针的过程中(比如刚断开前一个节点,还没接上后一个节点)被中断打断,且中断里又调用了 malloc,堆内存链表就会崩溃,导致内存泄漏或野指针访问。
5. 如何编写“可重入”函数?
要让函数变得纯净、安全,可以采取以下策略:
-
只使用局部变量: 局部变量分配在栈 (Stack) 上。 每次调用函数,CPU 都会在栈上新开辟一块区域。主程序调用的上下文,和中断调用的上下文,它们的栈空间是完全独立的,互不干扰。这是实现可重入最自然的方法。
-
数据隔离: 如果函数必须访问全局数据,通过参数指针传入,而不是直接在函数内部引用固定的全局变量。
-
临界区保护: 如果非要操作全局变量或硬件,请务必使用关中断(上一期讲的内容)来包裹那部分代码。
修改后的安全版本:
// 纯函数:只操作栈上的局部变量 x 和 temp
// 无论被重入多少次,每个上下文都有自己的 x 和 temp,互不影响
int functional_math_safe(int x) {
int temp;
temp = x * 2;
temp = temp + 5;
return temp;
}
关键点归纳:
-
可重入性:指一个函数可以安全地被“打断并再次调用”。
-
三大杀手:全局/静态变量、调用了不可重入函数(如 printf/malloc)、硬件资源冲突。
-
黄金法则:中断服务程序越简单越好,尽量只做纯计算或状态标记,涉及复杂逻辑和标准库调用的操作,尽量扔回主循环处理。
1536






