当前位置: 首页 > news >正文

【系列:uC/OS-II 内核源码精读:从 6736 行代码看懂一个 RTOS · 第 6 篇】

信号量只是“计数器 + 等待队列”?在 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 Block,ECB)。

它不是某个原语的私有结构,而是所有同步机制的统一模型。看源码(ucos_ii.h:352):

typedefstructos_event{INT8U OSEventType;/* Type of event control block(1=MBOX 2=Q 3=SEM 4=MUTEX) */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 最大的秘密,藏在最后两个字段里。

回顾前几篇的就绪表结构:OSRdyGrp(8 bit 组位图)+OSRdyTbl[8](每组 8 bit 任务位图),查最高优先级任务用OSUnMapTbl两步查表。

现在看等待表:OSEventGrp+OSEventTbl[8],结构一模一样。查最高优先级等待者,用的还是同一个OSUnMapTbl

一个是“谁可以跑”,一个是“谁在等同一个事件”。算法完全复用,这就是设计上的“孪生”。

更关键的是,这两个表在任务状态切换时是联动的。看OS_EventTaskWait()(os_core.c:1103):

OSTCBCur->OSTCBEventPtr=pevent;/* TCB 记录等哪个事件 */pevent->OSEventTbl[OSTCBCur->OSTCBY]|=OSTCBCur->OSTCBBitX;/* 挂进等待表 */pevent->OSEventGrp|=OSTCBCur->OSTCBBitY;y=OSTCBCur->OSTCBY;OSRdyTbl[y]&=~OSTCBCur->OSTCBBitX;/* 同时从就绪表清除 */if(OSRdyTbl[y]==0u){OSRdyGrp&=~OSTCBCur->OSTCBBitY;}

注意看这个对称操作:一手进等待表,一手出就绪表

这不是巧合。任务要么在就绪表里等着被调度,要么在等待表里等着事件发生,两个状态互斥。所以“登记等待”和“摘除就绪”必须一次性完成,不能拆开,否则调度器会选中一个根本不该跑的任务。

理解了这层孪生关系,后面 Pend/Post 的代码你基本能自己读懂了。

创建信号量:从空闲链表“领养”一块 OS_EVENT

OSSemCreate()(os_sem.c:94)是所有同步原语的起点。它的流程很短:

  1. ISR 中调用直接返回 NULL。中断里不能创建内核对象,这是硬性约束。
  2. OSEventFreeList空闲链表中取出一块 OS_EVENT。这块内存不是动态分配的,而是静态数组OSEventTbl[OS_MAX_EVENTS],空闲链表只是把这些块串起来管理。
  3. 填入OSEventType = OS_EVENT_TYPE_SEMOSEventCnt = cntOSEventName = "?",再调用OS_EventWaitListInit()把等待表全部清零。

OS_EventWaitListInit做的事和就绪表清零一样朴素:OSEventGrp 置 0,OSEventTbl 八个字节逐个清 0。等待表从"没人等"开始,谁 Pend 谁登记。

顺带区分两个概念:计数信号量(计数初始为 N,用于多资源管理,比如 5 个串口)和二进制信号量(计数只取 0/1,用于任务同步,比如"数据就绪"通知)。uC/OS-II 没有独立的二进制信号量类型——初始化时传 1 就是二进制语义。但注意:用信号量做互斥有风险——高优先级任务可能被低优先级任务持锁阻塞,这就是著名的"优先级翻转",第 7 篇的互斥量专门解决它。

至此,一个全新的信号量诞生了。它的等待表空空如也,计数就是初值。

OSSemPend:计数不够,就把任务”搬进”等待表

OSSemPend(pevent, timeout, perr)是信号量最核心的 API。先看关键路径(os_sem.c:323-360):

OS_ENTER_CRITICAL();if(pevent->OSEventCnt>0u){/* 计数>0:资源现成 */pevent->OSEventCnt--;/* 直接减计数,不等待 */*perr=OS_ERR_NONE;return;}/* 否则:进入等待 */OSTCBCur->OSTCBStat|=OS_STAT_SEM;/* 1) 置”等信号量”状态位 */OSTCBCur->OSTCBStatPend=OS_STAT_PEND_OK;OSTCBCur->OSTCBDly=timeout;/* 2) 超时写进 TCB */OS_EventTaskWait(pevent);/* 3) 挂等待表 + 清就绪表 */OS_EXIT_CRITICAL();OS_Sched();/* 4) 让出 CPU */OS_ENTER_CRITICAL();switch(OSTCBCur->OSTCBStatPend){/* 5) 被唤醒后判断结果 */caseOS_STAT_PEND_OK:*perr=OS_ERR_NONE;break;caseOS_STAT_PEND_ABORT:*perr=OS_ERR_PEND_ABORT;break;caseOS_STAT_PEND_TO:OS_EventTaskRemove(OSTCBCur,pevent);/* 超时:从等待表摘除 */*perr=OS_ERR_TIMEOUT;break;}OSTCBCur->OSTCBStat=OS_STAT_RDY;/* 清理状态恢复就绪 */OSTCBCur->OSTCBStatPend=OS_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)。只检查 err==NONE 才访问共享资源,是信号量使用的铁律——否则超时醒来还去操作资源,等于在毫无保护的情况下访问。

OS_EventTaskRdy:唤醒,只叫一个人

等信号量的任务怎么被叫醒?看 OSSemPost 的对手戏。OS_EventTaskRdy(os_core.c:1028)是唤醒的核心:

y=OSUnMapTbl[pevent->OSEventGrp];/* 等待表两步查表,找最高优先级等待者 */x=OSUnMapTbl[pevent->OSEventTbl[y]];prio=(y<<3u)+x;ptcb=OSTCBPrioTbl[prio];ptcb->OSTCBDly=0u;/* 防止节拍提前/重复唤醒 */ptcb->OSTCBStat&=~msk;/* 清等待状态位 */ptcb->OSTCBStatPend=pend_stat;if((ptcb->OSTCBStat&OS_STAT_SUSPEND)==OS_STAT_RDY){OSRdyGrp|=ptcb->OSTCBBitY;/* 置位就绪表 */OSRdyTbl[y]|=ptcb->OSTCBBitX;}OS_EventTaskRemove(ptcb,pevent);/* 从等待表移除 */

又是两步查表——等待表复用就绪表那套 OSUnMapTbl 算法。找出来的不是”任意一个等待者”,而是等待者里优先级最高的那个。

细节一:OSTCBDly = 0。如果任务带着超时在等(OSTCBDly 非 0),Post 唤醒时先把它清零——防止节拍中断晚一拍到达,把刚被唤醒的任务又当成”超时”处理。

细节二:唤醒后从等待表移除。等待表只登记”还在等”的任务,被叫醒的马上摘除,腾出位置。

OSSemPost:先看有没有人等,再决定加不加计数

Post 的逻辑只有三行核心(os_sem.c:488-499):

if(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->OSEventCnt<65535u){/* 没人等 → 计数 +1 */pevent->OSEventCnt++;return(OS_ERR_NONE);}return(OS_ERR_SEM_OVF);/* 溢出保护 */

这里藏着信号量最重要的语义:有等待者时,Post 把”信号”直接转交给等待者,计数不增加

为什么?设想一个反例:计数初始 1,任务 A Pend 拿到(计数 0),任务 B 再 Pend(挂起等)。此时如果 Post 先计数 +1 再唤醒 B,B 被唤醒后按”快路径”减一——看起来没事,但中间多了一个状态:如果唤醒和调度之间又有任务 C 插进来 Pend,C 会把那个 +1 抢走,B 就白等了。先转交后计数,从机制上杜绝了这种竞态。计数只有在”没人等”时才累加,等下次有人 Pend 时直接拿走。

另一个细节:Post 可以在 ISR 里调用(Pend 不行)——这是中断”通知”任务的标准姿势,第 4 篇的 ISR 纪律里提过。

三任务场景:一次完整的 Pend/Post 推演

系统里三个任务:A(优先级 5)、B(优先级 3)、C(优先级 10),一个计数为 0 的信号量。

  1. A 调 OSSemPend(sem, 0, &err):计数 0 → 慢路径。OS_STAT_SEM 置位、OSTCBDly=0、OS_EventTaskWait 把 A 挂进 sem 的等待表(OSEventTbl[0] bit5,OSEventGrp bit0)、A 从就绪表消失 → OS_Sched 切到 B。
  2. B 调 OSSemPend(sem, 0, &err):同样挂起。等待表现在有两个人:OSEventGrp=0x01,OSEventTbl[0]=0x28(bit3 和 bit5)。
  3. C 调 OSSemPost(sem):等待表非空 → OS_EventTaskRdy 两步查表:y=OSUnMapTbl[0x01]=0,x=OSUnMapTbl[0x28]=3 → prio=3,唤醒 B(最高优先级等待者)→ B 清状态、置就绪、摘等待表 → OS_Sched:就绪表里 B(3) 比 C(10) 优先级高,C 刚 Post 完就被 B 抢走 CPU——这就是抢占。
  4. 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 是四种同步原语的统一容器,等待表与就绪表同构、共享 OSUnMapTbl
  • OSSemPend 快路径减计数返回;慢路径五步挂起(状态位→超时→进等待表→让出→结果判断)
  • 超时路径的 OS_EventTaskRemove 补齐了 OSTimeTick 没做的工作
  • OS_EventTaskRdy 唤醒最高优先级等待者,OSTCBDly 清零防节拍误唤醒
  • OSSemPost 先转交后计数,从机制上杜绝信号竞态;ISR 可 Post 不可 Pend
  • 信号量唤醒无公平性,高优先级可插队

下一篇看 OS_EVENT 上最复杂的应用:互斥量。为什么普通信号量会导致优先级翻转?优先级继承(PIP)是怎么救场的?到时见。

http://www.cnnetsun.cn/news/4182515.html

相关文章:

  • 做了13年机器人,我发现插件从来不是越多越好
  • DeepSeek-V4多模态模型实战:从API调用到生产部署全指南
  • Azure生产级Vera Rubin平台:AI算力工程化与云服务实战指南
  • 为AI科学智能体构建长程记忆:情景与语义融合架构详解
  • 2026池州工程建筑材料检测排名 TOP5 CMA 资质提供钢材检测、水泥检测、砂石检测 全覆盖联系方式推荐.txt
  • AT32F421F8P7国产M4 MCU实战入门:TSSOP20裸芯快速点亮指南
  • 小红书无水印下载:4 个入口带走原画质作品,附避坑清单
  • 6 大直播平台+自定义直播源,免费直播播放器纯粹直播 5 分钟跑起来
  • 8G显存玩转4K AI视频生成:ComfyUI+AnimateDiff高清工作流实战
  • CefFlashBrowser:内置 Flash 播放器,播 SWF、导出存档、绕过版本校验三合一
  • 低显存显卡玩转AI视频生成:ComfyUI+AnimateDiff实战指南
  • OpenArm 开源人形机械臂:7 自由度遥操作数据采集与可复现评测搭建指南
  • VSCode+ESP-IDF开发实战:从环境搭建到JTAG调试
  • 从零构建AI Agent:实战智能数据分析助手开发指南
  • 毕业季必备AI工具:论文查重、简历优化与面试模拟实战评测
  • 层次分析法实战:从主观决策到量化分析,数学建模与多准则决策指南
  • 这几乎是看到过互动最多的了:340个点赞----半天
  • 实战InsightFace驾驶员注意力监测:从视线估计到疲劳预警
  • 功能测试面试题解析与四象限法实战
  • 回测最后一天刚发出买入信号:没有下一交易日时怎样收尾
  • 机器学习模型描述:从特征、假设到参数,构建AI项目的通用语言
  • 6G显存本地玩转4K AI视频:ComfyUI工作流拆解与优化实战
  • 无名杀三步跑起来:在浏览器里直接玩的三国杀网页版
  • SnowBamboo位图字体生成器:浏览器里出位图字体,6种格式直通游戏引擎
  • 微信朋友圈导出工具 WechatMoments:把朋友圈备份成HTML的实操教程
  • 从GitHub中断看系统扩展:超越反应式,构建预测与主动弹性策略
  • MetalLB 负载均衡器 v0.14 升级避坑指南:配置迁移前必须处理的 3 个破坏性变更
  • PaddleOCR移动端部署完整指南:4步走通端侧OCR最短路径
  • 一文搞懂 Flink Web UI:任务监控与性能分析完整指南
  • 深入解析var关键字:跨语言面试题与工程实践