引言

在嵌入式实时系统(RTOS)中,互斥锁(Mutex)用于保护共享资源,但若在中断服务程序(ISR)中调用互斥锁,会引发一种隐蔽的优先级反转,其后果远超普通任务间的反转,甚至导致系统崩溃。本文从原理出发,剖析中断上下文使用互斥锁的致命后果,并给出实战规避方案。

一、优先级反转的本质与中断特殊性

1.1 普通优先级反转

  • 经典场景:低优先级任务持有锁,高优先级任务等待锁,中优先级任务抢占低优先级任务,导致高优先级任务被间接延迟。
  • RTOS 通常通过优先级继承或优先级天花板协议缓解。

1.2 中断的独特地位

  • 中断具有最高抢占优先级,不受 RTOS 调度器管理。
  • 中断上下文不能阻塞,不能调用任何可能阻塞的 API(如获取互斥锁)。
  • 中断服务程序运行在特权模式,直接访问硬件,其执行时间必须极短。

二、中断里调用互斥锁的隐蔽触发场景

2.1 典型错误代码

// 错误示例:在中断中获取互斥锁
SemaphoreHandle_t xMutex;

void ISR_Handler(void) {
    // 试图获取互斥锁(错误!)
    if (xSemaphoreTakeFromISR(xMutex, NULL) == pdTRUE) {
        // 访问共享资源
        // 释放互斥锁
        xSemaphoreGiveFromISR(xMutex, NULL);
    }
}

void Task_Low(void *param) {
    while(1) {
        xSemaphoreTake(xMutex, portMAX_DELAY);
        // 长时间占用共享资源
        vTaskDelay(pdMS_TO_TICKS(1000));
        xSemaphoreGive(xMutex);
    }
}

void Task_High(void *param) {
    while(1) {
        xSemaphoreTake(xMutex, portMAX_DELAY);
        // 快速处理
        xSemaphoreGive(xMutex);
    }
}

2.2 后果分析

  • 死锁风险:若低优先级任务持有锁,中断尝试获取锁失败(因为 xSemaphoreTakeFromISR 不会阻塞),但若中断中错误使用阻塞版本 xSemaphoreTake,则直接触发断言或死机。
  • 优先级反转加剧:即使使用 FromISR 版本,若锁被低优先级任务持有,中断无法获得锁,只能放弃操作,但此时中断中本应紧急的处理被延迟,等效于中断优先级被低优先级任务“反转”。
  • 系统响应恶化:中断处理被阻塞,可能导致硬件超时、数据丢失,甚至看门狗复位。

三、底层机制:为什么中断不能阻塞

  • RTOS 调度器依赖 SysTick 或 PendSV 异常进行上下文切换,这些异常优先级低于大多数外设中断。
  • 若在中断中调用阻塞 API,调度器无法切换任务,因为当前上下文是中断,而非任务。
  • 中断中获取互斥锁若失败,代码若继续执行,则可能访问未保护的共享资源,造成数据竞争。

四、正确配置与规避策略

4.1 使用中断安全 API

  • 在中断中,必须使用 FromISR 结尾的 API,如 xSemaphoreGiveFromISR
  • 这些 API 不会阻塞,仅尝试操作,并返回是否成功。

4.2 采用二值信号量模拟互斥(中断中)

  • 若中断需要保护资源,应使用二值信号量,并配合临界区(taskENTER_CRITICAL)或关闭中断。
  • 但更推荐:中断中仅做标记,将实际处理推迟到任务中。

4.3 推荐设计模式:中断+任务协作

SemaphoreHandle_t xBinarySem;

void ISR_Handler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 仅发送信号量,不直接访问共享资源
    xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void Task_Handler(void *param) {
    while(1) {
        if (xSemaphoreTake(xBinarySem, portMAX_DELAY) == pdTRUE) {
            // 在任务上下文中安全访问共享资源
            // 这里可以使用互斥锁保护
        }
    }
}

4.4 配置步骤(以 FreeRTOS 为例)

  1. FreeRTOSConfig.h 中启用 INCLUDE_vTaskSuspendconfigUSE_MUTEXES
  2. 创建互斥锁:xMutex = xSemaphoreCreateMutex();
  3. 创建二值信号量:xBinarySem = xSemaphoreCreateBinary();
  4. 在中断中仅使用 FromISR 函数,并传递 pxHigherPriorityTaskWoken 参数。
  5. 确保中断优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY

五、完整代码示例(正确做法)

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

SemaphoreHandle_t xMutex;
SemaphoreHandle_t xBinarySem;

// 共享资源
int shared_data = 0;

// 中断服务程序(模拟)
void EXTI_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 仅通知任务,不直接操作共享资源
    xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 处理任务
void vHandlerTask(void *pvParameters) {
    while(1) {
        if (xSemaphoreTake(xBinarySem, portMAX_DELAY) == pdTRUE) {
            // 在任务中获取互斥锁保护资源
            xSemaphoreTake(xMutex, portMAX_DELAY);
            shared_data++;
            // 模拟处理
            vTaskDelay(pdMS_TO_TICKS(10));
            xSemaphoreGive(xMutex);
        }
    }
}

// 低优先级任务
void vLowTask(void *pvParameters) {
    while(1) {
        xSemaphoreTake(xMutex, portMAX_DELAY);
        // 长时间占用资源
        vTaskDelay(pdMS_TO_TICKS(500));
        xSemaphoreGive(xMutex);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void main(void) {
    xMutex = xSemaphoreCreateMutex();
    xBinarySem = xSemaphoreCreateBinary();
    xTaskCreate(vHandlerTask, "Handler", 128, NULL, 2, NULL);
    xTaskCreate(vLowTask, "Low", 128, NULL, 1, NULL);
    vTaskStartScheduler();
}

六、注意事项

  • 绝对禁止在中断中调用 xSemaphoreTake(非 FromISR 版本)。
  • 中断中访问共享资源时,优先使用临界区(taskENTER_CRITICAL/taskEXIT_CRITICAL),但临界区会关闭中断,影响实时性,需权衡。
  • 使用 FromISR 函数时,务必检查返回值,并处理 pxHigherPriorityTaskWoken
  • 确保中断优先级设置正确,低于 configMAX_SYSCALL_INTERRUPT_PRIORITY,否则 FreeRTOS 会断言。
  • 在中断中,尽量只做标记或发送信号量,将复杂处理移至任务。

七、总结

中断中调用互斥锁是嵌入式开发中的高危操作,其引发的优先级反转隐蔽且致命。通过理解中断上下文限制,采用“中断通知+任务处理”模式,并严格使用 FromISR API,可有效避免此类问题。记住:中断是短暂的,任务才是持久的。