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。

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


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

相关推荐
youcans_8 小时前
【嵌入式软件AI编程】01. 基于 STM32/Claude Code的AI编程
stm32·mcu·嵌入式·ai编程·claude code
小智学长 | 嵌入式8 小时前
实测GPT-6 Astra绘制硬件原理图和PCB效果分享
嵌入式·嵌入式开发·ai工具·单片机开发
一路往蓝-Anbo12 小时前
第七篇:以太网设计 —— 从 RMII 时序到变压器隔离的深度拆解
stm32·单片机·嵌入式硬件·嵌入式·stm32硬件设计
玄奕子1 天前
三相逆变器系列(一):看懂三相全桥拓扑
嵌入式·dsp开发·电力电子·ti c2000·三相全桥逆变
the sun342 天前
嵌入式易错点:ARM指令架构、IP核、MCU厂商关系+BOOTLoader刷机原理
stm32·嵌入式
嵌入式阿蔡2 天前
CoAP / LwM2M 轻量协议:受限设备为什么不用 MQTT
单片机·物联网·嵌入式
长江后浪博客3 天前
ESP32-SOLO-1 在 ESP-IDF v5.5 下启动不断重启问题分析与解决
单片机·嵌入式硬件·嵌入式·esp32·freertos·esp-idf
Jason_zhao_MR3 天前
Linux与采集MCU通信为什么我们用了双SPI
linux·arm开发·单片机·嵌入式硬件·嵌入式
嵌入式阿蔡3 天前
云平台设备接入实战:三元组鉴权、属性上报与命令下发(阿里云/华为云)
嵌入式硬件·物联网·嵌入式·存储
分支预测失败4 天前
RISC-V 上下文切换实战:从 Trap 入口到 Linux 进程调度
后端·嵌入式