调度器到底做什么
RTOS 调度器是内核的心脏。它每毫秒决定"现在该跑哪个任务"。核心原则很简单:永远跑最高优先级的就绪任务。但实际工程中,优先级怎么分配、任务之间怎么共享资源、高优先级被低优先级堵住怎么办,这些才是真正的坑。
调度器不是越复杂越好。简单的抢占式优先级调度,配合合理的优先级划分,能解决 90% 的实时问题。
这篇讲 RTOS 调度的核心概念和实战踩坑。
抢占式与协作式
两种调度方式:
调度方式对比
- 抢占式高优先级任务就绪立刻抢 CPU,实时性好。
- 协作式任务主动让出 CPU,简单但实时性差。
- 混合大部分抢占,关键段关中断临界区。
// FreeRTOS 创建任务
xTaskCreate(vHighTask, "High", 256, NULL, 3, NULL); // 优先级 3
xTaskCreate(vLowTask, "Low", 256, NULL, 1, NULL); // 优先级 1
// HighTask 一就绪就抢占 LowTask
优先级翻转
优先级翻转是 RTOS 最经典的坑。场景:高优先级任务 H 等低优先级任务 L 释放信号量,但中等优先级任务 M 一直跑,把 L 堵在后面,H 等不到信号量,实际变成 M 在跑。H 的优先级被 L 间接拉低了。
// 问题场景:
// H (prio 3) 等 mutex
// L (prio 1) 持有 mutex,但被 M (prio 2) 抢占
// M 一直跑 -> L 跑不完 -> 不释放 mutex -> H 永远等
// H 的实际响应时间 = M 的运行时间
优先级继承
解决办法:优先级继承(Priority Inheritance)。L 持有 mutex 时,临时把 L 的优先级提升到 H 的级别,让 L 尽快跑完释放 mutex,然后降回去。
// FreeRTOS 使用带优先级继承的互斥量
SemaphoreHandle_t m = xSemaphoreCreateMutex();
xSemaphoreTake(m, portMAX_DELAY); // 持有期间优先级被继承提升
// 做关键操作
xSemaphoreGive(m); // 释放后优先级降回
注意:递归互斥量(Recursive Mutex)允许同一任务多次 take。
时间片轮转
同优先级任务之间,默认时间片轮转:每个任务跑一个 tick 时间片,然后切到下一个。
// FreeRTOS 同优先级任务自动时间片轮转
configUSE_TIME_SLICING = 1 // 在 FreeRTOSConfig.h 开启
// 同优先级任务自动轮流跑,每个跑 configTICK_RATE_HZ 分之一秒
时间片大小要权衡:太小切换开销大,太大响应慢。一般 1~10ms。
死锁与饥饿
两个任务互相等对方的锁,死锁。
死锁四条件
- 互斥资源同时只能一个任务用。
- 持有并等待持有一个等另一个。
- 不可剥夺别人不能抢。
- 循环等待A 等 B,B 等 A。
破局:固定加锁顺序、带超时的 take。
调试调度问题
// 1. 用 trace 工具看任务 CPU 占用
// 2. 高优先级任务别做太多事,保持短小
// 3. 中断里别做复杂逻辑,发通知给任务处理
// 4. 栈大小别省,溢出是常见崩溃原因
// 5. 用 uxTaskGetStackHighWaterMark 看栈水位
RTOS 调试最怕"偶发卡死"。多半是优先级翻转或死锁。先画任务优先级图和锁依赖图,问题一目了然。
把调度原理吃透,RTOS 才不会变成黑盒。