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

调度机制,说白了就是操作系统怎么决定「下一个该谁跑」。

我做了这么多年嵌入式,见过太多因为调度没选好导致系统崩掉的案例。有一次,一个同事调试了三天,最后发现是优先级反转把整个系统卡死了。嗯,咱们今天就把这块彻底讲透。

4.1 抢占式调度:谁急谁先跑

抢占式调度,是实时系统最核心的调度方式。它的逻辑很简单:高优先级的任务来了,低优先级的任务立刻让路。

核心特点:

  • 每个任务都有固定优先级
  • 高优先级任务就绪时,立即抢占CPU
  • 系统响应时间可预测,适合硬实时场景

我在项目中遇到过这样一个场景:一个安防摄像头,需要同时处理视频编码和网络发包。视频编码优先级高,网络发包优先级低。如果不用抢占式,网络发包占着CPU不放,视频帧就会丢。用了抢占式,视频编码一来,网络发包立刻挂起,问题就解决了。

你想想看,如果系统里有个紧急报警任务,优先级设为最高。一旦触发,不管其他任务在干什么,CPU立刻响应。这就是抢占式的价值。

// FreeRTOS 中创建抢占式任务示例
xTaskCreate(vHighPriorityTask, "High", 256, NULL, 3, NULL);
xTaskCreate(vLowPriorityTask,  "Low",  256, NULL, 1, NULL);
// 优先级数值越大,优先级越高

注意:抢占式调度不是万能的。如果高优先级任务一直不阻塞,低优先级任务可能永远得不到CPU。这就是所谓的「饿死」问题。

4.2 时间片轮转:大家轮流用

时间片轮转,说白了就是「一人跑一会儿」。同优先级的任务,每人分一个时间片,时间到了就换下一个。

我个人习惯在非实时任务中使用时间片轮转。比如安防系统的日志记录、状态显示这些任务,不需要特别紧急,大家轮流跑就行。

调度方式 适用场景 典型时间片
抢占式 硬实时、紧急任务 无固定时间片
时间片轮转 同优先级、非实时任务 1-10ms
混合模式 大多数实际项目 按需配置

我记得有一次调试一个多路视频解码的项目,四个解码任务优先级相同。如果不加时间片轮转,第一个任务会一直占着CPU,直到解码完一帧才让出。结果其他三路视频卡得不行。加上时间片轮转后,每路视频轮流解码,画面就流畅了。

小技巧:时间片别设太短。太短会导致频繁上下文切换,浪费CPU。我一般设5ms左右,具体看你的任务切换开销。

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

优先级反转,是实时系统里最隐蔽的坑之一。它指的是:一个高优先级任务,被一个低优先级任务间接阻塞了。

为什么会这样?举个例子:

  1. 任务A(低优先级)拿到了共享资源(比如一个互斥锁)
  2. 任务C(高优先级)来了,想拿同一个资源,但被A占着,只能等
  3. 这时候任务B(中优先级)开始运行,它不需要那个资源,但因为它优先级比A高,所以A被B抢占了
  4. 结果:任务C在等A,A在等B,C反而被B阻塞了

我曾经在一个门禁控制器项目里遇到过这个问题。一个高优先级的刷卡响应任务,被一个中优先级的LED闪烁任务卡住了。刷卡后要等好几秒才有反应,客户差点退货。排查了整整一天,才发现是优先级反转。

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

解决优先级反转,主流方案有两个:优先级继承和优先级天花板。

4.4.1 优先级继承

优先级继承的思路很直接:当高优先级任务被低优先级任务阻塞时,把低优先级任务的优先级临时提升到高优先级任务的级别。这样中优先级任务就抢不了它了。

// FreeRTOS 中使用互斥量(自带优先级继承)
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();

void vHighPriorityTask(void *pvParameters) {
    // 尝试获取互斥量
    if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
        // 访问共享资源
        xSemaphoreGive(xMutex);
    }
}

关键点:FreeRTOS的互斥量(Mutex)默认支持优先级继承。但二值信号量(Binary Semaphore)不支持。我建议:只要涉及共享资源,优先用互斥量。

4.4.2 优先级天花板

优先级天花板更粗暴:谁拿了某个资源,谁的优先级就立刻升到该资源预设的「天花板」级别。不管有没有高优先级任务在等。

我个人更倾向优先级继承,因为它更灵活,不会过度提升优先级。但优先级天花板实现简单,适合资源少、逻辑清晰的小系统。

方案 优点 缺点
优先级继承 只在必要时提升,效率高 实现复杂,可能有多次提升
优先级天花板 实现简单,行为可预测 可能过度提升优先级

避坑指南:我曾经在一个项目中用了二值信号量保护共享资源,结果出现了优先级反转。后来换成互斥量,问题就解决了。记住:用互斥量,别用二值信号量做资源保护。

4.5 实际选型建议

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

  • 硬实时任务:必须用抢占式调度,优先级设高
  • 同优先级非实时任务:用时间片轮转,避免饿死
  • 共享资源访问:用互斥量,利用优先级继承机制
  • 避免中优先级干扰:合理规划优先级,别让不相关的任务插在中间

嗯,调度机制这块就讲到这里。下一章咱们聊聊内存管理,那也是个容易踩坑的地方。