4、优先级决定机制:任务创建时的优先级分配,优先级继承与优先级天花板协议
好,咱们接着聊优先级。上一节我们把优先级反转这个“坑”给挖出来了。那怎么填坑呢?这就涉及到我今天要讲的核心内容——优先级决定机制。
说白了,优先级不是一拍脑袋定的。它有一套完整的决策逻辑。我个人习惯把这个问题拆成三个阶段来看:任务创建时怎么分、运行时怎么动态调、遇到共享资源时怎么防死锁。咱们一个一个说。
4.1 任务创建时的优先级分配
这是最基础的一步。很多新手工程师上来就写:
osThreadNew(task1, NULL, &attr1); // 优先级设成 5
osThreadNew(task2, NULL, &attr2); // 优先级设成 5
osThreadNew(task3, NULL, &attr3); // 优先级设成 5
嗯,全一样。你想想看,这跟没设有什么区别?系统调度器一看,三个任务优先级相同,那就轮着来呗。实时性?不存在的。
我建议你遵循一个原则:任务优先级 = 任务的时间紧迫度 + 任务的重要性。注意,是“紧迫度”优先于“重要性”。
举个例子。一个温度采集任务,每100ms必须读一次传感器。另一个是日志打印任务,晚个几秒无所谓。那温度采集的优先级必须高于日志打印。哪怕日志打印再重要,也不能抢温度采集的CPU时间。
我在项目中遇到过一种情况:有个同事把UI刷新任务的优先级设得特别高,结果底层通信任务老是丢包。为什么?因为UI一刷新就把CPU占满了,通信任务抢不到时间片。这就是典型的“紧迫度”判断失误。
核心原则:
- 硬实时任务(有严格截止时间)→ 最高优先级
- 软实时任务(允许偶尔超时)→ 中等优先级
- 非实时任务(后台型)→ 最低优先级
另外,我建议你给优先级留出“余量”。比如系统最大支持32级优先级,你实际只用0~20。为什么?因为后期加功能时,你可能需要在两个现有优先级之间插入一个新任务。没有余量,你就得重构整个优先级表。
4.2 优先级继承协议
好,任务创建时的分配搞定了。但运行时呢?还记得上一节讲的那个低优先级任务持有锁、高优先级任务被阻塞的例子吗?
优先级继承协议就是用来解决这个问题的。它的核心思想很简单:当低优先级任务持有高优先级任务需要的资源时,低优先级任务临时“继承”高优先级任务的优先级。
你想想看,这招妙不妙?低优先级任务被临时“提拔”了,它就能更快地运行完、释放资源。高优先级任务也不用干等了。
具体实现上,RTOS内核会维护一个“优先级继承链”。比如:
- 任务A(优先级10)等待任务B持有的互斥锁
- 任务B(优先级3)临时提升到优先级10
- 任务B释放锁后,优先级降回3
- 任务A获得锁,继续运行
我在项目中用过FreeRTOS的互斥量,它默认就支持优先级继承。但要注意一点:优先级继承只在使用互斥量(Mutex)时生效,信号量(Semaphore)不支持。这是个常见的坑。
警告:优先级继承不是万能的。它只能缓解优先级反转,不能完全消除。而且继承链如果太长(比如A等B、B等C、C等D),系统开销会显著增加。我曾经在一个项目中看到过5层继承链,调度延迟直接翻倍。
4.3 优先级天花板协议
如果说优先级继承是“事后补救”,那优先级天花板协议就是“事前预防”。
这个协议的做法是:给每个共享资源设定一个“天花板优先级”。任何任务想要访问这个资源,它的优先级必须被临时提升到天花板优先级。
举个例子。一个共享缓冲区,天花板优先级设为8。那么:
- 任务A(优先级5)访问它 → 临时提升到8
- 任务B(优先级7)访问它 → 临时提升到8
- 任务C(优先级9)访问它 → 不需要提升,因为已经高于天花板
这样做的好处是什么?任何任务在访问共享资源时,它的优先级都不会低于天花板。这就从根本上避免了低优先级任务被高优先级任务抢占的情况。
我个人更倾向于在复杂系统中使用优先级天花板协议。为什么?因为它简单、可预测。你不需要像优先级继承那样去动态追踪继承链,系统开销更小。
但缺点也很明显:天花板优先级设得太高,会导致不必要的优先级提升。比如一个低优先级任务只是偶尔访问一下共享资源,结果被提升到很高的优先级,反而影响了其他任务的调度。
我的经验:天花板优先级一般设为“可能访问该资源的所有任务中的最高优先级”。别设得比这个还高,否则就是过度设计。我曾经见过有人把天花板设成系统最高优先级,结果所有任务都在最高优先级上跑,实时性反而下降了。
4.4 两种协议的对比与选择
说了这么多,到底该用哪个?我整理了一个对比表,你一看就明白:
| 特性 | 优先级继承 | 优先级天花板 |
|---|---|---|
| 触发时机 | 发生阻塞时动态调整 | 访问资源前静态提升 |
| 系统开销 | 较高(需要维护继承链) | 较低(固定提升) |
| 死锁风险 | 较低 | 更低 |
| 适用场景 | 任务数量少、资源竞争不激烈 | 任务数量多、资源竞争激烈 |
| 实现复杂度 | 中等 | 简单 |
我个人的建议是:小系统用优先级继承,大系统用优先级天花板。小系统任务少,继承链不会太长,开销可控。大系统任务多,用天花板协议更稳定、更可预测。
嗯,这里要注意一点:有些RTOS内核同时支持两种协议。比如μC/OS-III,你可以在创建互斥量时选择使用哪种。但别混用——同一个资源上同时用两种协议,调度逻辑会乱掉。
4.5 避坑指南
最后,我把自己踩过的坑总结一下,你写代码时多留个心眼:
我曾经...:
- 把信号量当互斥量用,结果优先级继承没生效,系统频繁死锁。后来才意识到,信号量不支持优先级继承。
- 在中断服务函数里使用了带优先级继承的互斥量。结果中断上下文不能阻塞,直接触发断言。记住:中断里别用互斥量,用信号量或者标志位。
- 天花板优先级设得过高,导致一个低优先级的后台任务频繁被提升,影响了整个系统的实时性。后来我把天花板降了两级,问题就解决了。
好了,优先级决定机制就讲到这里。下一节我们聊聊优先级反转的实战案例——我会拿一个真实的项目代码来拆解,看看问题是怎么出现的,又是怎么解决的。