第4章:任务优先级与调度策略
各位同学,今天我们来聊聊RTOS调度里最核心、也最容易踩坑的话题——任务优先级与调度策略。说实话,我做了十几年电机控制,见过太多因为优先级没设好导致电机抖成筛子的案例。咱们今天就把抢占式调度、时间片轮转和优先级反转这三个硬骨头啃下来。
4.1 抢占式调度:谁急谁先跑
抢占式调度,说白了就是「高优先级任务一来,低优先级任务立马靠边站」。我习惯把它比作医院急诊——心跳骤停的病人来了,你总不能让看感冒的人先看吧?
在电机控制里,这个特性特别重要。比如电流环任务通常跑在最高优先级,因为它要处理过流保护。我记得有一次调试伺服驱动器,电流环优先级设低了,结果位置环一卡顿,电流采样没及时处理,IGBT直接炸了……嗯,从那以后我再也不敢把电流环优先级排第二。
抢占式调度的核心规则就三条:
- 高优先级任务就绪时,立即抢占CPU——哪怕低优先级任务刚执行到一半
- 同优先级任务按FIFO顺序执行——先来后到,不搞特殊
- 任务主动让出CPU时,才轮到同优先级下一个——比如调用delay()或等待信号量
实际项目中的经验:
我在做无刷直流电机控制时,把电流环优先级设为最高(优先级0),速度环次之(优先级1),位置环最低(优先级2)。这样即使位置环算到一半,电流环要抢CPU,也能保证电流采样和PWM更新不被耽误。你想想看,电流环要是延迟了100微秒,电机可能就啸叫了。
4.2 时间片轮转调度:雨露均沾
时间片轮转,其实就是「大家轮流用CPU,每人分一个时间片」。这个策略在电机控制里用得不多,但也不是没用。比如你同时控制两台电机,它们优先级一样,用时间片轮转就能保证两台电机都能得到响应。
为什么会这样?因为抢占式调度下,同优先级任务如果不主动让出CPU,后面的任务永远跑不了。时间片轮转就解决了这个问题——每个任务跑完一个时间片(比如1ms),系统就强制切换。
我个人的习惯是:
- 实时性要求高的任务——用抢占式,设高优先级
- 多个同等重要的后台任务——用时间片轮转,比如日志记录、状态显示
- 混合使用——高优先级任务抢占,同优先级任务轮转
避坑指南:
我曾经在一个项目里把所有任务都设成相同优先级,指望时间片轮转自动分配。结果电机控制任务每次只跑1ms就被切走,电流环根本来不及算完。后来我把电流环优先级单独提上来,问题才解决。记住:时间片轮转不保证实时性,它只保证公平性。
4.3 优先级反转:一个经典的坑
优先级反转,这词听着挺吓人,其实说白了就是「低优先级任务把高优先级任务堵死了」。我刚开始做RTOS时,死活想不通——高优先级怎么会被低优先级堵住?直到我在一个电机控制项目里踩了这个坑。
场景是这样的:
- 任务A(高优先级)要访问共享资源,比如一个编码器数据缓冲区
- 任务C(低优先级)正占着这个缓冲区,还没释放
- 任务B(中优先级)不访问缓冲区,但一直在跑
结果呢?任务A等任务C释放资源,但任务C被任务B抢占了CPU。任务B跑得欢,任务C一直没机会释放资源,任务A就只能干等。你看,高优先级的任务A,反而被中优先级的任务B给「反转」了。
注意:
优先级反转在电机控制里特别危险。比如电流环任务(高优先级)要等一个低优先级的通信任务释放互斥锁,结果通信任务被其他任务抢占了。电流环迟迟拿不到锁,过流保护触发不了,电机可能直接烧毁。我见过一次,那味道……嗯,你懂的。
4.4 解决方案:优先级继承与优先级天花板
怎么解决优先级反转?业界常用的有两种方法:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 优先级继承 | 低优先级任务持有锁时,临时提升到等待该锁的最高优先级 | 锁的持有时间不确定 |
| 优先级天花板 | 任务持有锁时,优先级直接提升到可能请求该锁的最高优先级 | 锁的持有时间短且固定 |
我个人更推荐优先级继承,因为它在FreeRTOS和uC/OS里都原生支持。你只需要在创建互斥量时选择「优先级继承」选项就行。
// FreeRTOS 示例:创建带优先级继承的互斥量
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// 任务A(高优先级)请求互斥量
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
encoder_data = read_encoder();
xSemaphoreGive(xMutex);
}
// 任务C(低优先级)持有互斥量时,优先级自动提升
// 释放后恢复原优先级
我的建议:
在电机控制项目里,尽量少用互斥量。能用消息队列传递数据,就别用共享内存加锁。我现在的习惯是:电流环和速度环之间用队列通信,位置环和速度环之间也用队列。这样根本不需要锁,优先级反转问题自然就没了。
4.5 实际调度策略选择
说了这么多,到底怎么选?我总结了一张表,你直接照着用就行:
| 应用场景 | 推荐调度策略 | 优先级设置 |
|---|---|---|
| 电流环控制(1-10kHz) | 抢占式 | 最高优先级 |
| 速度环控制(100-1000Hz) | 抢占式 | 次高优先级 |
| 位置环控制(10-100Hz) | 抢占式 | 中等优先级 |
| 通信任务(CAN/RS485) | 抢占式或时间片轮转 | 低优先级 |
| 日志/监控任务 | 时间片轮转 | 最低优先级 |
最后说一句:调度策略没有银弹。你想想看,电机控制里每个任务的实时性要求都不一样,硬套一种策略肯定出问题。我的做法是:先按实时性排优先级,再处理共享资源,最后用示波器抓一下任务切换时间,确认没有优先级反转。嗯,这样基本就稳了。
下一章咱们聊聊任务间通信,到时候我会讲怎么用消息队列和信号量来避免今天说的这些问题。先消化一下,有问题随时问。