4. 临界区保护:关中断方法、锁调度器方法、互斥锁与信号量在临界区的应用
在 RTOS 环境下,外设驱动与任务、中断共享数据(如 FIFO、状态寄存器、DMA 描述符)时,必须保护临界区。临界区是指访问共享资源的代码段,若不加保护,任务抢占或中断嵌套会导致数据竞争、状态不一致甚至系统崩溃。本章深入分析四种临界区保护方法:关中断、锁调度器、互斥锁与信号量,并给出选型建议。
4.1 临界区问题与保护原则
临界区保护的核心目标是:在任意时刻,最多只有一个执行实体(任务或中断)访问共享资源。RTOS 下的竞争场景包括:
- 任务 vs 任务:高优先级任务抢占低优先级任务,导致共享数据被部分更新。
- 任务 vs 中断:中断服务例程(ISR)在任务执行临界区时触发,修改同一资源。
- 中断 vs 中断:高优先级中断嵌套低优先级中断,共享外设寄存器被交错访问。
保护原则:临界区应尽量短小,避免在临界区内执行阻塞操作(如延时、等待信号量)。临界区保护方法的选择取决于临界区长度、中断响应要求以及是否允许在 ISR 中使用。
4.2 关中断方法
关中断是最底层的保护手段,通过禁止 CPU 响应所有可屏蔽中断来确保临界区不被中断打断。在单核 MCU 上,关中断同时阻止了任务调度(因为调度器依赖时钟中断),因此也能防止任务抢占。
4.2.1 实现方式
/* 典型关中断/开中断宏(Cortex-M 架构) */
#define ENTER_CRITICAL() __disable_irq()
#define EXIT_CRITICAL() __enable_irq()
void driver_write_reg(uint32_t val) {
ENTER_CRITICAL();
/* 临界区:写外设寄存器 + 更新共享状态 */
HW_REG->DATA = val;
g_shared_flag = 1;
EXIT_CRITICAL();
}
4.2.2 优缺点对比
| 维度 | 说明 |
|---|---|
| 优点 | 实现简单,执行快(通常几条指令);能同时防护中断和任务抢占;无死锁风险。 |
| 缺点 | 关中断时间过长会增大中断延迟,影响实时性;不能用于 ISR 与 ISR 之间的保护(若 ISR 已关中断,嵌套中断仍可能发生);不支持在临界区内调用可能阻塞的 API。 |
| 适用场景 | 极短临界区(< 10 µs),如原子寄存器操作、状态标志更新。 |
注意:在关中断期间,系统无法响应包括心跳、通信、按键在内的所有中断。若临界区过长,可能导致看门狗超时或通信丢包。建议在临界区内只做内存赋值、寄存器写入等微秒级操作。
4.2.3 关中断保护流程 SVG
4.3 锁调度器方法
锁调度器只禁止任务切换,但允许中断响应。RTOS 内核通常提供 vTaskSuspendAll() 和 xTaskResumeAll() 接口。在临界区内,中断仍可触发并执行 ISR,但 ISR 结束后不会立即切换到其他任务。
4.3.1 实现方式
void driver_safe_update(void) {
vTaskSuspendAll(); /* 挂起调度器,禁止任务切换 */
/* 临界区:可被中断打断,但不会被其他任务抢占 */
g_buffer[head] = new_data;
head = (head + 1) % BUF_SIZE;
xTaskResumeAll(); /* 恢复调度器,可能触发一次上下文切换 */
}
4.3.2 与关中断的对比
| 特性 | 关中断 | 锁调度器 |
|---|---|---|
| 中断响应 | 完全禁止 | 允许中断 |
| 任务抢占 | 禁止 | 禁止 |
| 临界区长度 | 极短(µs 级) | 可稍长(ms 级) |
| ISR 中可用 | 否(已关中断) | 否(调度器 API 不能在 ISR 调用) |
| 适用场景 | 原子寄存器操作 | 任务间共享数据更新 |
建议:如果临界区需要被中断服务例程访问,则不能使用锁调度器,因为 ISR 不经过调度器。此时应使用关中断或中断屏蔽寄存器(如 Cortex-M 的 BASEPRI)进行更细粒度的中断控制。
4.4 互斥锁与信号量
互斥锁(Mutex)和二进制信号量(Binary Semaphore)是 RTOS 提供的更高级同步原语,适用于保护较长或可阻塞的临界区。它们允许任务在等待资源时主动挂起,让 CPU 执行其他任务。
4.4.1 互斥锁(Mutex)
互斥锁支持优先级继承,能解决经典优先级反转问题。适用于任务间互斥访问共享资源。
/* 创建互斥锁 */
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
void task_A(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
/* 临界区:操作共享外设 */
uart_send_data(tx_buffer);
xSemaphoreGive(xMutex);
}
/* 其他工作 */
}
}
4.4.2 二进制信号量
二进制信号量不提供优先级继承,但可用于任务与 ISR 之间的同步。ISR 通过 xSemaphoreGiveFromISR() 释放信号量,任务通过 xSemaphoreTake() 获取。
/* 创建二进制信号量 */
SemaphoreHandle_t xSem = xSemaphoreCreateBinary();
void UART_ISR(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 读取数据到缓冲区 */
g_rx_byte = HW_REG->DR;
xSemaphoreGiveFromISR(xSem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void task_rx(void *pvParameters) {
while(1) {
xSemaphoreTake(xSem, portMAX_DELAY);
/* 处理接收数据 */
process_byte(g_rx_byte);
}
}
4.4.3 互斥锁 vs 二进制信号量
| 特性 | 互斥锁 | 二进制信号量 |
|---|---|---|
| 优先级继承 | 支持 | 不支持 |
| ISR 中释放 | 否 | 是(GiveFromISR) |
| 递归获取 | 支持(递归互斥锁) | 否 |
| 典型用途 | 任务间互斥访问共享资源 | 任务与 ISR 同步、事件通知 |
| 死锁风险 | 需注意获取顺序 | 需注意释放次数 |
4.5 四种方法选型决策树
4.6 综合对比与使用建议
| 方法 | 中断延迟影响 | 任务抢占影响 | ISR 中可用 | 推荐临界区长度 |
|---|---|---|---|---|
| 关中断 | 大(完全屏蔽) | 完全阻止 | 否 | < 10 µs |
| 锁调度器 | 无影响 | 阻止任务切换 | 否 | < 1 ms |
| 互斥锁 | 无影响 | 任务可阻塞等待 | 否 | 任意(可阻塞) |
| 二进制信号量 | 无影响 | 任务可阻塞等待 | 是(GiveFromISR) | 任意(可阻塞) |
实践建议:
• 外设寄存器单次读写 → 关中断(最轻量)
• 任务间共享缓冲区更新(无中断参与)→ 锁调度器
• 任务间共享资源且临界区可能阻塞 → 互斥锁(注意优先级继承)
• 任务与 ISR 之间传递数据 → 二进制信号量 + 关中断保护 ISR 内数据访问
• 外设寄存器单次读写 → 关中断(最轻量)
• 任务间共享缓冲区更新(无中断参与)→ 锁调度器
• 任务间共享资源且临界区可能阻塞 → 互斥锁(注意优先级继承)
• 任务与 ISR 之间传递数据 → 二进制信号量 + 关中断保护 ISR 内数据访问
4.7 常见错误与避坑
- 嵌套关中断导致开中断过早:使用嵌套计数器(如 FreeRTOS 的
taskENTER_CRITICAL()),确保开中断次数匹配关中断次数。 - 在互斥锁临界区内调用延迟函数:导致其他任务长时间无法获取资源,应避免在持有互斥锁时调用
vTaskDelay()或阻塞式 I/O。 - ISR 中尝试获取互斥锁:互斥锁的
Take可能阻塞,不能在 ISR 中使用。ISR 应使用GiveFromISR系列 API。 - 忘记恢复调度器:
vTaskSuspendAll()后必须配对xTaskResumeAll(),否则系统永久停止调度。
调试技巧:在临界区入口和出口添加 GPIO 翻转,用逻辑分析仪测量临界区实际执行时间。若关中断时间超过系统允许的最大中断延迟,应改用锁调度器或互斥锁。