4. 任务调度机制:抢占式调度、时间片轮转、优先级反转与解决

调度器,说白了就是RTOS的“大脑”。它决定哪个任务在什么时候运行。我见过不少开发者,任务跑飞了第一反应是查中断,其实多半是调度策略没搞对。这一章,咱们把抢占式、时间片轮转,还有那个让人头疼的优先级反转,一次性说透。

4.1 抢占式调度:谁优先级高谁先跑

抢占式调度是RTOS的标配。它的规则很简单:高优先级任务就绪时,立即打断正在运行的低优先级任务。这个“立即”是微秒级的,由PendSV中断完成上下文切换。

核心机制:每次系统Tick或任务主动让出CPU时,调度器扫描就绪列表,选出优先级最高的任务执行。

我在项目中遇到过一个问题:一个数据采集任务优先级设得过高,导致UI刷新卡死。后来才意识到,抢占式调度下,高优先级任务如果一直不阻塞,低优先级任务永远没机会跑。

避坑指南:我曾经把一个电机控制任务的优先级设到最高,结果它在一个while循环里轮询传感器,占用了90%的CPU。其他任务全部饿死。后来加了信号量等待,问题解决。

4.2 时间片轮转:同优先级任务的“轮流坐庄”

如果多个任务优先级相同,抢占式调度就失效了——谁先来谁跑,直到跑完。这显然不合理。时间片轮转就是解决这个问题的。

每个任务分配一个固定时间片(通常1-10ms),时间到了就切换到下一个同优先级任务。你想想看,这就像几个人分蛋糕,每人吃一口就换人。

特性 抢占式调度 时间片轮转
触发条件 高优先级任务就绪 时间片耗尽
适用场景 不同优先级任务 同优先级任务
实时性 高 中等
上下文切换开销 较低 较高(频繁切换)

注意:时间片轮转不是所有RTOS都默认开启。FreeRTOS需要在FreeRTOSConfig.h中设置configUSE_TIME_SLICING为1。我建议你只在确实需要公平分配CPU时才开启,否则会增加不必要的切换开销。

4.3 优先级反转:一个经典的“坑”

优先级反转,说白了就是低优先级任务间接阻塞了高优先级任务。为什么会这样?看个例子:

  1. 任务A(高优先级)和任务C(低优先级)共享一个互斥锁。
  2. 任务C先拿到锁,然后被任务B(中优先级)抢占。
  3. 任务B一直运行,任务C无法释放锁。
  4. 任务A等待锁,被任务B间接阻塞。

结果:高优先级的A,被中优先级的B“反转”了。我在一个工业控制项目中遇到过这个现象:一个紧急停止任务(高优先级)因为等待一个被低优先级任务占用的锁,延迟了200ms才响应。嗯,那次差点出事故。

优先级反转的三个条件:

  • 存在共享资源(互斥锁/信号量)
  • 存在三个或以上不同优先级的任务
  • 低优先级任务持有锁时被中优先级任务抢占

4.4 解决方案:优先级继承与优先级天花板

解决优先级反转,主流RTOS提供了两种机制。我个人更推荐优先级继承,因为它更灵活。

4.4.1 优先级继承

当高优先级任务等待一个被低优先级任务持有的锁时,低优先级任务临时“继承”高优先级任务的优先级。这样,中优先级任务就无法抢占它了。锁释放后,低优先级任务恢复原优先级。

FreeRTOS的实现很简单:创建互斥量时使用xSemaphoreCreateMutex(),而不是xSemaphoreCreateBinary()。前者自动启用优先级继承。

// 启用优先级继承的互斥量
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();

// 任务A(高优先级)获取锁
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
    // 访问共享资源
    xSemaphoreGive(xMutex);
}

我的习惯:只要涉及共享资源,我统一用xSemaphoreCreateMutex()。虽然它比二进制信号量多一点点开销,但能避免99%的优先级反转问题。别为了省那几百纳秒给自己挖坑。

4.4.2 优先级天花板

另一种方案是优先级天花板。系统为每个互斥量设定一个“天花板优先级”——所有可能获取该锁的任务中的最高优先级。当任务持有锁时,它的优先级被提升到天花板级别。

特性 优先级继承 优先级天花板
提升时机 只有高优先级任务等待时才提升 获取锁时立即提升
提升幅度 动态,等于等待任务的优先级 固定,等于天花板优先级
实现复杂度 中等 较低
适用场景 通用 静态优先级系统

我个人很少用优先级天花板,因为它需要预先知道所有任务的优先级,这在复杂系统中很难维护。但如果你做的是航空或汽车电子这类静态系统,天花板方案更可预测。

4.5 调度机制选择实战建议

讲了这么多,到底怎么选?我总结了几条经验:

  • 不同优先级任务:用抢占式调度,配合优先级继承的互斥量。
  • 同优先级任务:用时间片轮转,但时间片别设太短(我一般用5ms)。
  • 硬实时任务:优先级设最高,且不要使用任何阻塞调用。
  • 避免优先级反转:优先使用互斥量(带优先级继承),而不是二进制信号量。

警告:不要在中断服务函数(ISR)中调用xSemaphoreTake()!ISR不能阻塞。如果需要从ISR中释放信号量,使用xSemaphoreGiveFromISR()。

最后,送你一张调度机制的关系图,帮你理清思路。

RTOS任务调度机制关系图 RTOS任务调度机制 任务调度器 抢占式调度 时间片轮转 优先级反转 高优先级抢占 PendSV切换 固定时间片 同优先级轮换 低优先级持锁 中优先级抢占 解决方案 优先级继承 优先级天花板 核心原则 高优先级任务 不应被低优先级阻塞

这张图把调度机制的关系理清楚了。从任务调度器出发,分三条路:抢占式、时间片轮转、优先级反转。优先级反转下面又引出两种解决方案。你写代码前,先对着这张图想清楚你的任务优先级怎么设计,能省不少调试时间。