三任务 H/M/L 的经典悲剧:最高优先级任务反而被两个低优先级任务“按在地上摩擦”。uC/OS-II 用一把创建时就定死优先级的锁解决了问题。本文拆解 OSMutex 的完整源码,讲透优先级继承(PIP)机制的每一行实现,并厘清它与 FreeRTOS 动态继承的本质区别。
先看一场事故:直升机为什么飞不动了
假设你正在写一个飞行控制系统。三个任务:
- H:直升机主控,优先级 2(最高,数值最小)
- M:姿态解算,优先级 5
- L:舵机驱动,优先级 8(最低)
L 任务正在临界区里修改舵机参数,持有互斥量。H 此时到来,想要同一把锁——被阻塞,挂起等待。一切正常:H 最多等 L 把临界区跑完。
坏就坏在 M 出现了。
M 优先级 5,高于 L。它一就绪,立刻抢占 L——因为 L 的优先级只有 8,根本没资格继续跑。L 被挂起,互斥量还被它攥在手里,而 H 在等锁。于是整个系统的执行顺序变成了:M 先跑完 → L 再跑完 → H 最后拿到锁。
H 明明是最高优先级,却被两个低优先级任务活活拖死。
更可怕的是:如果 M 是个周期性任务、持续就绪抢占 CPU,H 可能永远等不到锁。这在实时系统里叫优先级翻转——一个中等优先级任务,把最高优先级任务的执行时间无限推迟。

解法:把持有者“临时提上去”
问题的根源是:L 占着锁,却因为优先级低被 M 抢走 CPU。如果让 L 在持有互斥量期间,优先级暂时高于 M,M 就抢不动了。
这就是优先级继承(Priority Inheritance)的思路:
- L 持有互斥量时,若有一个更高优先级的任务 H 来等锁,L 的优先级被临时提升到某个指定值
- L 以提升后的优先级继续跑完临界区并释放锁
- 释放后,L 恢复原优先级,H 拿到锁
注意这里的关键词:临时提升、事后恢复。它不是把 L 永久降级,也不是把 H 拉低,而是把 L“拔高”。
但 uC/OS-II 的做法有个特殊之处,必须讲清楚:
uC/OS-II 的 PIP 是创建互斥量时固定指定的优先级,不是动态继承到等待者的最高优先级。
这和 FreeRTOS 的动态继承风格完全不同。FreeRTOS 会把持有者提升到“当前所有等待者中最高优先级”;而 uC/OS-II 是创建时定死的:不管谁来等,持有者一律提到那个固定优先级别。
这个设计有好处:实现简单、行为可预测。也有代价:PIP 值选得不合适,可能反而引发其他问题。后面讲创建函数时会细说。
OSMutexCreate:一出生就要占个坑
信号量和互斥量共用 OS_EVENT 数据结构,但互斥量把 OSEventCnt 从信号量的”计数”改造成了复合字段:高 8 位存 PIP,低 8 位存可用标志或持有者优先级——同一个字段,两种语义。
先看互斥量的核心数据结构(ucos_ii.h:464-470):
typedef struct os_mutex_data {
OS_PRIO OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待表(与信号量同构) */
OS_PRIO OSEventGrp;
BOOLEAN OSValue; /* OS_FALSE=已占用 OS_TRUE=可用 */
INT8U OSOwnerPrio; /* 持有者优先级或 0xFF */
INT8U OSMutexPIP; /* PIP 或 0xFF */
} OS_MUTEX_DATA;
创建函数的签名很特殊(os_mutex.c:170-227):
OS_EVENT *OSMutexCreate(INT8U prio, INT8U *perr)
第一个参数就是 PIP——优先级继承目标值。创建互斥量时,你必须指定“当翻转发生时,持有者该被提到哪个优先级”。
创建时的校验逻辑(逐字):
- prio >= OS_LOWEST_PRIO → 报 OS_ERR_PRIO_INVALID(超出有效优先级)
- 在中断里调用 → 报 OS_ERR_CREATE_ISR(创建不能在 ISR 中做)
- OSTCBPrioTbl[prio] 非空 → 报 OS_ERR_PRIO_EXIST(PIP 位置不能已有任务)
最核心的一步是占坑:
OSTCBPrioTbl[prio] = OS_TCB_RESERVED;
这个优先级从此不能再创建任何任务。因为 PIP 是预留的,运行期间持有者会被提到这儿来。如果这里已经跑着别的任务,一提升就撞车。
然后是这个关键编码:
pevent->OSEventCnt = (prio << 8) | OS_MUTEX_AVAILABLE;
OSEventCnt 高 8 位存 PIP 值,低 8 位存互斥量状态:OS_MUTEX_AVAILABLE 表示可用;不可用时,低字节存的是持有者的原优先级。
一个 OS_EVENT,高 8 位写死了 PIP,低 8 位动态记录持有者。这就是互斥量与信号量在 OSEventCnt 使用上的根本差异。
OSMutexPend:继承的本质是“挪位子”
获取互斥量的核心逻辑在 os_mutex.c:414-546,逐字拆解:
pip = (INT8U)(pevent->OSEventCnt >> 8u); /* 取出 PIP */
if (低字节 == OS_MUTEX_AVAILABLE) { /* 互斥量可用 */
OSEventCnt &= KEEP_UPPER_8; /* 清低字节 */
OSEventCnt |= OSTCBCur->OSTCBPrio; /* 存持有者优先级 */
OSEventPtr = OSTCBCur; /* 指向持有者 TCB */
if (OSTCBCur->OSTCBPrio <= pip) return OS_ERR_PIP_LOWER;
return OS_ERR_NONE;
}
互斥量可用时,任务直接拿走锁,低字节记录自己的优先级。但注意那个检查:如果持有者的优先级优于或等于 PIP,直接报 OS_ERR_PIP_LOWER。意思是:你自己优先级比 PIP 还高,这把锁的 PIP 对你没意义——说明 PIP 配置不合理。
互斥量被占时,进入优先级继承段:
mprio = 低字节; /* 持有者原优先级 */
ptcb = OSEventPtr; /* 持有者 TCB */
if (ptcb->OSTCBPrio > pip) { /* 持有者优先级比 PIP 低? */
if (mprio > OSTCBCur->OSTCBPrio) {
/* 把持有者从就绪表(或等待表)移除 */
OSRdyTbl[y] &= ~ptcb->OSTCBBitX;
if (OSRdyTbl[y] == 0u) OSRdyGrp &= ~ptcb->OSTCBBitY;
/* 提升:OSTCBPrio = pip,重新算位图索引 */
ptcb->OSTCBPrio = pip;
ptcb->OSTCBY = pip >> 3;
ptcb->OSTCBX = pip & 0x07;
ptcb->OSTCBBitY = 1 << Y;
ptcb->OSTCBBitX = 1 << X;
/* 按新优先级放回就绪表 */
OSRdyGrp |= ptcb->OSTCBBitY;
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
OSTCBPrioTbl[pip] = ptcb; /* 登记新优先级槽位 */
}
}
这段代码是优先级继承的魂。
两个继承条件缺一不可:
继承动作的本质是挪位子——第 3 篇讲的位图索引在这里被动态改写:
任务还是那个任务,TCB 还是那个 TCB,只是它在位图里的“格子”换了位置。调度器看到的值变了,就会按提升后的优先级调度它。
继承完成后,才执行标准的挂起流程(阻塞当前等待者)。这就是 OSMutexPend 的全貌:先处理继承,再执行挂起。
这里有一个必须知道的坑:uC/OS-II 的互斥量不可重入。如果持有者自己再次 Pend 同一把锁(比如不小心在临界区里调用了递归函数),低字节存的是自己的优先级、不等于 AVAILABLE——走继承段时 mprio > 自己的优先级 为假,不会触发继承,然后直接挂起自己等待一把自己持有的锁——死锁。所以使用互斥量要保证临界区里不能再次获取同一把锁。

OSMutexPost:还回优先级,亲手交锁
释放互斥量的逻辑在 os_mutex.c:572-624,第一行就与众不同:
if (OSIntNesting > 0u) return OS_ERR_POST_ISR;
互斥量不能从 ISR 里释放!信号量可以,互斥量不行。原因很本质:互斥量有所有权概念——谁拿的锁,谁才能还。中断是异步的,不能替别人“代持”或“代还”。这是互斥量和信号量在 API 层面的最大差异。
接下来是所有权检查:
prio = OSEventCnt 低字节; /* 持有者原优先级 */
if (OSTCBCur != OSEventPtr) return OS_ERR_NOT_MUTEX_OWNER;
必须持有者自己来 Post,否则报错。这是互斥量的“身份证明”。
如果持有者的优先级被提升过(OSTCBCur->OSTCBPrio == pip),要恢复原优先级:
if (OSTCBCur->OSTCBPrio == pip) {
OSMutex_RdyAtPrio(OSTCBCur, prio);
}
OSMutex_RdyAtPrio 是 os_mutex.c 的内部函数,逻辑和 Pend 里的继承完全对称:从新优先级位图挪回原优先级位图。提升和恢复,本质都是挪位子。
然后归还 PIP 槽位:
OSTCBPrioTbl[pip] = OS_TCB_RESERVED;
PIP 那个优先级重新变成“预留坑”,等待下一次提升。
最后处理等待者:
if (OSEventGrp != 0u) { /* 有等待者:直接转交 */
prio = OS_EventTaskRdy(pevent, 0, OS_STAT_MUTEX, OS_STAT_PEND_OK);
OSEventCnt = (pip << 8) | prio; /* 记录新持有者 */
OSEventPtr = OSTCBPrioTbl[prio];
if (prio <= pip) OS_ERR_PIP_LOWER;
OS_Sched();
return OS_ERR_NONE;
}
OSEventCnt |= OS_MUTEX_AVAILABLE; /* 没人等:释放 */
OSEventPtr = (void *)0;
有等待者时,互斥量直接转交给最高优先级等待者,不经过”可用”状态。新持有者的优先级立刻被写进低字节。没人等待时,才真正释放。
补充两个配套 API:OSMutexAccept 是非阻塞获取——拿不到锁立即返回,不挂起(适合”试着拿一下”的场景);OSMutexQuery 把 OS_MUTEX_DATA 里的 OSValue、OSOwnerPrio、OSMutexPIP 填出来供诊断——调试”锁被谁持有”时很有用。
终局推演:PIP 之后,世界安静了
现在做一次完整的推演,修正之前的场景。设三任务:
- H:优先级 10
- M:优先级 5
- L:优先级 8
- PIP 设为 4
注意这里的配置逻辑:PIP 必须比所有竞争者的优先级都高(数值更小)。竞争者里最高的是 M(5),所以 PIP 必须小于 5,比如 4。这样持有者一旦被提升,就能压住包括 M 在内的所有竞争者。
推演过程:
对比没有 PIP 的场景:H 要等 M 跑完再等 L 跑完。有了 PIP:H 最多等 L 的临界区 + 一次 PIP 竞争窗口。
翻转没有消失,但它被限制在了可控范围内。这就是优先级继承的意义:把不确定性变成确定性。
PIP 配置的陷阱也在这:如果 PIP 选得太低(数值太大),比如选了 6——L 被提升到 6,可 M 是 5,M 依然能抢占持有者,翻转只缓解了一半。所以 PIP 的选择必须压过所有可能竞争这把锁的任务,而不是只压过当前等锁的那个。注释里反复强调:PIP 必须比所有竞争者的数值更小,这在创建时就要设计好。
一句话总结
互斥量与信号量的本质区别,不在于“等待表同构”,而在于所有权和优先级继承:
- 所有权 → 只能持有者 Post,ISR 不能碰
- 优先级继承 → Pend 时挪位子提升持有者,Post 时挪位子恢复
互斥量的临界区本身也要尽量短——持有时间越长,其他任务被压住的时间越长。这两个机制合在一起,就是把”低优先级占锁导致高优先级被拖死”的失控场景,压缩成一个有界的等待窗口。
什么时候用互斥量,什么时候用信号量?一句话:互斥量是”锁”,信号量是”信号”。保护共享资源(变量、外设、缓冲区)用互斥量——要所有权、要防翻转;资源计数与任务同步用信号量。把信号量当锁用是新手最常见的错误:它没有继承保护,也没有所有权检查,任何任务都能 Post,临界区形同虚设。
第 8 篇预告:邮箱与消息队列。OS_EVENT 的指针语义,数据传递的本质,以及与信号量、互斥量的底层关系。到时见。
你写实时系统时,遇到过最隐蔽的优先级翻转 bug 是什么?欢迎在评论区聊聊。觉得有用就点个关注,后面 3 篇继续更新。





