
信号量只是“计数器 等待队列”在 uC/OS-II 里一个 OSSemPend 背后藏着与就绪表完全同构的等待表同一个位图结构、同一张 OSUnMapTbl。本文从 OS_EVENT 结构出发讲透 OSSemCreate/Pend/Post 完整流程与超时机制读懂内核任务同步的第一块基石。先问一个看似简单的问题当任务调用OSSemPend()却拿不到信号量时内核把它“藏”到了哪里答案是一张和就绪表长得几乎一模一样的表。前 5 篇我们搞定了“谁该跑”——就绪表位图、OSUnMapTbl 查表、OSTimeTick 节拍驱动。从这篇开始要解决“谁在等、等到了没”。而设计者用一个极其优雅的方式解决了它把“等待”也做成一张位图表和就绪表共享同一套算法。这就是 uC/OS-II 同步机制的基石——OS_EVENT 统一模型。一个结构装下四种原语信号量Semaphore用于资源计数与任务同步、邮箱、消息队列、互斥量——四种完全不同的同步原语在 uC/OS-II 里共用同一个容器OS_EVENT即事件控制块Event Control BlockECB。它不是某个原语的私有结构而是所有同步机制的统一模型。看源码ucos_ii.h:352typedefstructos_event{INT8U OSEventType;/* Type of event control block1MBOX 2Q 3SEM 4MUTEX */void*OSEventPtr;/* 指针邮箱/队列消息或互斥量相关信号量不用 */INT16U OSEventCnt;/* 信号量计数值 */OS_PRIO OSEventGrp;/* 等待任务组位图与 OSRdyGrp 同构 */OS_PRIO OSEventTbl[OS_EVENT_TBL_SIZE];/* 等待任务表与 OSRdyTbl 同构 */}OS_EVENT;逐字段看OSEventType类型标签。1邮箱、2队列、3信号量、4互斥量。内核靠它做类型校验防止你把一个邮箱当信号量 Post。OSEventPtr通用指针。信号量完全不用它邮箱存消息指针互斥量存优先级继承所需的结构体。OSEventCnt信号量的计数值。只有信号量用。OSEventGrp OSEventTbl等待任务组位图和等待任务表。本篇主角所有原语共用。OS_EVENT_TBL_SIZE被定义为OS_LOWEST_PRIO / 8 1即 8。也就是说等待表和就绪表拥有完全相同的容量。一句话总结OS_EVENT 是个“万能插座”四种原语各自只取自己需要的字段。等待表和就绪表天生的孪生兄弟OS_EVENT 最大的秘密藏在最后两个字段里。回顾前几篇的就绪表结构OSRdyGrp8 bit 组位图OSRdyTbl[8]每组 8 bit 任务位图查最高优先级任务用OSUnMapTbl两步查表。现在看等待表OSEventGrpOSEventTbl[8]结构一模一样。查最高优先级等待者用的还是同一个OSUnMapTbl。一个是“谁可以跑”一个是“谁在等同一个事件”。算法完全复用这就是设计上的“孪生”。更关键的是这两个表在任务状态切换时是联动的。看OS_EventTaskWait()os_core.c:1103OSTCBCur-OSTCBEventPtrpevent;/* TCB 记录等哪个事件 */pevent-OSEventTbl[OSTCBCur-OSTCBY]|OSTCBCur-OSTCBBitX;/* 挂进等待表 */pevent-OSEventGrp|OSTCBCur-OSTCBBitY;yOSTCBCur-OSTCBY;OSRdyTbl[y]~OSTCBCur-OSTCBBitX;/* 同时从就绪表清除 */if(OSRdyTbl[y]0u){OSRdyGrp~OSTCBCur-OSTCBBitY;}注意看这个对称操作一手进等待表一手出就绪表。这不是巧合。任务要么在就绪表里等着被调度要么在等待表里等着事件发生两个状态互斥。所以“登记等待”和“摘除就绪”必须一次性完成不能拆开否则调度器会选中一个根本不该跑的任务。理解了这层孪生关系后面 Pend/Post 的代码你基本能自己读懂了。创建信号量从空闲链表“领养”一块 OS_EVENTOSSemCreate()os_sem.c:94是所有同步原语的起点。它的流程很短ISR 中调用直接返回 NULL。中断里不能创建内核对象这是硬性约束。从OSEventFreeList空闲链表中取出一块 OS_EVENT。这块内存不是动态分配的而是静态数组OSEventTbl[OS_MAX_EVENTS]空闲链表只是把这些块串起来管理。填入OSEventType OS_EVENT_TYPE_SEM、OSEventCnt cnt、OSEventName ?再调用OS_EventWaitListInit()把等待表全部清零。OS_EventWaitListInit做的事和就绪表清零一样朴素OSEventGrp 置 0OSEventTbl 八个字节逐个清 0。等待表从没人等开始谁 Pend 谁登记。顺带区分两个概念计数信号量计数初始为 N用于多资源管理比如 5 个串口和二进制信号量计数只取 0/1用于任务同步比如数据就绪通知。uC/OS-II 没有独立的二进制信号量类型——初始化时传 1 就是二进制语义。但注意用信号量做互斥有风险——高优先级任务可能被低优先级任务持锁阻塞这就是著名的优先级翻转第 7 篇的互斥量专门解决它。至此一个全新的信号量诞生了。它的等待表空空如也计数就是初值。OSSemPend计数不够就把任务”搬进”等待表OSSemPend(pevent, timeout, perr)是信号量最核心的 API。先看关键路径os_sem.c:323-360OS_ENTER_CRITICAL();if(pevent-OSEventCnt0u){/* 计数0资源现成 */pevent-OSEventCnt--;/* 直接减计数不等待 */*perrOS_ERR_NONE;return;}/* 否则进入等待 */OSTCBCur-OSTCBStat|OS_STAT_SEM;/* 1) 置”等信号量”状态位 */OSTCBCur-OSTCBStatPendOS_STAT_PEND_OK;OSTCBCur-OSTCBDlytimeout;/* 2) 超时写进 TCB */OS_EventTaskWait(pevent);/* 3) 挂等待表 清就绪表 */OS_EXIT_CRITICAL();OS_Sched();/* 4) 让出 CPU */OS_ENTER_CRITICAL();switch(OSTCBCur-OSTCBStatPend){/* 5) 被唤醒后判断结果 */caseOS_STAT_PEND_OK:*perrOS_ERR_NONE;break;caseOS_STAT_PEND_ABORT:*perrOS_ERR_PEND_ABORT;break;caseOS_STAT_PEND_TO:OS_EventTaskRemove(OSTCBCur,pevent);/* 超时从等待表摘除 */*perrOS_ERR_TIMEOUT;break;}OSTCBCur-OSTCBStatOS_STAT_RDY;/* 清理状态恢复就绪 */OSTCBCur-OSTCBStatPendOS_STAT_PEND_OK;OSTCBCur-OSTCBEventPtr(OS_EVENT*)0;读这段代码有三个关键点第一快路径。计数大于 0 时Pend 就是一次”减一然后返回”连等待表都不碰。这是信号量最常见的用法——资源本来就有。第二慢路径的五步。计数为 0 时置状态位OS_STAT_SEM→ 把 timeout 写进 OSTCBDly衔接第 5 篇的延时机制→ OS_EventTaskWait 挂等待表/清就绪表 → OS_Sched 让出 → 被唤醒后按结果分支。timeout 传 0 表示永不超时——OSTCBDly 为 0节拍中断根本不会递减它。第三超时路径为什么多一步 OS_EventTaskRemove。看第 5 篇节拍到期时 OSTimeTick 只做了”清状态位 置位就绪表”没有把任务从等待表摘除。所以 Pend 的 switch 里要补这一步——否则任务还在等待表里下次 Post 会把它再”唤醒”一次。这个细节是信号量与延时机制的交汇点。还有一组前置守卫ISR 里调用返回 OS_ERR_PEND_ISR、调度器锁定返回 OS_ERR_PEND_LOCKED、类型不对返回 OS_ERR_EVENT_TYPE——都在进入临界区之前拦截。这组校验透露了一个设计取向uC/OS-II 在性能与安全之间选了一个中间点。OS_ARG_CHK_EN 开启时所有 API 都做参数校验pevent 空指针、类型标签关闭时这些检查全部编译掉——第 1 篇讲过的配置驱动在这里就是牺牲一点健壮性换几个周期的选择。再补充一个容易踩的坑Pend 之后必须检查 err。任务被唤醒有三种结果正常拿到OS_ERR_NONE、被 PendAbort 中止OS_ERR_PEND_ABORT、超时OS_ERR_TIMEOUT。只检查 errNONE 才访问共享资源是信号量使用的铁律——否则超时醒来还去操作资源等于在毫无保护的情况下访问。OS_EventTaskRdy唤醒只叫一个人等信号量的任务怎么被叫醒看 OSSemPost 的对手戏。OS_EventTaskRdyos_core.c:1028是唤醒的核心yOSUnMapTbl[pevent-OSEventGrp];/* 等待表两步查表找最高优先级等待者 */xOSUnMapTbl[pevent-OSEventTbl[y]];prio(y3u)x;ptcbOSTCBPrioTbl[prio];ptcb-OSTCBDly0u;/* 防止节拍提前/重复唤醒 */ptcb-OSTCBStat~msk;/* 清等待状态位 */ptcb-OSTCBStatPendpend_stat;if((ptcb-OSTCBStatOS_STAT_SUSPEND)OS_STAT_RDY){OSRdyGrp|ptcb-OSTCBBitY;/* 置位就绪表 */OSRdyTbl[y]|ptcb-OSTCBBitX;}OS_EventTaskRemove(ptcb,pevent);/* 从等待表移除 */又是两步查表——等待表复用就绪表那套 OSUnMapTbl 算法。找出来的不是”任意一个等待者”而是等待者里优先级最高的那个。细节一OSTCBDly 0。如果任务带着超时在等OSTCBDly 非 0Post 唤醒时先把它清零——防止节拍中断晚一拍到达把刚被唤醒的任务又当成”超时”处理。细节二唤醒后从等待表移除。等待表只登记”还在等”的任务被叫醒的马上摘除腾出位置。OSSemPost先看有没有人等再决定加不加计数Post 的逻辑只有三行核心os_sem.c:488-499if(pevent-OSEventGrp!0u){/* 有任务在等 */(void)OS_EventTaskRdy(pevent,(void*)0,OS_STAT_SEM,OS_STAT_PEND_OK);/* 唤醒最高优先级等待者 */OS_EXIT_CRITICAL();OS_Sched();/* 立刻看调度 */return(OS_ERR_NONE);}if(pevent-OSEventCnt65535u){/* 没人等 → 计数 1 */pevent-OSEventCnt;return(OS_ERR_NONE);}return(OS_ERR_SEM_OVF);/* 溢出保护 */这里藏着信号量最重要的语义有等待者时Post 把”信号”直接转交给等待者计数不增加。为什么设想一个反例计数初始 1任务 A Pend 拿到计数 0任务 B 再 Pend挂起等。此时如果 Post 先计数 1 再唤醒 BB 被唤醒后按”快路径”减一——看起来没事但中间多了一个状态如果唤醒和调度之间又有任务 C 插进来 PendC 会把那个 1 抢走B 就白等了。先转交后计数从机制上杜绝了这种竞态。计数只有在”没人等”时才累加等下次有人 Pend 时直接拿走。另一个细节Post 可以在 ISR 里调用Pend 不行——这是中断”通知”任务的标准姿势第 4 篇的 ISR 纪律里提过。三任务场景一次完整的 Pend/Post 推演系统里三个任务A优先级 5、B优先级 3、C优先级 10一个计数为 0 的信号量。A 调 OSSemPend(sem, 0, err)计数 0 → 慢路径。OS_STAT_SEM 置位、OSTCBDly0、OS_EventTaskWait 把 A 挂进 sem 的等待表OSEventTbl[0] bit5OSEventGrp bit0、A 从就绪表消失 → OS_Sched 切到 B。B 调 OSSemPend(sem, 0, err)同样挂起。等待表现在有两个人OSEventGrp0x01OSEventTbl[0]0x28bit3 和 bit5。C 调 OSSemPost(sem)等待表非空 → OS_EventTaskRdy 两步查表yOSUnMapTbl[0x01]0xOSUnMapTbl[0x28]3 → prio3唤醒 B最高优先级等待者→ B 清状态、置就绪、摘等待表 → OS_Sched就绪表里 B(3) 比 C(10) 优先级高C 刚 Post 完就被 B 抢走 CPU——这就是抢占。B 恢复运行从 Pend 返回 OS_ERR_NONE。A 继续在等待表里。这个推演还揭示一个易错点Post 唤醒的永远是等待者里优先级最高的不一定是”先来先等”的那个——信号量没有排队公平性高优先级可以插队。什么时候用信号量三个典型场景资源计数N 个同类资源、N 次机会、任务同步生产者/消费者握手、ISR 通知任务Post from ISR。什么时候不用互斥用 OSMutex第 7 篇、传数据用邮箱/队列第 8 篇——选错原语不是性能问题是正确性问题。为什么四种原语要共用 OS_EVENT回到篇首的问题。信号量、邮箱、队列、互斥量等待/唤醒的机制 90% 相同等待表登记、就绪表联动、HPT 两步查表、超时处理、ISR 规则。差异只在两处——触发条件计数、指针、队列缓冲和唤醒后的数据交付OSEventPtr、OSTCBMsg。共用 OS_EVENT 意味着等待逻辑只写一遍四个模块各自只写差异部分。这也解释了为什么 OS_EVENT 的注释敢说”这就是全部”——它确实就是全部。总结OS_EVENT 是四种同步原语的统一容器等待表与就绪表同构、共享 OSUnMapTblOSSemPend 快路径减计数返回慢路径五步挂起状态位→超时→进等待表→让出→结果判断超时路径的 OS_EventTaskRemove 补齐了 OSTimeTick 没做的工作OS_EventTaskRdy 唤醒最高优先级等待者OSTCBDly 清零防节拍误唤醒OSSemPost 先转交后计数从机制上杜绝信号竞态ISR 可 Post 不可 Pend信号量唤醒无公平性高优先级可插队下一篇看 OS_EVENT 上最复杂的应用互斥量。为什么普通信号量会导致优先级翻转优先级继承PIP是怎么救场的到时见。