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

关中断临界区保护流程 任务进入临界区 关中断 (__disable_irq) 执行临界区代码 开中断 (__enable_irq) 任务继续执行 中断被屏蔽 任务调度也被阻止 临界区必须极短

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 四种方法选型决策树

临界区保护方法选型决策 临界区需要保护 临界区是否在 ISR 中访问? 是 → 关中断 否 → 仅任务间共享 临界区长度? 短 → 锁调度器 长 → 互斥锁

4.6 综合对比与使用建议

方法 中断延迟影响 任务抢占影响 ISR 中可用 推荐临界区长度
关中断 大(完全屏蔽) 完全阻止 否 < 10 µs
锁调度器 无影响 阻止任务切换 否 < 1 ms
互斥锁 无影响 任务可阻塞等待 否 任意(可阻塞)
二进制信号量 无影响 任务可阻塞等待 是(GiveFromISR) 任意(可阻塞)
实践建议:
• 外设寄存器单次读写 → 关中断(最轻量)
• 任务间共享缓冲区更新(无中断参与)→ 锁调度器
• 任务间共享资源且临界区可能阻塞 → 互斥锁(注意优先级继承)
• 任务与 ISR 之间传递数据 → 二进制信号量 + 关中断保护 ISR 内数据访问

4.7 常见错误与避坑

  • 嵌套关中断导致开中断过早:使用嵌套计数器(如 FreeRTOS 的 taskENTER_CRITICAL()),确保开中断次数匹配关中断次数。
  • 在互斥锁临界区内调用延迟函数:导致其他任务长时间无法获取资源,应避免在持有互斥锁时调用 vTaskDelay() 或阻塞式 I/O。
  • ISR 中尝试获取互斥锁:互斥锁的 Take 可能阻塞,不能在 ISR 中使用。ISR 应使用 GiveFromISR 系列 API。
  • 忘记恢复调度器:vTaskSuspendAll() 后必须配对 xTaskResumeAll(),否则系统永久停止调度。
调试技巧:在临界区入口和出口添加 GPIO 翻转,用逻辑分析仪测量临界区实际执行时间。若关中断时间超过系统允许的最大中断延迟,应改用锁调度器或互斥锁。