4、优先级继承协议:原理、实现方式、优缺点
好,咱们接着聊优先级反转的解决方案。
上一章我讲了优先级反转是怎么发生的,也提到了几种解决思路。今天重点聊其中一种——优先级继承协议。这个协议在实际项目中用得非常多,我个人觉得它是性价比最高的方案之一。
4.1 什么是优先级继承?
说白了,优先级继承的核心思想就一句话:当低优先级任务持有高优先级任务需要的资源时,低优先级任务临时“继承”高优先级任务的优先级。
你想想看,为什么会出现优先级反转?就是因为低优先级任务L占着锁不放,中优先级任务M在那儿瞎跑,把高优先级任务H活活饿死了。那怎么办?让L临时“升级”,升到和H一样高,这样M就抢不过L了。L赶紧跑完,释放锁,H就能拿到资源继续执行。
嗯,这里要注意:继承是临时的。任务L释放资源后,优先级会立刻恢复原样。
关键点:优先级继承不是改变任务的静态优先级,而是动态调整。只在持有资源期间生效。
4.2 实现方式
我在项目中实现过好几次优先级继承,其实逻辑并不复杂。核心就两个动作:提升和恢复。
4.2.1 核心数据结构
每个任务需要多维护几个字段。我一般这样定义:
typedef struct {
uint8_t base_priority; // 任务的基础优先级
uint8_t current_priority; // 当前实际优先级
uint8_t inherited_priority; // 继承来的最高优先级
osMutexId_t *held_mutex; // 当前持有的互斥锁列表
} TCB_t;
这里 current_priority 是真正参与调度的优先级。它等于 max(base_priority, inherited_priority)。
4.2.2 获取锁时的逻辑
当一个高优先级任务H尝试获取一个已被低优先级任务L持有的锁时:
- 系统发现锁被L持有
- 检查L的当前优先级是否低于H
- 如果是,把L的当前优先级提升到H的优先级
- 记录这个继承关系(谁继承了谁的优先级)
- H进入阻塞状态,等待锁释放
代码实现大概长这样:
void osMutexAcquire(osMutexId_t mutex, Task_t *task) {
if (mutex->owner == NULL) {
// 锁空闲,直接获取
mutex->owner = task;
return;
}
// 锁被占用,检查是否需要优先级继承
Task_t *owner = mutex->owner;
if (owner->current_priority < task->current_priority) {
// 继承!把持有者的优先级提上来
owner->inherited_priority = task->current_priority;
owner->current_priority = max(owner->base_priority,
owner->inherited_priority);
// 触发重新调度
osSchedulerReschedule();
}
// 当前任务进入阻塞
task->state = BLOCKED;
}
4.2.3 释放锁时的逻辑
释放锁的时候,需要把继承的优先级还回去:
void osMutexRelease(osMutexId_t mutex, Task_t *task) {
// 检查是否有其他任务因为等待这个锁而提升了我们的优先级
Task_t *waiting_task = getHighestPriorityWaiter(mutex);
if (waiting_task != NULL) {
// 还有任务在等,把锁转给它
mutex->owner = waiting_task;
waiting_task->state = READY;
} else {
// 没有等待者了,恢复基础优先级
task->inherited_priority = task->base_priority;
task->current_priority = task->base_priority;
mutex->owner = NULL;
}
osSchedulerReschedule();
}
我的经验:释放锁时一定要检查是否还有任务在等这个锁。如果有,直接把锁转给等待者,而不是先释放再重新获取。这样可以减少一次上下文切换的开销。
4.3 优缺点分析
任何方案都有两面性。优先级继承协议也不是银弹。
4.3.1 优点
- 实现相对简单:比起优先级天花板协议,继承协议的逻辑更直观,代码量也少
- 不会过度提升优先级:只提升到恰好需要的级别,不会造成不必要的调度延迟
- 死锁风险低:因为优先级是动态调整的,不会像天花板协议那样容易引发死锁
- 实时性有保障:高优先级任务的阻塞时间有上界,理论上可以计算
4.3.2 缺点
- 可能发生链式继承:如果多个任务嵌套持有锁,优先级会像多米诺骨牌一样传递。我曾经在一个项目中遇到过三层继承,调试起来相当头疼
- 不能完全避免反转:只能缩短反转时间,不能彻底消除。极端情况下反转时间仍然可能很长
- 增加了调度开销:每次获取/释放锁都要检查优先级,频繁操作时性能会受影响
- 对嵌套锁支持复杂:任务持有多个锁时,继承逻辑会变得很绕
避坑指南:我曾经在一个项目中遇到过一个bug——任务释放锁后忘记恢复优先级,导致一个低优先级任务一直以高优先级运行,把其他重要任务都饿死了。排查了两天才找到原因。所以实现时一定要在释放锁的路径上做严格的优先级恢复检查。
4.4 实际应用建议
我个人习惯在以下场景使用优先级继承:
- 锁的持有时间短:比如保护一个全局变量、一个状态标志
- 任务数量不多:10个以内,链式继承的概率低
- 对实时性要求高:需要精确计算最坏情况响应时间
如果锁的持有时间很长(比如操作外设、等待DMA完成),我建议考虑其他方案,比如用消息队列代替互斥锁,或者干脆重新设计任务划分。
嗯,优先级继承协议就聊到这儿。下一章我会讲优先级天花板协议,那个方案更激进一些,但某些场景下更好用。