FEATURED · 精选文章

手写RTOS互斥锁:从零实现并解决优先级反转

发布时间 / 2026/9/8 8:26:06
来源 / 创域科博编辑部
栏目 / 资讯中心
手写RTOS互斥锁:从零实现并解决优先级反转 1. 从点灯到抢资源为什么跑通调度器之后你迟早得面对一把锁前六篇我们把任务控制块、就绪队列、Systick节拍、上下文切换、信号量一路搓了过来点灯点的风生水起。直到某天你给项目里加了两个任务一个任务读传感器然后更新共享缓冲区另一个任务负责把缓冲区内容通过串口发出去。运行一段时间后你发现串口发出的数据偶尔会出现半个帧——前一半是新数据后一半是旧数据。更诡异的是用调试器暂停一看一个低优先级任务死死占着CPU而高优先级任务在就绪表里躺着等仿佛整个系统被人按了暂停键。恭喜你正式撞上了RTOS世界里两个最经典的社会问题资源竞争和优先级反转。前者让你意识到共享变量不是你想写想写就能写后者让你意识到优先级高不代表你能跑。而这两件事的常规解法就是互斥锁Mutex以及在此基础上必须配套的优先级继承Priority Inheritance机制。这篇我们就干一件事在自己的小操作系统里从零手搓一把正经的互斥锁再把优先级反转这个坑填平。代码风格延续之前的系列直接用C语言写能在QEMU或者你手头的STM32裸板上跑。先说清楚范围。我们不聊POSIX的pthread_mutex不聊Linux内核的futex也不聊C的std::mutex——那些都是用锁的层面。今天聊的是造锁的层面锁的本质是什么、阻塞队列怎么挂、释放锁时怎么挑下一个任务、优先级继承把自己优先级借出去之后怎么还。这些搞明白了你再看FreeRTOS的互斥锁源码会发现它就是个加了优先级继承机制的信号量特化版。另外要提前打预防针如果你只是想用RTOS那直接调OSAL层接口就行本篇属于想知道锁底下到底怎么转系列。如果你正在自己写RTOS玩或者面试被问到互斥锁和二值信号量的区别优先级反转怎么解决那这篇就是照着面试答案反向拆解的活教材。2. 临界区的进化史关中断、忙等待、还是睡一觉再回来2.1 最粗暴的方案关中断谁也不许动很多人写裸机程序时保护共享变量就一招——关中断。写关键区之前__disable_irq()写完之后__enable_irq()。在单核MCU上这一招确实管用因为它把可能抢走CPU的人直接拒之门外。但放到RTOS里这个方案很快会露馅。关中断的代价不是功能性的而是实时性的。你把中断关了意味着定时器、串口接收、ADC转换完成这些事全都被挂起。轻则丢数据重则错过紧急事件。有人会说我就关几个微秒但你没有想过关中断期间万一来了NMI不可屏蔽中断或者更高异常优先级的事件你的临界区会被硬生生打断那锁就白加了。这里面还藏着一个更隐蔽的问题在SMP多核场景下关中断只能关掉当前核的管不了别的核。所以关中断本质上是一个单核专属、实时性代价极高的临界区保护方式只适合保护几条指令级的原子操作比如就绪表位图操作、链表头插这种微操。一旦临界区里要干的事稍多——比如拷一个几十字节的缓冲区——关中断就不合适了。2.2 忙等待短临界区还行长临界区直接翻车既然不能关中断太久有人就想我用一个全局标志位想进临界区就while(flag); flag 1;忙等行不行在单核上这个写法连正确都做不到——如果访问flag不是原子的两个任务可能同时读到0然后同时进临界区。就算你在ARM上用LDREX/STREX把flag的读取和置位做成原子的获得了自旋锁它仍然有一个致命伤忙等本身就是在浪费CPU。低优先级任务抱着自旋锁跑的时候它不主动让出CPU高优先级任务在另一个循环里一直转圈等锁。单核上如果调度是抢占式的高优先级任务压根轮不到运行低优先级任务又坚定地认为我的临界区很短马上就好两个任务死磕系统卡死。这个场景就是标题里说的——低优先级任务卡死高优先级任务。为什么锁的等待不能是忙等因为RTOS的基本盘是让出CPU。既然我们手头已经有了一套任务调度器正确的做法是拿不到锁的任务把自己挂起来把CPU让给别人等锁被释放了再被唤醒。这才是一个RTOS该有的修养。2.3 谁来管锁调度器才是更好的协调者于是结论变得很清晰互斥锁的底层本质就是暂停当前任务 加入等待队列 触发一次调度解锁的底层本质就是从等待队列里挑一个任务唤醒 把它放入就绪队列 触发一次调度。想到这一层了吗这和我们之前实现的信号量几乎一模一样。事实上如果用二值信号量直接当一个互斥锁用功能上是能跑通的。但你会在实践中遇到两个非常头疼的问题优先级反转低优先级任务持锁高优先级任务等锁中优先级任务把CPU抢走低优先级任务永远没机会释放锁高优先级任务活活饿死。这是二值信号量无法解决的。递归加锁同一个任务在持锁期间再次对同一把锁加锁如果是普通信号量第二次加锁就把自己阻塞了——你把自己锁死了。这两个问题就是互斥锁区别于信号量的精髓所在。前者靠优先级继承后者靠持有者重入计数。本文会逐个击破。3. 优先级反转推演三个任务怎么把系统玩死的3.1 从一把锁和三个优先级开始先构造一个最简单的复现场景。三个任务假设优先级数值越低代表优先级越高和uC/OS、FreeRTOS的通用习惯保持一致任务A高优先级优先级1负责处理紧急事件需要访问共享资源。任务B中优先级优先级2普通计算任务不访问共享资源但是吃CPU。任务C低优先级优先级3持有一把锁后要跑很大一段临界区然后释放。调度规则抢占式调度高优先级任务就绪立即抢占低优先级任务时间片轮转只对同优先级任务生效。3.2 反转发生的完整时间线一步步推演注意看时间顺序时间点事件CPU运行等待锁的任务T0任务C运行成功获取锁进入临界区C无T1任务A就绪比如外部中断置位事件标志抢占CA无C还占着锁呢T2任务A尝试获取同一把锁失败阻塞CA被挂起AT3任务B就绪比如定时器触发抢占CBAT4任务B持续运行不释放CPUBAT5……任务C永远没机会运行锁永远无法释放A永远等锁BA看到问题了吗任务A明明拥有最高优先级但实际上它的命运被任务C捏在手里。而任务B本来跟共享资源毫无关系却因为跑得欢而间接卡死了A。这就是教科书式的优先级反转Priority Inversion。注意真正的优先级反转有严格定义高优先级任务因为等待一个低优先级任务释放资源导致执行被延迟且延迟时间不可预测甚至可能被无关的中优先级任务无限延长。如果是低优先级任务正在临界区而高优先级任务必须等它出来这是正常的、可预测的等待不叫反转。反转最毒的地方在T3之后无关任务B的出现让等待时间从看C脸色变成了看所有中优先级任务脸色。3.3 为什么二值信号量救不了这个场如果这把锁是用二值信号量实现的信号量被C拿走A来取时信号量计数为0于是A阻塞。B来了抢占C。信号量机制里没有任何信息能告诉调度器现在有一个高优先级任务正等着某个低优先级任务的信号量于是调度器按照普通规则让B继续跑。在B不主动让出CPU或者B的任务更长的情况下C永远排在B后面A永远排在C后面。你可能想那就让C在临界区里把优先级提到A之上不就行了没错这正是优先级继承的思路。但这件事必须由锁的机制自动完成不能依赖任务编写者的自觉。因为人类在写任务代码时根本预料不到我的任务会被什么优先级的任务等锁锁是动态交互的必须由内核去感知和反馈。4. 手搓互斥锁数据结构、阻塞队列与重入计数4.1 设计目标互斥锁必须知道的几件事动手写代码前先把设计目标立清楚。一把正经的RTOS互斥锁需要维护这些状态锁是否被持有一个布尔值就够了。锁空闲时值为0被持有时值为1。当前持有者是谁必须记住持有者的任务ID因为后面优先级继承要以持有者为作用对象。持有者重入计数同一个任务可以重复获取同一把锁每获取一次计数加1对应的释放一次减1。只有计数归零才算真正释放锁。这就是递归互斥锁的核心。等待者队列没拿到锁的任务按优先级顺序排队而不是FIFO。这样才能保证一旦锁释放等锁任务里优先级最高的那个先被唤醒。这样设计的一个额外好处是等待队列按优先级排列后我们会发现二值信号量的唤醒谁根本不用纠结直接取队头就行。4.2 数据结构定义延续系列前几篇的代码风格我们用一个结构体把锁对象定义出来然后把链表、就绪队列、任务控制块这些基础设施直接复用。假设你已经有了os_tcb_t结构体至少包含task_id、priority、state和用于链表串联的next指针。/* os_mutex.h */ #ifndef OS_MUTEX_H #define OS_MUTEX_H #include os_types.h #include os_task.h typedef struct os_mutex { uint8_t locked; /* 1已被持有0空闲 */ uint8_t owner_id; /* 当前持有者ID */ uint16_t nest_count; /* 重入计数 */ os_tcb_t *wait_list; /* 等待队列按优先级排序 */ } os_mutex_t; void os_mutex_init(os_mutex_t *mutex); void os_mutex_lock(os_mutex_t *mutex); void os_mutex_unlock(os_mutex_t *mutex); #endif这里没有用复杂的内核对象头直接把字段平铺开便于你跟踪状态。wait_list是一个按优先级排序的单链表节点就是任务TCB中的成员需要在os_tcb_t里预留一个mutex_wait_next指针。如果你之前的移植里已经有多级就绪表相关的排序逻辑队列插入排序可以照搬思路。4.3 初始化干干净净别留垃圾初始化这段代码看似废话但很多早期手搓系统都会在忘记初始化上翻车。尤其是wait_list如果不用NULL清掉后面insert_to_wait_list会遍历到一个野指针然后硬故障。void os_mutex_init(os_mutex_t *mutex) { mutex-locked 0; mutex-owner_id 0; /* 0 号任务通常是空闲任务永不该持锁 */ mutex-nest_count 0; mutex-wait_list NULL; }4.4 加锁先查持有者再决定睡不睡加锁的逻辑拆成四步如果锁空闲直接抢下记录持有者置locked1nest_count1。如果锁被当前任务持有重入nest_count。如果锁被其他任务持有把当前任务从就绪队列摘除按优先级插入该锁的wait_list触发调度。当前任务被唤醒后说明锁已经到手补上持有者信息。写代码时有几个细节值得注意。第一关中断必须极其克制因为锁函数本身就讲究短小精悍关中断包住的操作必须只有判断 改状态 摘就绪 插等待这几步涉及阻塞和调度的部分放到关中断之后。第二把当前任务从就绪队列摘除后要立刻改任务状态为BLOCKED否则调度器可能又把它选上出现一个任务在两个队列里的灵异事件。void os_mutex_lock(os_mutex_t *mutex) { os_tcb_t *current os_get_current_tcb(); uint32_t sr; sr os_enter_critical(); if (mutex-locked 0) { /* 锁空闲直接获取 */ mutex-locked 1; mutex-owner_id current-task_id; mutex-nest_count 1; os_exit_critical(sr); return; } if (mutex-owner_id current-task_id) { /* 重入同一任务再次加锁 */ mutex-nest_count; os_exit_critical(sr); return; } /* 锁被其他任务持有当前任务必须阻塞 */ current-state TASK_STATE_BLOCKED; os_remove_from_ready(current); insert_to_wait_list(mutex, current); /* 按优先级排序插入 */ os_exit_critical(sr); /* 让出CPU切换至最高优先级就绪任务 */ os_schedule(); /* 被唤醒回来后锁一定属于当前任务 */ mutex-owner_id current-task_id; mutex-nest_count 1; }insert_to_wait_list是按优先级从高到低排的插入逻辑从头遍历找到第一个优先级低于自己的节点插到它前面。这样队头永远是等锁任务里优先级最高的。4.5 解锁重入计数归零才是真释放解锁逻辑也有讲究不是直接locked0就完事。必须先把重入计数减掉如果减完不为零说明任务只是退了一层嵌套锁还需要继续持有。只有计数值归零才把锁状态清成空闲然后从等锁队列里挑出队头任务唤醒并加入就绪队列。void os_mutex_unlock(os_mutex_t *mutex) { os_tcb_t *current os_get_current_tcb(); os_tcb_t *next; uint32_t sr; sr os_enter_critical(); /* 严谨性检查只能由持有者释放 */ if (mutex-owner_id ! current-task_id) { os_exit_critical(sr); return; } if (mutex-nest_count 1) { mutex-nest_count--; os_exit_critical(sr); return; } /* 最后一次释放 */ next remove_head_from_wait_list(mutex); if (next) { /* 直接把锁移交给下一个等待者 */ mutex-owner_id next-task_id; mutex-nest_count 1; next-state TASK_STATE_READY; os_add_to_ready(next); } else { /* 没有等待者锁彻底空闲 */ mutex-locked 0; mutex-owner_id 0; mutex-nest_count 0; } os_exit_critical(sr); /* 无论是否移交成功都重新调度一次 */ os_schedule(); }注意这段代码里锁直接移交给下一个等待者的处理我们没有把锁先置空闲然后再让被唤醒的任务去抢而是直接过户。这么做的目的是避免二次竞争和额外调度。如果先置空闲、再唤醒等待者等待者从os_mutex_lock后半段醒来后还要再做一次锁状态判断中间如果被其他任务穿插就会多一次无谓的调度。直接过户省事且确定性强。4.6 重入计数是怎么来的以及为什么它不能省重入计数看起来只是多了一个nest_count但它解决的是一个真实存在的工程问题假设任务A持锁后调用了一个函数这个函数内部也需要对同一把锁做保护。如果锁不具备重入能力函数里的第二次加锁会把任务A自己阻塞住——持有锁的人等锁死锁。更阴险的是嵌套路径不可控。你今天写的临界区代码只调用三个函数明天别人加了第四个又在内部加了一次锁整条链路上锁的需求是不可预期的。互斥锁既然要做成通用内核对象就必须一开始就支持同一持有者重入否则将来你们团队里每个人都会踩一遍这个坑。5. 优先级继承真正治好卡死的关键机制5.1 思路把优先级临时借给锁的持有者要打破3.2的死亡时间线核心是避免无关任务B抢占锁持有者C。优先级继承的做法很朴素当一个高优先级任务A开始等待一把锁时如果锁当前被低优先级任务C持有内核把C的优先级临时提升到与A相同。这样一来C就有资格去抢占B了。C赶紧跑完临界区释放锁然后把优先级降回原来的值。A随后被唤醒真正的高优先级恢复统治。首先继承这个词很形象——A把优先级借给了C用用完要还。其次要注意继承不是永久的锁释放后必须立刻恢复原优先级否则低优先级任务借了高优先级之后赖着不走整个系统的优先级体系就崩了。5.2 实现方案继承优先级放在哪个时机触发理论上触发优先级继承的时机有两个任务A加锁失败、正要进入阻塞队列的那一刻。这是最直接的时机,因为此刻我们就知道了持锁任务C会因为A的阻塞而延迟释放锁这一事实。任务D解锁唤醒任务A之后、A开始运行时。这个时机太晚了,继承的意义在于提前把C提上来跑,而不是等A等了一万年才去补救。所以实现锁定在时机1os_mutex_lock中当前任务即将阻塞之前检查持有者优先级是否低于当前任务如果是就临时提升持有者优先级。有一个细节必须处理锁的持有者可能同时持有好几把锁而A在等锁1锁1的持有者C可能还在等锁2。如果只提升C不继续往上追溯仍然可能死锁。要彻底解决需要沿着持有者等待的锁链递归提升。不过作为一个迷你RTOS的学习实现我们先处理单层继承把链式继承放在进阶扩展里讲。5.3 需要新增的字段原始优先级与等待锁指针为了支持优先级继承任务TCB里至少要新增两个字段/* os_task.h 中 os_tcb_t 新增字段 */ uint8_t base_priority; /* 任务原始优先级调度和比较时使用 */ os_mutex_t *wait_mutex; /* 当前正在等待的锁用于链式继承 */这里有个容易想歪的点到底是用base_priority参与调度还是用priority参与调度我的实现是base_priority任务创建时设定的原始优先级稳定不变。priority动态优先级可能被临时提升调度器比较、就绪表插入都用priority。任务被提升后priority变大数值变大优先级变低恢复时priority base_priority。这样设计的好处是内核在任何一个位置都能区分你本来是谁和你现在是谁。等锁释放后把priority还原成base_priority即可不会错乱。5.4 加锁时机注入继承逻辑在os_mutex_lock的锁被其他任务持有分支里插入一段继承判断/* 找出当前锁的持有者 */ os_tcb_t *owner find_tcb_by_id(mutex-owner_id); /* 如果持有者的动态优先级比当前任务低就提升它 */ if (owner-priority current-priority) { /* 数值越小优先级越高 */ owner-priority current-priority; }但这里有一个很大的坑持有者当前可能正在就绪队列里比如它被B抢占后就绪了也可能正在阻塞等待另一把锁。如果它正在就绪队列中直接改它的优先级字段会破坏就绪队列的优先级排序一致性。正确做法是先把持有者从就绪队列摘出来修改优先级再按新优先级重新插入就绪队列。if (owner-priority current-priority) { uint8_t new_prio current-priority; /* 如果持有者正在就绪队列先摘再改再插 */ if (owner-state TASK_STATE_READY) { os_remove_from_ready(owner); owner-priority new_prio; os_add_to_ready(owner); } else { owner-priority new_prio; } }这里还联动了一个问题current即将被阻塞并从就绪队列移除owner被提升后可能直接变成就绪队列里最高优先级任务。等os_schedule()执行时调度器自然会选到ownerC 就突破 B 的封锁跑起来了。5.5 解锁时机归还优先级有借有还再借不难。在os_mutex_unlock中真正释放锁的时刻持有者要把动态优先级恢复为原始优先级。但我们不能无脑恢复——嵌套锁的情况下外层锁可能还需要继承来的优先级。举个例子C 持有锁1A等待锁1C被提升。C在临界区内又去获取锁2此时B等待锁2。如果C释放锁1时直接把优先级降回去那B等着锁2又会被C的原始低优先级耽误。所以解锁时的优先级恢复必须检查持有者是否还持有其他锁并且这些锁是否有新的等待者。在迷你实现里我们退一步解锁时先看nest_count是否归零归零后遍历该任务持有的其他锁用一个任务持有锁链表如果其他锁的等待队列非空且队头优先级更高就继续保持提升状态否则恢复为base_priority。这个任务持有锁链表实现起来要动一些数据结构我先给出简化版的解锁恢复逻辑假设每个任务同一时间最多持有一把需要继承的锁绝大多数的学习场景都够用/* 在 os_mutex_unlock 的最后一次释放分支里 */ next remove_head_from_wait_list(mutex); /* 恢复持有者优先级 */ current-priority current-base_priority; if (next) { mutex-owner_id next-task_id; mutex-nest_count 1; next-state TASK_STATE_READY; os_add_to_ready(next); } else { mutex-locked 0; mutex-owner_id 0; mutex-nest_count 0; }说句实话这个简化版本在多把锁嵌套 多个等待者的边缘情况下是有瑕疵的。比如C持有锁1和锁2A等锁1B等锁2。C释放锁1时直接把优先级降回基础值但锁2的等待者B优先级更高C应该继续保持高优先级去跑完锁2的临界区。为了不把复杂度推到不合理的程度我建议把这个边缘情况放到进阶功能里用任务持有锁链表解决。在动手实现之前单继承版本已经能覆盖90%以上的教学演示和中等复杂度项目。5.6 优先级继承 vs 优先级天花板两条路线选哪个提优先级继承时很多人还会想到另一个方案优先级天花板Priority Ceiling。这个方案更简单粗暴——给每把锁预设一个天花板优先级这个优先级高于所有可能使用这把锁的任务。任何任务获取这把锁时优先级直接提到天花板级别不管有没有人被阻塞。两种方案对比对比项优先级继承优先级天花板原理动态有阻塞才提升静态获取即提升实现复杂度较高需要维护动态优先级较低锁初始化时配一个数任务阻塞时长更短非必要不提频较长每次都提到最高死锁风险低配合链式继承可避免天然免疫适用场景通用OS航天、汽车等确定性要求极高的场景优先级天花板来自实时计算领域的经典理论它有个特别大的优点是可以预防死锁——因为你知道了所有锁的天花板之后可以设计加锁顺序。代价是任何任务拿锁瞬间都要跳频到很高即使根本没有高优先级任务在等性能有浪费。优先级继承则更抠门只在确实出现反转风险时调整实现复杂但系统整体更流畅。对于手搓RTOS我建议先实现优先级继承因为它的动态特性会让你更深刻地理解调度器是活的东西而不是死板的优先级表。6. 实测验证三个任务一台戏日志里看真相6.1 实验设计代码写完了不能光靠脑子说应该好了。我们需要一个可复现的测试场景用串口日志证明以下三点没有互斥锁或只用二值信号量时低优先级任务确实会卡死高优先级任务。打开优先级继承后高优先级任务等锁时间显著缩短。重入计数确实让嵌套加锁不会死锁。设计三个测试任务优先级数值越小优先级越高任务A优先级1尝试获取锁拿到后点亮LED并打印[A] lock ok随后立刻释放。任务B优先级2一个死循环计数器打印[B] running不碰锁。任务C优先级3获取锁后在临界区里打印[C] in criticaldelay 一小段时间模拟长时间临界区然后释放锁打印[C] out critical。为了让反转现象稳定复现需要让 C 先拿到锁然后 A 再抢锁。所以设计中给 C 一个初始延时比如延时500ms确保 C 第一个进入临界区A 用外部触发或定时器延时 600ms 后开始抢锁B 在 550ms 时启动抢占 C。6.2 日志分析改进前后对比关闭优先级继承时典型日志长这样[C] in critical [B] running [B] running ... [B] running 刷屏持续很久 [A] lock ok A才拿到锁C在角落默默释放A被卡住的时间长达B任务运行的总时长这个时长完全不可控取决于B的代码逻辑。这在实际产品里就是偶发性系统反应迟钝的根源。打开优先级继承后典型日志变成[C] in critical [B] running [C] out critical C被临时提升到优先级1抢回了CPU快速出临界区 [A] lock ok A立刻拿到锁 [A] out critical [B] running B重新回来刷屏锁的等待时间从中优先级任务想跑多久就跑多久变成锁持有者执行完临界区的时间后者是确定且可预测的。这正是实时系统需要的特性。6.3 重入场景测试测试重入可以设计任务A执行如下伪代码void task_a_entry(void *arg) { os_mutex_lock(lock); os_mutex_lock(lock); /* 第二次加锁 */ os_mutex_lock(lock); /* 第三次加锁 */ /* 临界区操作 */ os_mutex_unlock(lock); os_mutex_unlock(lock); /* 注意此处故意只释放两次 */ os_mutex_lock(lock); /* 继续用说明锁还没释放 */ os_mutex_unlock(lock); os_mutex_unlock(lock); /* 最后一次 */ }如果在没有重入计数的信号量实现上跑这段代码第二次os_mutex_lock就会把 A 自己阻塞然后A永远不会被唤醒——死锁。而有了nest_count每次加锁加1释放减1只有归零才算真正释放。日志上应该看到A完整执行完所有加解锁操作任务状态始终是RUNNING没有跳转到BLOCKED。7. 边界情况与踩坑清单手写互斥锁最容易翻车的地方7.1 在中断里调用os_mutex_lock这是头号禁忌我在自己早期项目里就干过这种事串口接收中断里解析到一帧完整数据想直接把数据拷进共享缓冲区顺手调了os_mutex_lock。结果系统偶发性卡死查了两天才定位到问题。锁的设计天然假设拿不到锁就阻塞而阻塞需要切换上下文这在中断上下文里是非法操作——你把当前任务挂起但实际上中断返回后应该恢复原来的任务上下文就完全错乱了。任何RTOS的互斥锁接口都不允许在中断里调用。中断里保护共享数据正确方案是临界区关中断或无锁环形缓冲。7.2 持有锁的任务被删除锁变成僵尸锁任务A持有锁后被别的任务调用os_task_delete直接删除。锁的owner_id还指向A但A已经不存在了。之后所有任务来拿这把锁都会看到锁被持有且持有着永远不会释放于是整把锁被锁死。正规RTOS的处理方式是删除任务时内核自动释放它持有的所有锁并且唤醒对应等待队列。这要求内核里维护一个任务持有的锁列表。我们的迷你RTOS如果要加这个能力需要在TCB里增加一个锁链表头在加锁成功和解锁时维护这个链表。我把它列为进阶扩展但实际产品里这不是可选项是必选项。7.3 优先级继承只做了一半忘了链式继承前面提到wait_mutex字段就是为链式继承准备的。场景是A优先级1等锁1C优先级3持有锁1同时C在等锁2锁2被D优先级4持有。此时正确做法是A的优先级不仅要传给C还要通过C正在等待的锁2把优先级继续传给D。这才能保证D尽快释放锁2让C拿到锁2后继续跑完临界区释放锁1最终A被唤醒。省略链式继承前面的单层继承在锁嵌套场景下仍然会反转。实现链式继承的递归思路void os_boost_priority_chain(os_tcb_t *task) { os_tcb_t *blocked_on; while (task-state TASK_STATE_BLOCKED task-wait_mutex ! NULL task-wait_mutex-owner_id ! 0) { blocked_on find_tcb_by_id(task-wait_mutex-owner_id); if (blocked_on-priority task-priority) { blocked_on-priority task-priority; task blocked_on; } else { break; } } }这个递归在每组等待-持有关系上都要遍历但锁路径一般很短不会成为性能瓶颈。7.4 优先级提升后没有让它立刻跑起来调度时机错了优先级继承只是改了持有者的优先级数值如果修改后没有触发一次调度已经降低优先级的持有者依然留在就绪队列里排队继承就白做了。所以记住完成优先级提升动作后要主动调用os_schedule()或者至少os_sched_yield()让调度器重新评估当前就绪任务。这一步其实在原始os_mutex_lock的os_schedule()里会自然触发——因为当前任务反正要阻塞调度器必然要选一个新任务。但如果你是手动去调整其他任务优先级比如调试工具或别的内核模块就很容易忽略这个调度触发点。7.5 多核场景关中断不再是万能保护本系列目前基于单核MCU关中断保护的临界区是安全的。但如果你把代码移植到双核或者AMP非对称多核、SMP对称多核场景__disable_irq()只能保护当前核其他核仍然能访问共享数据结构。届时锁的内核内部数据结构自身也需要一把真正的内核自旋锁来保护。我做出这个提示是因为不少人在QEMU上跑通了单核自信心膨胀后直接把代码搬到多核板子结果疯狂踩坑。8. 从互斥锁到真正的RTOS下一步该往哪走互斥锁写到这里你手头的迷你RTOS已经具备了资源互斥 阻塞唤醒 优先级动态调整这几个核心能力。但先别急着把这套代码焊死在项目上还有几个问题值得你继续往下深挖。第一锁和信号量的关系值得再梳理一遍。从实现角度互斥锁就是二值信号量加了三个机制持有者记录、重入计数、优先级继承。你完全可以把锁看成有主人的信号量——它知道自己被谁拿着也知道谁在等自己。而信号量是无主之物谁都能给谁都能取适合生产-消费这种通知场景。选错这个后面写驱动的时候你会很痛苦。第二等待队列可以抽象成通用的内核等待队列。我们现在wait_list是放在互斥锁结构体里的。如果后面你要实现事件标志组、消息队列、读写锁你会反复复制这套按优先级插入 唤醒队头的逻辑。抽出os_wait_queue_t结构把这套逻辑做成公共设施代码复用率立马上来。第三优先级继承还可以往优先级天花板 立即天花板协议方向扩展。我们用的继承属于动态优先级协议中的基础款RTOS领域还有更高级的优先级天花板协议和立即天花板协议它们在死锁预防上有更强保证。从互斥锁这扇门出去你就能走进实时调度理论的更深水域了——那边不再是点灯而是航空电子、机器人控制这些真正硬实时的战场。我个人的建议是先把单层继承的版本跑熟多构造几个反转场景用日志验证理解透这个问题的时间线然后再动手加链式继承和任务持锁链表。一次吃太多代码写出来你都不敢保证它是对的。手搓OS的乐趣恰恰在这里——每一个机制都要自己设计实验去证明它真的有用而不是好像看起来能用。下一篇如果往下写可能会聊消息队列怎么和互斥锁配合或者怎么在任务删除时自动释放持有锁。你更想先看哪个可以在评论区聊。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻