在实时操作系统中,互斥锁不仅需要解决共享资源的互斥访问问题,还需要考虑资源竞争对任务调度的影响。
当低优先级任务持有互斥锁,而高优先级任务需要获取同一把锁时,高优先级任务只能等待。如果此时又有中优先级任务不断抢占低优先级任务,那么持锁任务可能迟迟无法运行,高优先级任务也就无法及时获得资源。
这种现象称为优先级反转(Priority Inversion)。
为缓解这一问题,HRTOS 在互斥锁实现中引入了优先级继承机制。本文结合mutex_lock.c和mutex_unlock.c,分析优先级继承的工作原理、实现流程,以及当前实现需要关注的边界情况。
一、什么是优先级反转?
假设系统中有三个任务:
| 任务 | 初始优先级 | 职责 |
|---|---|---|
| Task L | 1 | 低优先级任务,持有互斥锁 |
| Task M | 3 | 中优先级任务,不需要该锁 |
| Task H | 5 | 高优先级任务,需要获取同一把锁 |
这里假设数值越大,任务优先级越高。
执行过程如下:
Task L 获得互斥锁,开始访问共享资源。
Task H 抢占 Task L,但发现互斥锁已经被占用,因此进入等待状态。
Task M 就绪,其优先级高于 Task L,因此抢占 Task L。
Task L 无法及时继续执行,也就无法释放互斥锁。
Task H 只能继续等待。
问题在于,Task H 本来具有最高优先级,却因为低优先级任务持有资源而被迫等待;与此同时,与该资源无关的 Task M 还可能延长这段等待时间。
这就是优先级反转问题。
需要注意,优先级反转并不意味着调度器直接把高优先级任务排到了低优先级任务之后,而是任务之间的资源依赖关系间接影响了调度结果。
二、优先级继承的基本原理
优先级继承的核心思路是:
当高优先级任务因互斥锁而阻塞时,临时提高持锁任务的当前优先级,使其能够更快获得 CPU 时间并释放资源。
在上面的例子中:
Task L 的基础优先级为 1;
Task H 的优先级为 5;
Task H 因等待互斥锁而阻塞;
Task L 的当前优先级从 1 提升到 5;
Task L 更容易抢占 Task M,继续执行并释放互斥锁;
Task H 获得锁后继续运行。
当互斥锁释放后,持锁任务的优先级需要恢复。
优先级继承并不是永久修改任务的基础优先级,而是根据资源竞争情况临时调整当前优先级。
因此,内核需要区分两个概念:
基础优先级(base_prio):任务原本的优先级。
当前优先级(cur_prio):任务当前参与调度时使用的优先级。
HRTOS 的任务控制块包含这两个字段:
typedef struct { u8 base_prio; /* 静态优先级 */ u8 cur_prio; /* 动态优先级 */ u8 state; u8 wait_type; u8 wait_flag; u8 wait_obj; u16 wait_tick; } OS_TCB;这种设计为优先级继承提供了必要的数据基础:内核可以临时调整cur_prio,并保留base_prio作为恢复依据。
三、HRTOS 如何实现优先级继承?
1. 检查互斥锁参数
在os_mutex_lock()中,首先检查互斥锁编号是否合法:
if (mid < 0 || mid >= OS_RESOURCE_MAX) { return -1; }通过参数检查,避免使用超出资源对象范围的互斥锁编号。
随后,内核获取当前任务 ID 和对应的资源对象,并进入临界区。
2. 禁止递归获取同一把锁
HRTOS 当前的互斥锁不支持递归获取。
代码通过检查锁的拥有者来识别当前任务是否已经持有该锁:
if (m->owner == tid) { EA = 1; return -1; }如果当前任务已经是锁拥有者,再次获取同一把锁就会返回失败。
这能够避免任务因为重复获取非递归互斥锁而陷入自我等待。
3. 互斥锁空闲时直接获取
如果互斥锁没有拥有者,当前任务可以直接获得该锁:
if (m->owner == OS_INVALID_ID) { m->owner = tid; OS_TASK[tid].base_prio = (OS_PROCESS_OK[tid] & 14) >> 1; OS_TASK[tid].cur_prio = OS_TASK[tid].base_prio; EA = 1; return 1; }这里有两个重要操作。
第一,将当前任务设置为互斥锁拥有者。
第二,根据OS_PROCESS_OK中编码的优先级恢复任务的基础优先级,并同步到cur_prio。
这段逻辑体现了当前实现的优先级管理方式:在直接获得空闲互斥锁时,任务的当前优先级被重新设为基础优先级。
4. 锁已被占用时,执行优先级继承
这是整个实现最关键的部分:
OS_TASK[tid].base_prio = (OS_PROCESS_OK[tid] & 14) >> 1; OS_TASK[tid].cur_prio = OS_TASK[tid].base_prio; if (OS_TASK[tid].cur_prio > OS_TASK[m->owner].cur_prio) { OS_TASK[m->owner].cur_prio = OS_TASK[tid].cur_prio; OS_PROCESS_OK[m->owner] &= 0xF1; OS_PROCESS_OK[m->owner] |= ((OS_TASK[tid].cur_prio & 0x07) << 1); }可以将这段代码拆成三个步骤。
第一步:取得等待任务的基础优先级。
内核从OS_PROCESS_OK中提取优先级,并将其保存为当前任务的基础优先级。
按照当前代码的位操作,优先级存储在OS_PROCESS_OK的 bit1~bit3 中。
第二步:比较等待任务与锁拥有者的当前优先级。
if (OS_TASK[tid].cur_prio > OS_TASK[m->owner].cur_prio)只有等待任务的当前优先级更高时,才需要提升锁拥有者的优先级。
如果等待任务的优先级低于或等于锁拥有者,则不需要通过这次竞争进一步提升锁拥有者的优先级。
第三步:更新锁拥有者的当前优先级及调度字段。
OS_TASK[m->owner].cur_prio = OS_TASK[tid].cur_prio;这条语句完成任务控制块中的优先级提升。
随后:
OS_PROCESS_OK[m->owner] &= 0xF1; OS_PROCESS_OK[m->owner] |= ((OS_TASK[tid].cur_prio & 0x07) << 1);清除OS_PROCESS_OK中原有的 bit1~bit3,再写入新的优先级编码。
这一步很重要:如果调度器实际根据OS_PROCESS_OK中的优先级字段进行任务选择,仅修改cur_prio就不够,还需要同步调度器使用的优先级信息。
因此,HRTOS 当前的优先级继承涉及两个层面:
更新
OS_TCB中的当前优先级;同步
OS_PROCESS_OK中用于调度的优先级编码。
5. 进入等待状态
优先级继承处理完成后,当前代码退出临界区,再调用:
return os_wait(WAIT_MUTEX, mid, 0);这表示当前任务等待指定的互斥锁,等待类型为WAIT_MUTEX,超时参数为 0。
这里需要区分两个动作:
优先级继承:提高锁拥有者的当前优先级。
任务等待:让当前任务进入互斥锁等待流程。
前者用于改善持锁任务的调度机会,后者用于管理无法立即获取锁的任务。
四、释放互斥锁时,如何恢复优先级?
优先级继承不仅需要提升优先级,还需要处理资源释放后的优先级恢复。
在os_mutex_unlock()中,首先验证互斥锁编号,并确认当前任务确实是锁拥有者。
随后执行:
OS_TASK[tid].cur_prio = OS_TASK[tid].base_prio; OS_PROCESS_OK[tid] &= 0xF1; OS_PROCESS_OK[tid] |= ((OS_TASK[tid].cur_prio & 0x07) << 1);这段代码将当前任务的优先级恢复为基础优先级,并同步更新调度字段。
在只有一把相关互斥锁、且没有其他优先级继承需求的简单场景中,这符合预期:任务不再需要为等待者继承高优先级,便可以恢复原有优先级。
但这里存在一个需要认真对待的边界条件:如果一个任务同时持有多把互斥锁,释放其中一把锁,并不一定意味着它可以立即恢复到基础优先级。
例如:
Task L 持有互斥锁 A。
Task L 又获得互斥锁 B。
高优先级任务因等待锁 A,导致 Task L 的优先级被提升。
Task L 释放锁 B,但仍然持有锁 A。
在这种情况下,Task L 仍然可能需要维持继承优先级。
当前代码在每次成功释放互斥锁时,都会直接执行cur_prio = base_prio。如果释放的锁不是导致优先级继承的唯一原因,这种恢复方式就可能过早降低任务优先级。
因此,这段代码适用于怎样的资源使用约束,需要结合 HRTOS 对多锁持有及优先级继承的整体设计进一步确认。
五、互斥锁释放时,如何选择等待任务?
完成当前任务的优先级恢复后,代码检查互斥锁的等待位图:
if (m->wait_mask)如果存在等待任务,就遍历等待集合,选择当前优先级最高的任务:
for (i = 0; i < OS_PROCESS_MAX; i++) { if (mask & ((u16)1 << i)) { if (OS_TASK[i].cur_prio > best_prio) { best_prio = OS_TASK[i].cur_prio; next = i; } } }这里采用位图表示等待任务集合,再逐个检查对应任务的当前优先级。
由于当前代码使用严格大于号比较,如果多个等待任务具有相同的最高优先级,那么最终选择的是遍历顺序中最先遇到的那个任务,也就是 ID 较小的任务。
找到等待任务后,内核先转移互斥锁所有权:
m->owner = next;随后唤醒该任务:
wake_task(next, WAIT_SIGNAL);最后返回 1,表示释放互斥锁并将所有权转移给等待任务。
这种处理方式的一个重要特点是:所有权转移发生在唤醒之前。
对于被唤醒的任务而言,内核已经把互斥锁所有者更新为该任务,而不是先把锁设为空闲,再等待任务重新竞争。这种直接交接方式能够让锁的释放与等待者唤醒保持一致。
不过,优先级继承与等待者选择仍然是两个不同的问题:前者影响持锁任务的调度优先级,后者决定释放锁后哪个等待任务获得所有权。
六、当前实现的适用边界
从代码可以看出,HRTOS 已经具备优先级继承的基本路径:
检查互斥锁编号与递归获取;
空闲时直接获取互斥锁;
竞争发生时比较等待任务与锁拥有者的当前优先级;
必要时提升锁拥有者优先级;
在
OS_PROCESS_OK中同步调度优先级;释放互斥锁时恢复基础优先级;
从等待位图中选择最高优先级任务并转移锁所有权。
但从实时操作系统的完整协议角度,还需要区分几个问题。
第一,多互斥锁场景下的优先级恢复。
如果任务持有多把锁,释放其中一把锁后,是否仍然存在其他优先级继承需求,需要由内核明确处理。简单恢复基础优先级无法覆盖所有此类场景。
第二,优先级继承链的传播。
如果任务 L 持有锁 A,同时等待任务 M 持有的锁 B,那么更高优先级任务等待锁 A 时,优先级提升是否需要继续传播到锁 B 的拥有者,取决于内核是否实现了传递式优先级继承。当前给出的代码没有展示这类传播逻辑。
第三,基础优先级的来源。
当前实现从OS_PROCESS_OK中提取基础优先级,因此需要保证这个字段始终反映正确的基础优先级,而不会因为动态优先级更新而丢失原始值。
第四,优先级提升后的调度一致性。
当前代码同步修改cur_prio与OS_PROCESS_OK,这是必要的实现细节。还应结合调度器确认,在锁拥有者已经就绪、等待或处于其他状态时,优先级更新是否能够正确反映到任务选择过程。
这些问题不意味着当前实现必然存在实际故障,而是说明:基础优先级继承路径与完整、可处理复杂资源依赖的优先级继承协议之间,仍然存在需要明确的设计边界。
总结
优先级继承是 RTOS 互斥锁设计中的重要机制。它通过临时提高锁拥有者的当前优先级,缓解低优先级任务持锁导致高优先级任务长期等待的问题。
HRTOS 当前实现将优先级继承与任务控制块、OS_PROCESS_OK调度字段、互斥锁等待位图以及任务唤醒机制结合起来,形成了一条完整的基本处理路径。
与此同时,互斥锁机制的可靠性不仅取决于能否提升优先级,还取决于释放锁后的优先级恢复、多锁持有以及资源依赖链等边界条件。
优先级继承真正需要解决的,不只是让持锁任务变得更高优先级,而是让优先级调整始终与任务的资源依赖关系保持一致。
对于 8051 这类资源受限平台,如何在有限的内存与代码规模下实现清晰、可验证的互斥锁行为,是内核设计中值得深入研究的问题。