4. 任务优先级:优先级分配策略、优先级反转问题、优先级继承机制
好,咱们来聊聊优先级。这玩意儿在RTOS里,说白了就是「谁先跑」的问题。我刚开始做嵌入式那会儿,觉得优先级嘛,数字大的先跑呗,多简单。结果呢?项目一复杂,各种诡异问题就冒出来了。今天我把这些年踩过的坑和总结的经验,一次性给你讲透。
4.1 优先级分配策略:不是拍脑袋定的
优先级怎么分?很多人上来就「这个任务重要,给高优先级;那个任务不重要,给低优先级」。嗯,听起来没毛病,但实际上一堆坑。
我个人习惯用这么几种策略:
- 速率单调调度(RMS):周期越短的任务,优先级越高。比如一个10ms的传感器采集任务,优先级肯定比一个100ms的显示刷新任务高。这个策略在数学上被证明是最优的静态优先级分配方案。
- 截止时间最早优先(EDF):谁先到截止时间,谁先跑。这个更灵活,但实现起来复杂,我一般只在高端MCU上用。
- 关键性分析:有些任务虽然不频繁,但一旦出问题就是灾难。比如充电宝的电池保护任务,哪怕它一年只触发一次,也得给最高优先级。
核心原则:优先级分配不是「谁重要谁高」,而是「谁耽误不起谁高」。一个10ms必须响应的按键扫描,比一个1秒刷新一次的温度显示「重要」得多。
我在项目中遇到过这么个事:一个同事把UI刷新任务设成了最高优先级,结果电池保护任务被活活饿死。充电宝过热了,UI还在那美滋滋地刷新动画呢。你说这设计是不是要命?
4.2 优先级反转问题:一个经典陷阱
优先级反转,这词听着挺唬人。其实说白了就是:一个高优先级任务,被一个低优先级任务堵住了。
举个例子你就明白了:
- 任务A(高优先级)和任务C(低优先级)共享一个互斥锁
- 任务B(中优先级)不共享这个锁,但一直在跑
- 场景:C先拿到了锁,然后被B抢占了CPU。这时候A来了,想拿锁,但锁在C手里。C又被B抢占了,跑不了。结果A只能等B跑完,等C释放锁。A明明优先级最高,却被B和C联手堵死了。
你想想看,这合理吗?完全不合理。这就是优先级反转。
我曾经踩过的坑:在一个充电宝项目中,我用了全局变量做标志位,没加锁。结果高优先级的电量检测任务读到的数据,总是被低优先级的通信任务写了一半。数据错乱,电量显示忽高忽低。用户投诉说「你们这充电宝电量是随机的吧?」。嗯,从那以后我再也不敢裸奔共享数据了。
4.3 优先级继承机制:解决反转的利器
怎么解决优先级反转?最常用的就是优先级继承。
它的原理很简单:当一个低优先级任务持有高优先级任务需要的锁时,低优先级任务临时「继承」高优先级任务的优先级。这样它就能不被中优先级任务打断,赶紧跑完释放锁。
还是刚才那个例子:
- C拿到锁后,A来了要锁。这时候C的优先级被临时提升到和A一样高。
- B(中优先级)抢不过C了,C继续跑。
- C释放锁后,优先级恢复原样,A拿到锁开始跑。
完美解决,对吧?
代码实现上,FreeRTOS和uC/OS都内置了这个机制。你只需要在创建互斥量时选择支持优先级继承的版本就行:
// FreeRTOS 示例:使用支持优先级继承的互斥量
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// 任务A(高优先级)
void TaskA(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
xSemaphoreGive(xMutex);
}
}
}
// 任务C(低优先级)
void TaskC(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
xSemaphoreGive(xMutex);
}
}
}
小提示:用二值信号量(Binary Semaphore)做互斥时,没有优先级继承机制。一定要用互斥量(Mutex)。我见过太多人把信号量当互斥量用,然后被优先级反转折磨得死去活来。
4.4 实际项目中的优先级设计建议
说了这么多理论,来点实际的。我在充电宝项目里一般这么分优先级:
| 优先级 | 任务名称 | 说明 |
|---|---|---|
| 最高(5) | 电池保护任务 | 过压、过流、过温保护,必须立即响应 |
| 高(4) | 充电管理任务 | 充电电流控制,需要实时调节 |
| 中(3) | 电量检测任务 | ADC采样,周期10ms |
| 中低(2) | 按键扫描任务 | 去抖处理,周期20ms |
| 低(1) | 显示刷新任务 | LED/LCD更新,周期100ms |
| 最低(0) | 通信任务 | 蓝牙/串口通信,可以慢一点 |
你看,电池保护任务优先级最高,哪怕它一年只触发一次。通信任务优先级最低,因为用户等个几百毫秒收到数据完全没问题。
避坑指南:我曾经把通信任务优先级设得比电量检测还高,结果电量数据更新不及时,用户看到的电量永远是5分钟前的。嗯,用户体验极差。后来我学乖了:实时性要求高的任务,优先级一定要高,哪怕它看起来「不重要」。
4.5 总结一下
优先级这东西,说简单也简单,说复杂也复杂。我给你的建议是:
- 先画任务图:把每个任务的周期、截止时间、共享资源都列出来。
- 用RMS做初步分配:周期短的给高优先级。
- 检查共享资源:有锁的地方,一定要用支持优先级继承的互斥量。
- 留余量:别把优先级用满,留一两个给紧急任务。
- 实测验证:用逻辑分析仪抓一下任务切换时序,看看有没有反转现象。
记住一句话:优先级分配不是一劳永逸的事。项目迭代过程中,任务变了,优先级也得跟着调。别怕改,就怕不改。
好,这一章就到这。下一章咱们聊聊任务间通信,那又是一个大坑。到时候我给你讲讲我当年用全局变量通信,结果被DMA踩内存的惨痛经历。