FreeRTOS 临界段:为什么关中断?怎么用才安全?

在 FreeRTOS 开发中,我们经常看到这样的代码:

c 复制代码
taskENTER_CRITICAL();
/* 访问共享资源 */
taskEXIT_CRITICAL();

很多初学者知道这是"进入临界区",但不知道它到底做了什么、为什么要用、有什么坑。本文用最简单的方式讲清楚。

一、什么是临界段?

临界段,也叫临界区,就是一段 不能被打断的代码

打个比方:

共享资源就像一台公用打印机,一次只能一个人用。如果你打印到一半,别人也来打印,纸张就会混在一起。所以你在打印前要"锁门",打完再"开门"。

临界段就是代码里的"锁门"操作。

在操作系统中,如果两个任务同时访问一个全局变量、链表、队列等共享资源,就可能出现数据错乱。所以必须保证某段代码执行期间,其他任务或中断不能插进来。

二、FreeRTOS 怎么实现临界段?

FreeRTOS 提供了两组宏:

1. 在普通任务中使用

c 复制代码
taskENTER_CRITICAL();
/* 保护代码 */
taskEXIT_CRITICAL();

2. 在中断服务函数中使用

c 复制代码
UBaseType_t uxSavedInterruptStatus;

uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
/* 保护代码 */
taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);

三、底层原理是什么?

简单说:进入临界区就是关闭/屏蔽中断,退出临界区就是恢复中断。

为什么不关中断就不行?

因为 FreeRTOS 的任务切换依赖中断。比如:

  • SysTick 定时器中断负责时间片轮转;
  • PendSV 中断负责真正的任务上下文切换。

如果不暂时关闭中断,任务 A 可能正在修改一个全局变量,还没修改完,就被切换走了。任务 B 接着修改同一个变量,等切回任务 A 时,数据已经乱了。

所以关中断的作用,就是让这段代码 暂时具备原子性,不会被其他任务或中断打断。

需要注意的是,FreeRTOS 支持临界区嵌套:

c 复制代码
taskENTER_CRITICAL();   /* 第 1 层 */
taskENTER_CRITICAL();   /* 第 2 层 */
/* ... */
taskEXIT_CRITICAL();    /* 退出第 2 层 */
taskEXIT_CRITICAL();    /* 退出第 1 层,此时才真正恢复中断 */

内部通过一个计数器记录嵌套层数,只有最外层退出时才恢复中断。

另外,在很多 Cortex-M 移植中,并不是直接关闭所有中断,而是使用 BASEPRI 屏蔽掉优先级较低的中断,优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断仍然可以响应。但初学阶段可以先简单理解为"关中断"。

四、为什么必须关中断?

用一个简单例子说明。

假设有一个全局计数器:

c 复制代码
volatile uint32_t g_total_count = 0;

任务 A 里执行:

c 复制代码
g_total_count++;

任务 B 里也执行:

c 复制代码
g_total_count++;

从 C 语言看这是一行代码,但编译后可能变成多条汇编指令:

  1. 读取 g_total_count 到寄存器;
  2. 寄存器加 1;
  3. 写回 g_total_count

如果任务 A 执行到第 2 步时被任务 B 打断,任务 B 也执行了完整的"读-加-写"过程,然后任务 A 再写回。这时任务 B 的修改就被覆盖了,最终结果会少 1。

解决办法就是加临界区:

c 复制代码
taskENTER_CRITICAL();
g_total_count++;
taskEXIT_CRITICAL();

这样在修改 g_total_count 期间,任务切换和大部分中断都不会发生,操作就是安全的。

五、中断里怎么用?

如果在中断服务函数里也要访问共享资源,不能使用普通版本:

c 复制代码
taskENTER_CRITICAL();  /* 错误!中断里不要这样用 */

要使用 FROM_ISR 版本:

c 复制代码
void TIM3_IRQHandler(void)
{
    UBaseType_t uxSavedInterruptStatus;

    uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();

    /* 访问共享资源 */
    g_total_count++;

    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);
}

注意:taskENTER_CRITICAL_FROM_ISR()taskEXIT_CRITICAL_FROM_ISR() 必须在同一个中断函数中成对使用。

六、使用临界区的注意事项

这些是实际开发中最容易犯的错误:

1. 临界区要尽量短

临界区会关闭或屏蔽中断,时间太长会影响系统实时性。只保护必要的共享资源操作,不要把无关代码放进去。

2. 不能在临界区调用会阻塞的 API

例如:

c 复制代码
taskENTER_CRITICAL();
vTaskDelay(100);      /* 错误!可能导致系统卡死 */
taskEXIT_CRITICAL();

关中断后,调度器无法工作,任务无法切换,阻塞类 API 会导致异常。

3. 不要遗漏 taskEXIT_CRITICAL()

如果只进入不退出,中断一直关闭,系统会卡死。建议成对编写,先写退出再写中间代码也可以减少遗漏。

4. 中断里只使用 FROM_ISR 版本

普通版本不保存中断状态,在中断里使用可能导致中断状态被错误恢复。

5. 临界区不能保护所有情况

如果共享资源只在任务之间使用,可以使用挂起调度器的方式代替,避免长时间关中断:

c 复制代码
vTaskSuspendAll();
/* 较长的共享资源操作 */
xTaskResumeAll();

vTaskSuspendAll() 只阻止任务切换,不会关闭中断,因此对中断响应更友好。但如果共享资源也会被中断服务函数访问,就必须使用临界区。

七、总结

  • 临界段是一段不能被其他任务或中断打断的代码。
  • FreeRTOS 通过 taskENTER_CRITICAL() / taskEXIT_CRITICAL() 实现。
  • 本质是暂时关闭或屏蔽中断,让共享资源操作具备原子性。
  • 中断中要使用 taskENTER_CRITICAL_FROM_ISR() / taskEXIT_CRITICAL_FROM_ISR()
  • 临界区要短、要成对、不能调用阻塞 API。

记住一句话:临界区能短则短,能小则小。


如果你觉得这篇博客有帮助,欢迎点赞、收藏、关注。

相关推荐
捷瑞电子工坊4 小时前
FreeRTOS 空闲任务与阻塞延时
freertos·嵌入式实时操作系统·空闲任务·阻塞延时·vtaskdelay
hsjiasb5 小时前
FreeRTOS学习(二十九)——低功耗Tickless Idle
学习·学习笔记·freertos
Dr.kangder8 小时前
嵌入式面试总结(二十二)——指针
java·面试·职场和发展·架构·嵌入式
Dr.kangder8 小时前
嵌入式面试总结(二十)——ISA原理
单片机·面试·职场和发展·系统架构·嵌入式
橘色的喵10 小时前
SD 卡在上电后的完整加载流程:从中断信号到文件系统挂载
单片机·嵌入式硬件·嵌入式·rt-thread·rtos
虎王物联1 天前
物联网协议网关设计:多协议融合与数据转换的工程实战
物联网·嵌入式·esp32
捷瑞电子工坊1 天前
FreeRTOS 就绪列表详解:任务调度的核心枢纽
freertos·任务调度·优先级·嵌入式实时操作系统·就绪列表
Dr.kangder2 天前
嵌入式面试总结(十五)——异常处理
单片机·面试·职场和发展·系统架构·嵌入式
Dr.kangder2 天前
嵌入式面试总结(十八)——JTAG调试
嵌入式硬件·面试·职场和发展·架构·嵌入式