ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

uC/OS-II信号量源码拆解:事件控制块、等待队列与优先级继承

uC/OS-II信号量源码拆解:事件控制块、等待队列与优先级继承 把 uC/OS-II 内核里的OS_SEM.C单独拎出来数一遍你会发现它只有两百行出头对外函数就五个OSSemCreate、OSSemPend、OSSemPost、OSSemAccept、OSSemQuery。但真正带着问题去读它的人都会撞上同一堵墙——比如你想搞清楚两个不同优先级的任务同时 Pend 一个计数为 0 的信号量Post 一来谁先被唤醒翻遍OS_SEM.C你会发现一行排队逻辑都没有。担当排队、唤醒、超时摘除这些脏活的函数全部躺在OS_CORE.C里名字还都带着Event前缀。这就是第七篇到第九篇一直在铺的东西uC/OS-II 把等一个事件这件事抽象成了一个公共底座信号量、消息邮箱、消息队列、互斥量四种东西全都长在它上面。这一篇就顺着OS_SEM.C往上钻把事件控制块ECB这条链路彻底拆开——从OS_EVENT结构体的字段排布到OS_EventTaskWait、OS_EventTaskRdy、OS_EventTO三个函数的对称设计再到互斥量为了对抗优先级反转额外做的那段优先级继承代码。如果你正在 GD32F103 这类 Cortex-M3 上跑 uC/OS-II或者准备把 RTOS 信号量相关的问题拎到面试桌上去聊这篇里拆出来的东西基本都能直接对上。内容会偏源码视角但不会停在这行长这样每个设计选择我都会解释它为什么只能这么做、不这么做会出什么问题。看之前只要保证你手上有uCOS_II.H、OS_CORE.C、OS_SEM.C这三个文件就行编译器配置、移植层的东西这一篇不展开。1. 两百行出头的 OS_SEM.C排队逻辑为什么一行都不在这里1.1 把 6736 行拆开看内核其实分了四层6736 行这个数字我第一次听到时也愣了一下以为内核代码量不大一天能看完。实际拆开才发现这里面有一半是注释和空行级别的操作真正干活的 C 代码大概四千行上下再加上uCOS_II.H这个大块头。如果按职责分我会把它切成四层层级代表文件干什么与硬件打交道的边界层OS_CPU.H、OS_CPU_C.C、OS_CPU_A.ASM开关中断、栈操作、任务切换入口内核调度核心OS_CORE.C、OS_TASK.C、OS_TIME.C就绪表、调度器、TCB、时钟节拍同步与通信OS_SEM.C、OS_MBOX.C、OS_Q.C、OS_MUTEX.C四个薄壳各自几十到两百多行可选组件OS_MEM.C、OS_FLAG.C、OS_TMR.C内存分区、事件标志组、软定时器你会发现最反直觉的一点同步与通信这一层四个文件的体量加起来还不如OS_CORE.C一个文件。这不是因为它们功能弱而是因为真正重的东西被提到OS_CORE.C里做成公共设施了。具体说OS_CORE.C里跟事件相关的核心就是四个函数OS_EventWaitListInit、OS_EventTaskWait、OS_EventTaskRdy、OS_EventTO。前两个负责把当前任务挂到某个事件的等待队列上第三个负责从某个事件的等待队列里摘出最高优先级的任务并让它就绪第四个负责超时了把自己从等待队列里摘掉。信号量、邮箱、队列、互斥量的所有 Pend/Post 逻辑全都是在这四个函数上包了一层参数校验和状态更新。1.2 四个 API 各自的职责边界理解了这个分层再看OS_SEM.C就会很清爽它做的无非四件事OSSemCreate从空闲事件块链表里摘一个OS_EVENT出来打上类型标记OS_EVENT_TYPE_SEM把计数值写进OSEventCnt然后调用OS_EventWaitListInit清空等待表。OSSemPend先看计数值大于 0 就直接减一返回否则把自己挂到等待表上调OS_Sched让出 CPU等回来之后判断是被 Post 唤醒还是超时。OSSemPost先看等待表里有没有人有人就调OS_EventTaskRdy唤醒一个并触发调度没人就把计数值加一。OSSemAccept非阻塞版本计数值大于 0 就减一返回原值否则直接返回 0绝对不挂起当前任务。注意OSSemAccept的位置它是唯一一个可以被中断服务程序随便调用的获取接口。这一点后面第 7 节会重点说很多人的工程事故就出在把OSSemPend塞进了 ISR。1.3 这套分层的价值在你移植或换组件的时候才会真正显现我最早读 uC/OS-II 时觉得这种分层是为了显得架构漂亮直到有一次项目需要把一个消息队列换成事件标志组才明白它的好处有多大。因为四个通信组件的内核等待逻辑是共用的你要验证的只是一层薄薄的外壳公共部分只需要验证一次而且一旦你在调试时看到任务卡在OS_EventTaskWait里你就知道问题不在信号量实现错了而是在没人 Post或者Post 的对象搞错了排查半径瞬间缩小一半。更实际的是面试里问到uC/OS-II 的信号量是怎么实现的你能答出真正实现等待队列的是 OS_CORE.C 里的事件机制OS_SEM.C 只是薄封装这一句就能把讨论从背 API 拉到读源码的层次上。顺带提一句和 Linux 的差异这也是热词里经常被一起问到的Linux 的信号量有用户态和内核态两套语义还要处理睡眠、唤醒、内存屏障这些复杂场景uC/OS-II 全部运行在同一个特权态里没有 MMU、没有进程概念所以它可以用位图 数组这种最粗暴但确定性最强的结构来实现等待队列。确定性才是 RTOS 的命根子。2. OS_EVENT 结构体16 个字节里装下一条完整等待队列2.1 五个成员各管一段逻辑OS_EVENT的定义在uCOS_II.H里短得让人想笑typedef struct { INT8U OSEventType; /* 事件类型邮箱/队列/信号量/互斥量 */ INT8U OSEventGrp; /* 等待任务组位图的高 6 位 */ INT16U OSEventCnt; /* 计数仅信号量用 */ void *OSEventPtr; /* 消息指针或指向 OSMutex / OS_Q */ INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待任务的优先级位图 */ } OS_EVENT;我挨个说说它们在实际运行中扮演的角色这比单纯背定义有用得多成员谁在用怎么用OSEventType所有 API 的第一道校验和OS_EVENT_TYPE_SEM等宏比较防止把邮箱当信号量 PostOSEventCnt信号量、互斥量信号量存剩余计数互斥量只当0 或 1用OSEventPtr邮箱、队列、互斥量、空闲链表邮箱存消息指针队列指向OS_Q互斥量指向OSMutexOSEventGrp事件等待表8 个优先级分组的位图非 0 就代表有人在等OSEventTbl[]事件等待表每个字节记录一组 8 个优先级的占用情况在 32 位机器上这个结构体按 4 字节对齐算下来正好 16 字节。OS_MAX_EVENTS一般配成 10 到 20 个也就是说整个事件池也就占几百字节 RAM对 GD32F103 这种 48KB SRAM 的片子完全没有压力。2.2 OSEventTbl 与 OSEventGrp就是第二张就绪表这个地方是整篇文章里我认为最值得反复琢磨的一点。如果你还记得第 3 篇讲就绪表时的那两个全局变量OSRdyGrp和OSRdyTbl[]再看OSEventGrp和OSEventTbl[]会发现它们结构完全一样算法完全一样连查表用的OSUnMapTbl都是同一张。差别只在语义上就绪表里的1表示这个优先级的任务可以运行事件等待表里的1表示这个优先级的任务正在等这个事件。为什么可以这么复用因为在一个任务的生命周期里它要么在就绪表里要么在某一个事件的等待表里或者同时短暂地在两处这个双重登记窗口第 4 节细讲几乎不会出现两个状态都合法且都需要长期存在的情况。uC/OS-II 干脆把两个位图做成同一套算法好处是查找最高优先级等待任务直接复用现成的OSUnMapTbl常数时间 O(1)不需要遍历等待队列。代码量极小OS_EventTaskRdy整个函数不到 30 行。开发者只需要理解一套位图逻辑心智负担直接减半。2.3 OS_EVENT_TBL_SIZE 这个宏决定了你多花多少 RAM这个宏的定义是#define OS_EVENT_TBL_SIZE (OS_LOWEST_PRIO / 8 1)OS_LOWEST_PRIO是你在OS_CFG.H里配的最低优先级编号。常见的配置有两种一是给满 64 个优先级OS_LOWEST_PRIO 63此时OS_EVENT_TBL_SIZE 8二是像很多小项目那样为了省 RAM 配成 31此时OS_EVENT_TBL_SIZE 4。这里有个容易踩的隐形成本每个事件控制块都要自带一份等待表。假设你配了 20 个事件、满优先级那么光等待表就吃掉 20 × 8 160 字节。如果配到 31 个优先级就只用 80 字节。所以我把优先级数量配大一点以后好扩展这个想法在小容量 MCU 上是要算账的事件数量 × 等待表长度 是它带来的真实内存代价。提示改OS_LOWEST_PRIO时不要只改这一个宏。任务优先级数组OSTCBPrioTbl[]、位图数组OSRdyTbl[]的长度都跟着它走改完之后建议把OS_MAX_TASKS和OS_MAX_EVENTS一起重新核对一遍。3. 创建与删除空闲链表的两行摘取和 OSSemDel 的拒绝删除3.1 OSEventFreeList 是一条静态池链成的单向链表OSInit里做的事情很朴素定义了全局数组OSEventTbl[OS_MAX_EVENTS]然后从第 0 个开始把每一块的OSEventPtr指向下一块的地址最后一块指向NULL链表头是全局变量OSEventFreeList。OSEventFreeList OSEventTbl[0]; for (i 0; i OS_MAX_EVENTS - 1; i) { OSEventTbl[i].OSEventPtr (void *)OSEventTbl[i 1]; } OSEventTbl[OS_MAX_EVENTS - 1].OSEventPtr (void *)0;所以在 uC/OS-II 里创建信号量是绝对不会失败的除非你把池子用完了返回NULL。这也是它和动态内存分配最大的区别——没有 malloc就没有碎片也就没有某个时刻突然分配不出来的不确定性。实时系统里这个性质比灵活值钱得多。3.2 OSSemCreate 里那对临界区的位置很有讲究看OSSemCreate的骨架OS_EVENT *OSSemCreate (INT16U cnt) { OS_EVENT *pevent; OS_ENTER_CRITICAL(); pevent OSEventFreeList; /* 摘链表的头 */ if (OSEventFreeList ! (OS_EVENT *)0) { OSEventFreeList (OS_EVENT *)OSEventFreeList-OSEventPtr; } OS_EXIT_CRITICAL(); /* 注意出了临界区才做初始化 */ if (pevent ! (OS_EVENT *)0) { pevent-OSEventType OS_EVENT_TYPE_SEM; pevent-OSEventCnt cnt; pevent-OSEventPtr (void *)0; OS_EventWaitListInit(pevent); /* 清空等待表 */ } return pevent; }摘链表那两行必须在临界区里因为OSEventFreeList是全局共享资源但字段初始化和清等待表这几行被放到了临界区外面。这不是偷懒——这几行只操作刚摘下来的、还没被任何人引用的结构体不存在竞争。把临界区开得越短越好是实时内核里一以贯之的纪律。你在自己写驱动或者多任务共享数据时也可以拿这个当参考临界区里只干必须原子完成的那一步。3.3 OSSemDel 在有人排队时会明确拒绝删除信号量的逻辑比创建精彩得多因为它要回答一个问题如果这时候有人正在等这个信号量删还是不删uC/OS-II 把决定权交给你通过一个opt参数传OS_DEL_NO_PEND只要等待表非空OSEventGrp ! 0立刻返回OS_ERR_TASK_WAITING什么都不做。传OS_DEL_ALWAYS循环调用OS_EventTaskRdy把等待表里的任务逐个摘出来、置为就绪、并把它们的状态掩码清掉让它们带着被中止的错误码返回。第二种模式在工程上很有用设备卸载、通信链路重连、模块去初始化的时候与其让几个任务永远卡在 Pend 上不如统一唤醒并告知失败让上层逻辑走清理流程。但有个细节一定要清楚——这些任务被唤醒后拿到的是错误码不是成功获取到信号量如果你的业务代码不检查*err就会带着我拿到资源了的错误认知继续往下跑后面出现任何离奇现象都不要惊讶。删除动作的最后两步也值得看一眼先把OSEventType置为OS_EVENT_TYPE_UNUSED然后把这个块通过OSEventPtr挂回OSEventFreeList。整块内存原封不动地回收下一句OSSemCreate立刻就能再拿到它。4. OSSemPend 的完整执行链从快速路径到挂起、调度、超时摘除4.1 快速路径计数值减一然后立刻走人void OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *err) { OS_ENTER_CRITICAL(); if (pevent-OSEventCnt 0) { /* 快速路径 */ pevent-OSEventCnt--; OS_EXIT_CRITICAL(); *err OS_NO_ERR; return; /* 没有调度、没有挂起 */ } /* ……慢速路径 */ }这几行是 uC/OS-II 性能的一个关键。信号量够用的时候整个过程就是一次临界区加减法没有任务挂起、没有调度器介入、没有上下文切换。你在周期任务里频繁 Pend 同一把信号量成本极低。这里也是新手最容易困惑的点很多人以为 Pend 一定会触发任务切换所以把 Pend 当让出 CPU的手段用结果发现自己的任务照样占着 CPU 跑。Pend 只在资源不够时才挂起想主动让出 CPU用OSTimeDly或者OSTaskSuspend别用信号量。4.2 慢速路径三个动作、一次调度、一次回检计数值不够的时候进入慢速路径顺序非常固定给当前任务的 TCB 打标记OSTCBCur-OSTCBStat | OS_STAT_SEM;表示我在等信号量。写入超时值OSTCBCur-OSTCBDly timeout;timeout为 0 代表无限等待。调用OS_EventTaskWait(pevent)把自己从就绪表摘掉、把等待表的对应位置 1并记录OSTCBEventPtr指向这个事件。退出临界区调OS_Sched()真正切走。等被别人唤醒或超时后重新进入临界区回检OSTCBCur-OSTCBStat OS_STAT_SEM。第 5 步是整段代码的灵魂。任务被重新调度回来时它并不知道自己是被 Post 唤醒的还是等到超时了。判断依据就是那个状态掩码如果OS_STAT_SEM已经被清掉说明是OS_EventTaskRdy干的它负责清掩码那就是正常获取如果还挂着说明是超时必须调OS_EventTO把自己从等待表里摘干净然后返回OS_TIMEOUT。4.3 超时那一刻的双重登记窗口是理解整条链路的关键很多人读到这里会有一个疑问超时是怎么发生的时钟节拍中断OSTimeTick里遍历OSTCBList递减OSTCBDly减到 0 就把任务挂回就绪表。但注意此刻它还在事件的等待表里因为OS_EventTO要等任务真正被调度回来才执行。于是出现了一个短暂窗口这个任务同时在就绪表和某个事件的等待表里。这个窗口里如果恰好OSSemPost来了会发生什么OS_EventTaskRdy从等待表里把它摘出来清掉OS_STAT_SEM然后再往就绪表里置位重复置位无害。等它被调度回来一看OS_STAT_SEM没了于是按成功获取处理返回OS_NO_ERROS_EventTO根本不会被调用。这套设计很巧妙信号量和超时是谁先到谁赢不存在两边都生效导致的资源泄漏或者状态错乱。反过来如果超时先落地、任务已经被唤醒并调用了OS_EventTO摘除等待表那之后到来的 Post 就会走没人等待分支把计数值加一留给下一个人。逻辑上是自洽的。提示OSTimeTick里对还没解除阻塞的任务有个小技巧——递减到 0 时如果发现OSTCBStat还挂着阻塞位就把OSTCBDly强制写回 1让它下个节拍继续被计数。这保证了超时行为在 Tick 层面是水位保持真正的摘除动作永远由任务自己完成。5. OSSemPost 的两条分支与 OS_EventTaskRdy 的摘取算法5.1 有人等待查两次表拿到最高优先级OS_EventTaskRdy是整条链路里最短也最漂亮的一段核心就三行查表y OSUnMapTbl[pevent-OSEventGrp]; /* 找组号 */ x OSUnMapTbl[pevent-OSEventTbl[y]]; /* 找组内偏移 */ prio (INT8U)((y 3) x); /* 拼出优先级 */OSUnMapTbl是一张 256 字节的固定表输入一个字节输出其中最低位为 1 的位置。所以从一组里找编号最小的置位是查一次表的事跟等待任务数量完全无关。这就是 O(1) 调度在事件层的复刻。拿到优先级之后动作依次是清OSEventGrp对应的位、清OSEventTbl[y]对应的位、通过OSTCBPrioTbl[prio]找到 TCB、把OSTCBDly清零、清掉状态掩码里的OS_STAT_SEM最后判断掩码是否为 0因为任务可能还被OSTaskSuspend挂着为 0 才真正往就绪表里置位。5.2 没人等待加计数以及 65535 这个天花板另一条分支简单到只有几行但有个边界值得一提if (pevent-OSEventCnt 65535) { pevent-OSEventCnt; return OS_NO_ERR; } return OS_SEM_OVF; /* 溢出 */OSEventCnt是INT16U上限 65535。超出就返回溢出错误。正常用法几乎不可能碰到但如果你把信号量当事件计数器滥用——比如在 ISR 里无脑 Post 而没人 Pend——跑个几万次就会撞上这个天花板表现为 Post 开始失败。这种情况的正确做法是改用事件标志组或者消息队列而不是把信号量当计数器堆。5.3 三个事件函数之间的对称性把OS_EventTaskWait、OS_EventTaskRdy、OS_EventTO摆在一起看能看出一组非常工整的对称关系函数对就绪表对等待表调用者OS_EventTaskWait清自己那一位置自己那一位Pend 挂起时OS_EventTaskRdy置对方那一位清对方那一位Post 唤醒时OS_EventTO不动清自己那一位Pend 超时收尾时OS_EventTaskWait和OS_EventTO是同一件事的正反两面一个挂上去一个摘下来操作的对象都是当前任务OS_EventTaskRdy则由别人代劳。三个函数共用同一套位图约定所以无论从哪条路径进来位图的最终状态都是一致的。我自己在纸上画过这三者的状态迁移图画完才意识到 uC/OS-II 为什么敢把四个通信组件共用这套底座——只要位图状态自洽上层是信号量还是消息队列对事件层来说毫无区别。参数里的那个状态掩码OS_STAT_SEM/OS_STAT_MBOX/OS_STAT_Q就是唯一的区分依据。6. 互斥量与优先级反转OSMutexPend 里那段改优先级的代码6.1 先复现一遍优先级反转的现场假设三个任务H 优先级 5M 优先级 10L 优先级 20。L 先拿到互斥量H 随后 Pend 被阻塞。此时如果 M 就绪了它会抢占 L——因为 L 的优先级比 M 低。结果就是 H 明明优先级最高却要等 L 执行完而 L 又被 M 无限打断高优先级任务的响应时间被中优先级任务拖成了不确定值。这就是优先级反转。普通信号量完全无法处理这个问题它只认计数器。所以 uC/OS-II 单独做了一个OSMutexPend把我是谁拥有的这个信息记进了事件块typedef struct { INT8U OSMutexPrio; /* 拥有者的优先级 */ OS_TCB *OSMutexOwner; /* 指向拥有者的 TCB */ } OSMutex;pevent-OSEventPtr指向的就是这个结构。所以互斥量的OSEventCnt只当 0/1 的标志用真正的所有权信息在OSMutex里。6.2 优先级继承具体做了哪些动作OSMutexPend在发现互斥量已被别人占有时会比较当前任务优先级和拥有者优先级。如果当前任务的优先级更高数值更小就把拥有者的优先级临时提升到和当前任务一样高这个动作不是简单改一个字段就完事它要同步搬迁三处信息OSTCBPrioTbl[]里把拥有者的 TCB 从老优先级位置挪到新位置。拥有者当前在就绪表里的位从老的组/位搬到新的组/位。如果拥有者此刻自己也阻塞在另一个事件上等待表里有它那它的位也要在新的优先级位置上重新置位。搬完之后M 就抢不动 L 了因为 L 现在的优先级已经和 H 一样高。等 L 释放互斥量时OSMutexPost会把优先级恢复回OSMutexPrio记录的原值并把所有权移交给等待队列里优先级最高的那个任务。我在 GD32F103 上实测过一个典型场景用三个周期任务模拟上面那套优先级关系加上互斥量之后高优先级任务的最坏等待时间从被中优先级任务随意延长的毫秒级抖动收敛到了L 的一小段临界区时间效果非常明显。反过来如果把互斥量换成普通信号量同一套代码就会出现偶发的几十毫秒延迟而且这个延迟和系统负载正相关非常难查。6.3 互斥量的四条使用纪律读到这里互斥量的使用边界其实已经由源码本身划出来了我总结成四条纪律纪律原因绝不在中断里 Pend/Post 互斥量中断没有 TCB 参与调度优先级继承会失去意义谁 Pend 谁 Post所有权记录在OSMutex里别的任务 Post 会破坏所有权链临界区尽量短优先级继承只解决被中优先级抢占不解决持锁太久不支持递归获取同一个任务重复 Pend 会自己死锁不像某些实现有递归计数顺带对比一下普通信号量两者的差别其实比很多人想的更根本对比项信号量 OSSem互斥量 OSMutex计数语义可以是 0 到 65535 的计数只能是 0 或 1所有权无谁 Post 都行有记录拥有者 TCB优先级继承无有中断里使用Post 可以Pend 不可以都不建议典型场景任务间同步、ISR 通知任务保护共享资源uC/OS-II 走的是优先级继承PIP这条路没有实现优先级天花板协议。这意味着它没法从数学上严格保证最坏阻塞时间但对绝大多数嵌入式场景已经足够了。如果你在做硬实时性分析这一点必须写进你的时序预算里。7. 落到 GD32F103 工程上信号量使用的六个坑7.1 中断里只能 Post不能 Pend这是最经典也最致命的一条。OSSemPend的慢速路径里有一步OS_EventTaskWait它要操作当前任务的 TCB还要调OS_Sched。在中断服务程序里根本没有当前任务要被换掉这个概念一旦走到慢速路径就是未定义行为典型现象是任务状态位图被写坏、几个任务莫名其妙都不跑了。正确的 ISR 写法就三步void EXTI0_IRQHandler(void) { OSIntEnter(); /* 通知内核进入中断 */ if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { /* 干活尽量短 */ OSSemPost(MySem); /* 只 Post不 Pend */ EXTI_ClearITPendingBit(EXTI_Line0); } OSIntExit(); /* 退出并触发调度判断 */ }OSIntExit会在中断嵌套全部退出后检查是否需要调度。少了它你在 ISR 里 Post 完任务可能一直不切回去现象就是信号量发出去了但任务没动。如果确实需要在中断里获取资源用OSSemAccept它不会挂起任何任务拿不到就返回 0让上层自己决定丢帧还是缓存。7.2 忽略 *err 返回值是偶发死锁的第一嫌疑OSSemPend的第三个参数是错误码输出很多人图省事传个变量进去就再也不看了。但OS_TIMEOUT和OS_NO_ERR的处理路径完全不一样OSSemPend(MySem, 100, err); if (err OS_NO_ERR) { /* 真的拿到资源了才能操作共享数据 */ do_something(); } else { /* 超时或者被中止绝对不能碰共享资源 */ handle_timeout(); }漏掉这个判断超时的任务会带着我以为我拿到了的错觉去动共享缓冲区表现出来就是数据偶发错乱而且只在负载高的时候出现查起来要人命。我见过一次现场症状是串口偶尔吐出半包乱码最后追到根源就是这里少了一个if。7.3 调度器启动前 Pend 会直接卡死OSSemPend的慢速路径最终会调OS_Sched而OSStart之前调度器还没跑起来。如果你在main里创建了任务、然后想在OSStart之前 Pend 一个还没被 Post 的信号量程序会停在那儿不动而且没有任何错误码提示。调度器启动前能用的是OSSemCreate、OSSemAccept这类不涉及挂起的接口。要同步就让信号量在创建时带上初始计数值或者干脆把这段初始化逻辑挪到一个最先运行的任务里。7.4 timeout 传 0 是无限等待不是立刻返回timeout参数为 0 的语义是一直等下去。想立刻返回用OSSemAccept。这两个接口混淆的后果是本该快速失败的地方变成了永久阻塞整个任务链条悄无声息地停摆。我在代码审查里养成的一个习惯就是看到OSSemPend的第二个参数是 0就要追问一句确定这里可以无限等7.5 同一个信号量被多个地方 Post小心语义失控信号量没有所有权谁都能 Post。这在ISR 通知任务这种一对一场景下很舒服但一旦有多个生产者往同一个信号量上 Post就很容易出现计数虚高某个任务 Pend 一次拿走一个计数实际却有两份数据于是数据被消费两次。这不是信号量的 bug是用法超出了它的语义。这种场景应该用消息队列让数据和计数绑定在一起。7.6 面试视角这几个问题能把读过源码和背过 API分开现在 RTOS 相关的问题越来越往原理上问我列几个和本篇直接对得上的uC/OS-II 的信号量在资源不足时任务是怎么排队的——位图等待表不是链表查找 O(1)。超时是怎么实现的会不会和 Post 撞车——Tick 递减双重登记窗口靠OSTCBStat回检判定谁赢。为什么互斥量不能在中断里用——优先级继承依赖 TCB 和调度器参与中断上下文里没有这些前提。信号量和互斥量的本质区别是什么——所有权而不是计数范围。X 信号量计数溢出会怎样——返回溢出错误码OSEventCnt是 16 位。能把这些串起来讲清楚基本就说明你不是只看了 API 手册。7.7 我自己读这段源码时的一个小习惯最后分享一个我个人的做法读OS_SEM.C的时候我会在旁边开一个记事本把每个函数里临界区的进出位置单独列一列。原因很简单——RTOS 里的 bug 十有八九和临界区有关。列完之后你会发现OSSemPost的临界区里包含了OS_EventTaskRdy但排除了OS_SchedOSSemPend的临界区里包含了OS_EventTaskWait但同样排除了OS_Sched。这个规律一旦被你看穿再去看消息邮箱、消息队列、互斥量你就知道该盯哪儿了速度会快很多。下一篇我会顺着这条线往OS_Q.C走看消息队列怎么在同一个事件底座上长出环形缓冲区 生产者消费者这套结构以及OSEventPtr指向OS_Q之后OS_EventTaskRdy多出来的那个参数到底是干什么用的。
返回列表