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持有的锁时:

  1. 系统发现锁被L持有
  2. 检查L的当前优先级是否低于H
  3. 如果是,把L的当前优先级提升到H的优先级
  4. 记录这个继承关系(谁继承了谁的优先级)
  5. 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 实际应用建议

我个人习惯在以下场景使用优先级继承:

  1. 锁的持有时间短:比如保护一个全局变量、一个状态标志
  2. 任务数量不多:10个以内,链式继承的概率低
  3. 对实时性要求高:需要精确计算最坏情况响应时间

如果锁的持有时间很长(比如操作外设、等待DMA完成),我建议考虑其他方案,比如用消息队列代替互斥锁,或者干脆重新设计任务划分。

嗯,优先级继承协议就聊到这儿。下一章我会讲优先级天花板协议,那个方案更激进一些,但某些场景下更好用。