优先级反转:RTOS 实时性的隐形杀手

在嵌入式实时系统中,优先级反转(Priority Inversion)是指高优先级任务因等待低优先级任务持有的资源而被中优先级任务抢占,导致高优先级任务执行被无限期延迟的现象。经典场景如下:

  • 任务 A(高优先级)等待信号量 S,该信号量被任务 B(低优先级)持有。
  • 任务 C(中优先级)就绪,抢占任务 B,导致任务 B 无法释放信号量。
  • 任务 A 被阻塞,直到任务 C 完成,任务 B 释放信号量。

这种问题在 FreeRTOS、RT-Thread 等主流 RTOS 中普遍存在,虽然多数 RTOS 提供了优先级继承或优先级天花板协议来缓解,但实际效果仍需量化验证。

为什么需要量化测试?

  • 定性分析无法揭示反转的实际持续时间,而该时间直接影响系统实时性指标(如响应时间)。
  • 不同 RTOS 的调度策略和优先级继承实现差异,导致反转时长不同,需通过实测对比。
  • 在开发阶段,量化测试可帮助定位调度配置问题,避免上线后出现偶发超时。

基于 SysTick 的测量原理

Cortex-M 内核自带 24 位 SysTick 定时器,通常用作 RTOS 的时基(tick)。我们可以利用 SysTick 的当前值寄存器(VAL)和重装载值(LOAD)来获取高精度时间戳。测量思路如下:

  1. 在任务 A 尝试获取信号量前,记录时间戳 T1。
  2. 任务 A 获取信号量成功后,记录时间戳 T2。
  3. 反转持续时间 = T2 - T1(需考虑 tick 溢出,但若测量时间小于 tick 周期,可直接计算)。

SysTick 是递减计数器,频率通常为 CPU 时钟(如 72MHz)。获取时间戳的代码:

static uint32_t get_systick_us(void) {
    uint32_t ticks = SysTick->LOAD - SysTick->VAL; // 已递减的 tick 数
    return ticks / (SystemCoreClock / 1000000);   // 转换为微秒
}

注意:若测量跨越 tick 溢出,需处理计数器回绕,但本测试中反转时间通常小于一个 tick 周期,可忽略。

实验设计:模拟优先级反转

我们使用 FreeRTOS 在 STM32F103 上创建三个任务:

  • 任务 A(优先级 3):高优先级,尝试获取二值信号量。
  • 任务 B(优先级 1):低优先级,持有信号量,并故意延时以模拟长时间占用。
  • 任务 C(优先级 2):中优先级,执行空循环占用 CPU。

任务 B 先运行,获取信号量后延时 100ms;任务 C 在任务 B 延时期间就绪,抢占 CPU;任务 A 在任务 B 延时结束后尝试获取信号量,此时信号量仍被任务 B 持有,任务 A 阻塞,直到任务 B 释放。测量任务 A 从请求到获得信号量的时间。

配置步骤

  1. 初始化 SysTick 为 1ms 周期(FreeRTOS 默认),并确保 SystemCoreClock 正确。
  2. 创建二值信号量,初始为 0。
  3. 任务 B:获取信号量,打印持有时间,延时 100ms,释放信号量。
  4. 任务 C:空循环(如 for 循环累加变量),模拟 CPU 占用。
  5. 任务 A:记录 T1,获取信号量,记录 T2,打印差值。

完整代码示例

以下为基于 FreeRTOS 的测量代码(仅核心部分):

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

SemaphoreHandle_t xSemaphore;
volatile uint32_t t1, t2, inversion_time;

static uint32_t get_systick_us(void) {
    uint32_t ticks = SysTick->LOAD - SysTick->VAL;
    return ticks / (SystemCoreClock / 1000000);
}

void TaskA(void *arg) {
    while (1) {
        t1 = get_systick_us();
        xSemaphoreTake(xSemaphore, portMAX_DELAY);
        t2 = get_systick_us();
        inversion_time = t2 - t1;
        printf("TaskA acquired semaphore, inversion time: %lu us\n", inversion_time);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void TaskB(void *arg) {
    while (1) {
        xSemaphoreGive(xSemaphore); // 初始释放,让任务B先持有
        vTaskDelay(pdMS_TO_TICKS(100)); // 持有100ms
        xSemaphoreTake(xSemaphore, portMAX_DELAY); // 重新获取,模拟持有
        vTaskDelay(pdMS_TO_TICKS(100)); // 持有期间被任务C抢占
        xSemaphoreGive(xSemaphore); // 释放
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void TaskC(void *arg) {
    volatile uint32_t i;
    while (1) {
        for (i = 0; i < 100000; i++); // 空循环,占用CPU
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    xSemaphore = xSemaphoreCreateBinary();
    xTaskCreate(TaskA, "A", 128, NULL, 3, NULL);
    xTaskCreate(TaskB, "B", 128, NULL, 1, NULL);
    xTaskCreate(TaskC, "C", 128, NULL, 2, NULL);
    vTaskStartScheduler();
    while (1);
}

注意:任务 B 的初始逻辑需确保先持有信号量,再被任务 C 抢占。实际测试中,可调整任务 B 的延时和任务 C 的循环次数以观察不同反转时长。

测试结果与分析

在未启用优先级继承的 FreeRTOS 配置下(configUSE_PRIORITY_INHERITANCE 设为 0),实测反转时间约为 100ms(任务 B 的持有时间)。若启用优先级继承(设为 1),任务 A 会临时提升任务 B 的优先级,反转时间可缩短至微秒级。

  • 未启用继承:反转时间 ≈ 任务 B 持有时间 + 任务 C 执行时间。
  • 启用继承:反转时间 ≈ 任务 B 释放信号量的时间(通常小于 1ms)。

通过 SysTick 测量,我们能清晰对比两种配置的实时性差异,为系统设计提供数据支持。

注意事项

  • 测量精度受 SysTick 频率影响,若 CPU 主频较低,需考虑误差。
  • 任务 A 的阻塞时间可能跨越多个 tick,需处理 SysTick 回绕,但本测试中反转时间小于 tick 周期,可简化。
  • 任务 B 的持有时间应大于任务 C 的抢占时间,否则反转不明显。
  • 在真实系统中,优先级反转可能由多个资源嵌套引起,测量方法需扩展为记录多个时间点。
  • 使用 printf 打印可能影响时序,建议通过串口 DMA 或存储到缓冲区。

总结

本文通过 SysTick 测量法量化了 RTOS 中优先级反转的持续时间,验证了优先级继承的有效性。该方法简单易行,适用于任何 Cortex-M 内核,帮助开发者评估调度器的实时性。在实际项目中,建议结合具体场景调整测量参数,并考虑使用硬件定时器提高精度。