资讯动态

uC/OS-II信号量源码精读:从ECB到位图,理解内核同步本质

发布时间:2026/9/9 8:09:59 来源:尧图企业网站定制
如果你在项目里用过uC/OS-II的信号量大概率有过这种经历查了无数帖子、反复对照Demo工程以为自己已经把API用明白了结果程序一跑还是偶发卡死、偶发丢事件。我当年调一个双串口数据分发的小项目两个任务抢同一个外设加了信号量还是莫名丢数据最后把源码翻到os_sem.c和os_core.c里才彻底搞明白问题根本不在“有没有加锁”而在“信号量内部那张等待表是怎么被内核维护的”。这篇是uC/OS-II内核源码精读系列的第4篇。前面我们已经把启动流程、任务控制块、就绪表和调度器的主线聊了一遍这一篇我准备把信号量这个最常用的同步原语彻底拆开。重点不是你怎么调OSSemPend/OSSemPost这两个API而是这两个API背后到底摸了哪些数据结构、为什么拿不到信号量的任务会被“挂到墙上”、为什么释放信号量之后不一定立刻发生任务切换。搞懂这些你再回头看RTOS面试题里那些“信号量与互斥量区别”“Pend为什么不能放中断里”之类的问题基本就不用背了。如果你是刚接触嵌入式内核源码的初学者建议先把前面那几份就绪表和TCB的笔记过一遍这一篇会大量复用之前讲过的OSRdyTbl、OSTCBCur、OS_TASK_SW这些概念。如果你只想解决实际问题那直接从第5节的故障排查和面试考点看起也行但说实话不看原理直接查问题你连从哪下手都不知道。1. 信号量的本质一个计数器和一张等待位图1.1 信号量到底在管理什么先别急着抠代码。你要理解信号量得先有一个统一心理模型信号量 一个计数器 一张“谁在等”的名单。计数器表示当前还有多少份“资源”可用。任务调用OSSemPend就是申请一份资源有计数器减一任务带着资源继续跑没有任务从“就绪名单”挪到“等待名单”里内核转去跑别的任务。任务调用OSSemPost就是归还一份资源有人等着就把等待名单里优先级最高的那个人拉回就绪名单没人等着计数器加一把资源攒起来。这个模型本身就解释了二值信号量和计数信号量的差别。二值信号量就是计数器初始值只有0或1典型的用法是“事件已经发生”和“事件未发生”计数信号量初始值是N典型用法是“还有N个空闲缓冲区可用”。uC/OS-II源码里并没有为二值信号量和计数信号量单独设计两套结构只是在创建时cnt初始化不一样后面所有动作完全相同。很多初学者在这里会有个误区以为信号量是个“锁”Post之后另一个任务必须立刻拿到。其实在源码里OSSemPost干的第一件事是先看等待名单里有没有人没有人的话只是默默把OSEventCnt然后返回。这个“默默加一”的动作后面会成为很多隐蔽bug的来源特别是中断里Post、任务里Pend这种异步场景。1.2 一张位图代替链表是uC/OS-II最经典的设计之一如果让我选uC/OS-II源码里最值得反复品味的一段我一定会选它的事件等待表。其他RTOS比如FreeRTOS等待某个信号量的任务是用链表串起来的uC/OS-II用的是位图。位图的好处有两个。第一是快查“谁在等”不是遍历链表而是查表 位运算几条指令就出来了时间开销是确定的O(1)对实时系统来说确定性比平均快更重要。第二是省内存一个任务只需要一个bit位来表示“我在不在等”64个任务就是8字节OSEventTbl[8] 1字节OSEventGrp。但位图也有位图的代价它不适用于“等待任务数量非常多且动态变化”的场景因为位图的大小在编译期就由OS_LOWEST_PRIO定死了。在uC/OS-II的年代这种取舍是合理的追求的是极简和确定。阅读内核源码时不能只看它“做了什么”还要体会它“为什么这么做”这种设计思路本身就是你以后写精简代码的养料。这里我需要先建立一个关键概念uC/OS-II内部有两张长得几乎一模一样的位图。全局就绪表OSRdyGrpOSRdyTbl[]管的是“系统中哪些任务当前可以运行”。每个事件各自的等待表OSEventGrpOSEventTbl[]管的是“哪些任务正在等这个事件”。两张表操作逻辑完全一样都是“把任务对应位置1、清0、查最低置位”区别只是挂在谁身上。后面读OS_EventTaskWait和OS_EventTaskRdy时你会看到它们做的事情就是在这两张表之间把任务挪来挪去。理解了这个“挪动”的过程信号量60%的内容你就吃透了。1.3 一个结构体统一三种事件代码量就是这么省下来的打开uCOS_II.H搜OS_EVENT你会看到这样一个结构体typedef struct os_event { INT8U OSEventGrp; /* 等待任务组位图 */ INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待任务表 */ INT16U OSEventCnt; /* 信号量计数器仅信号量使用 */ void *OSEventPtr; /* 指向消息或消息队列 */ INT8U OSEventType; /* 事件类型 */ } OS_EVENT;注意这个结构体同时服务信号量、互斥量、邮箱、队列。信号量用OSEventCnt存计数器邮箱和队列用OSEventPtr存消息指针OSEventType字段标记当前这个控制块到底是哪种事件。uC/OS-II把这种统一抽象叫做“事件控制块”ECB。这个设计的直接收益是OSSemPend、OSMutexPend、OSMboxPend、OSQPend它们的Pend流程几乎共用同一套核心逻辑和同一个内部函数OS_EventTaskWait。内核源码少了出bug的面也小了对于只能放到芯片内部Flash跑的RTOS来说这种代码紧凑度非常重要。ECB的数量由OS_MAX_EVENTS决定在OS_CFG.H里配置。OSInit()启动时会把所有ECB串成一个空闲链表OSEventFreeList。你每次调用OSSemCreate就是从这条空闲链表上摘一个节点下来删除信号量时再把它还回去。这也是为什么代码里查空余链表、临界区保护、OSEventFreeList的操作会反复出现它就是一套“内存池”只是用链表形式管理。2. 从OSSemCreate看ECB与空闲链表管理2.1 创建信号量的完整动作信号量创建函数不长真正做事的代码加起来十行左右但它把所有内核管理细节都包了进去OS_EVENT *OSSemCreate (INT16U cnt) { OS_EVENT *pevent; OS_ENTER_CRITICAL(); pevent OSEventFreeList; /* 从空闲链表取一个ECB */ 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里取节点。为什么取节点要进临界区因为OSEventFreeList是全局变量如果此时发生中断而中断里恰好也创建或删除了一个事件对象两个上下文同时改这个链表链表就断了。uC/OS-II管这种共享数据结构的保护叫“临界区”大部分源码里就是一句话的事但这句话背后对应的是关中断或锁调度具体选哪种由OS_CRITICAL_METHOD宏决定。第二步是初始化ECB的各个字段。这里有个细节OSEventType不只为了标识类型它还承担着“参数校验”的功能。你去看OSSemPend的第一段代码它一定会检查pevent-OSEventType OS_EVENT_TYPE_SEM如果不等于说明你传错指针了比如把一个邮箱的ECB传给了信号量函数。内核返回OS_ERR_EVENT_TYPE而不是拿着错误的内存瞎操作。这种防御式编程读起来朴实无华但在实际项目中救过我好几次特别是重构代码时手一抖把全局变量换错的情况。2.2 OS_EventWaitListInit等待表为什么必须每次清零每个ECB在OSInit()阶段都只是从空闲链表上串起来的孤儿节点里面的OSEventGrp、OSEventTbl[]很可能残留着上次使用时的数据。所以创建信号量时必须用OS_EventWaitListInit把它们全部清零否则新信号量会莫名其妙带着一堆“等待任务”。void OS_EventWaitListInit (OS_EVENT *pevent) { INT8U i; pevent-OSEventGrp 0; for (i 0; i OS_EVENT_TBL_SIZE; i) { pevent-OSEventTbl[i] 0; } }有很多刚接触源码的人会问为什么不直接用memset原因不复杂uC/OS-II的定位是跨平台可移植的RTOS它只依赖标准C里最基础的语法而且memset这种库函数在一些裁剪版C库里的实现效率未必理想。内核作者连这么小的开销都要自己控制这种“能不依赖库函数就不依赖”的代码洁癖在你写嵌入式底层模块时非常值得模仿。2.3 创建信号量时初始值到底该怎么给这个属于“代码之外的江湖经验”。cnt初始值是0还是1直接决定了信号量的语义初始值为0典型的事件通知。创建后Pend方必定阻塞直到某个地方Post一下。比如外设中断要通知任务“数据到了”信号量初始给0中断里Post一次任务就从阻塞中醒来。初始值为1典型的互斥访问。虽然从语义上能实现“锁”但uC/OS-II里对互斥访问更推荐用OSMutexCreate因为它带了优先级继承能缓解优先级反转问题。初始值为N典型的有界资源管理。比如有4个DMA缓冲区信号量初始给4任务每次申请一个缓冲区就Pend一次用完了Post归还。我见过不少项目把“事件通知”和“资源计数”两种场景混用然后去查为什么逻辑乱了。每次排查到最后都是开头OSSemCreate(0)和OSSemCreate(1)这一步就选错了。所以在写代码之前先想清楚这个信号量是“发信号”还是“数资源”再决定初始值。3. OSSemPend拿不到就睡觉睡完还得弄清楚怎么醒的3.1 一个if分支把“有资源”和“没资源”分开了OSSemPend是信号量机制里逻辑最重的函数它处理的事情很多尝试获取、拿不到就阻塞自己、等超时、然后被唤醒后判断自己是怎么醒的。我们按代码执行顺序来拆。void OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *perr) { OS_ENTER_CRITICAL(); if (pevent-OSEventCnt 0) { /* 有资源 */ pevent-OSEventCnt--; /* 消耗一个 */ OS_EXIT_CRITICAL(); *perr OS_ERR_NONE; return; } /* 没资源开始阻塞当前任务 */ OSTCBCur-OSTCBStat | OS_STAT_SEM; OSTCBCur-OSTCBPendTO OS_FALSE; OSTCBCur-OSTCBDly timeout; OS_EventTaskWait(pevent); /* 从就绪表摘除挂到等待表 */ OS_EXIT_CRITICAL(); OSSched(); /* 让出CPU */ OS_ENTER_CRITICAL(); switch (OSTCBCur-OSTCBStat) { /* 醒来后检查状态 */ case OS_STAT_RDY: ... /* 拿到了 */ case OS_STAT_SEM: ... /* 超时了 */ ... } OS_EXIT_CRITICAL(); }注意第一个分支如果信号量还有资源任务拿到资源后直接return完全不会触发调度。这个动作是“非阻塞”的开销极小。很多性能敏感的代码路径里资源充足时整个Pend过程就是关几次中断、减一个数而已这就是内核级同步比普通开关中断更精细的原因。3.2 没资源时当前任务是怎么“挂”上去的真正精彩的部分在OS_EventTaskWait。它实现了我们前面说的“从就绪表挪到等待表”。void OS_EventTaskWait (OS_EVENT *pevent) { INT8U y; OSTCBCur-OSTCBEventPtr pevent; y OSTCBCur-OSTCBY; pevent-OSEventTbl[y] | OSTCBCur-OSTCBBitX; /* 挂到事件等待表 */ pevent-OSEventGrp | OSTCBCur-OSTCBBitY; OSRdyTbl[y] ~OSTCBCur-OSTCBBitX; /* 从全局就绪表摘除 */ if (OSRdyTbl[y] 0) { OSRdyGrp ~OSTCBCur-OSTCBBitY; } }你会发现这里根本不用计算优先级和位的映射关系因为TCB里早就缓存好了。回想一下任务创建时设置过什么OSTCBY是优先级的高3位OSTCBBitY是对应OSRdyGrp里的掩码位OSTCBX是优先级的低3位OSTCBBitX是对应OSRdyTbl[y]里的掩码位。这些字段在任务初始化时就准备好了所以OS_EventTaskWait才能这么干净利落。这一步做完之后当前任务已经从“我能跑”变成了“我在等”。此时必须调OSSched()让出CPU。OSSched()做的事情是先看有没有比当前任务更高优先级的就绪任务如果有把OSTCBHighRdy指过去然后设置OS_TASK_SW触发上下文切换。因为当前任务已经被摘出就绪表调度器几乎必然会选中别的任务。3.3 任务被唤醒后我是拿到资源还是超时了任务再次被调度回来时会进入OSSemPend后半段。这段代码要看OSTCBStat和OSTCBPendTO的组合情况这也是很多人读源码时容易绕晕的地方。OSTCBStat OS_STAT_RDY说明任务已经恢复成就绪态而且是正常拿到了信号量perr置OS_ERR_NONE。OSTCBStat还带着OS_STAT_SEM且OSTCBPendTO TRUE说明是超时唤醒的perr置OS_ERR_TIMEOUT。内核会在超时处理里把任务从等待表里摘出去并把OSEventTbl、OSEventGrp清理干净。OSTCBPendTO FALSE但OSTCBStat还是OS_STAT_SEM说明任务等待的事件被显式删除比如OSSemDel此时perr置OS_ERR_PEND_ABORT。这个状态机的妙处在于任务醒来时不用去翻“我是谁唤醒的”这种日志只需要看两个标志位就知道自己为什么会处于当前状态。这也是为什么我说uC/OS-II的代码是“读得懂的代码”它把复杂的状态收敛成了几个bit。这里有一个非常容易踩的坑timeout参数不要乱传0。0表示“永远等下去直到事件发生”。如果你的系统里Post方因为某种bug一直没执行任务就会永久阻塞看起来整个系统像死了。排查这种问题最简单的办法是给timeout设一个较大值跑一段时间后用调试器看任务状态就能发现哪个任务卡在Pend上。不要问我为什么知道我曾经在一台设备上抓了一下午“死机”问题最后发现是某个任务的Pend超时给成了0而Post方在初始化时被另一个任务抢先阻塞了。3.4 两个位图的对照是所有理解的关键到这里我们就能把前面两张位图正式放到一起看。项目全局就绪表事件等待表变量OSRdyGrpOSRdyTbl[]OSEventGrpOSEventTbl[]管理谁的运行状态系统中所有任务只等待该事件的任务任务进表动作OSRdyTbl[y] bitx任务出表动作OSRdyTbl[y] ~bitxOSEventTbl[y] ~bitx谁是“主人”内核全局某个ECB实例OSSemPend把任务从右表挪到左表不是反过来的Pend把任务从左表能跑挪到右表在等Post把任务从右表挪回左表。理解这张表的双向移动你就能理解os_core.c里那些OS_EventTaskRdy、OS_EventTaskWait到底在干嘛了。4. OSSemPost唤醒谁怎么唤醒唤醒完要不要切换4.1 OS_LowestBitPos为什么最低位置就是最高优先级OSSemPost的流程和Pend正好镜像INT8U OSSemPost (OS_EVENT *pevent) { OS_ENTER_CRITICAL(); if (pevent-OSEventGrp ! 0) { /* 有人等在等待表里 */ OS_EventTaskRdy(pevent, (void *)0, OS_STAT_SEM); OS_EXIT_CRITICAL(); OSSched(); return (OS_ERR_NONE); } if (pevent-OSEventCnt 65535u) { /* 没人等累加计数 */ pevent-OSEventCnt; } OS_EXIT_CRITICAL(); return (OS_ERR_NONE); }那个if (pevent-OSEventGrp ! 0)是整个函数的决策点等待表非空说明有任务在等那这次Post的意义就是唤醒其中一个人等待表为空说明没人要任务只把资源数加上去。如果等待表非空内核要选“谁先醒”。uC/OS-II的优先级数字越小优先级越高所以OSEventGrp里数值最低的置位位对应的就是最高优先级的等待组。OS_LowestBitPos[]是一张查找表传入一个8位数值返回最低置位位的位置0-7。比如传入0x10二进制00010000返回4传入0x2100100001返回0。为什么要用查表而不是循环判断因为实时内核里这一步在每次Post时都要执行如果写成for循环从bit0挨个查到bit7虽然最多也就8次可累积起来开销仍然可观。查表是拿ROM空间换运行时间一次索引就定位。这种空间换时间的思路在老的RTOS源码里到处都是是嵌入式优化的重要思想。4.2 OS_EventTaskRdy把等待者物归原主选中了最高优先级等待者之后OS_EventTaskRdy负责把它从等待表搬回就绪表void OS_EventTaskRdy (OS_EVENT *pevent, void *msg, INT8U msk) { INT8U y; INT8U x; INT8U bitx; INT8U bity; y OS_LowestBitPos[pevent-OSEventGrp]; bity OSMapTbl[y]; x OS_LowestBitPos[pevent-OSEventTbl[y]]; bitx OSMapTbl[x]; ptcb OSTCBTbl[((INT8U)((y 3) x))]; /* 由位图位置还原优先级 */ ptcb-OSTCBDly 0; ptcb-OSTCBStat ~msk; /* 清掉等待信号量标志 */ if (ptcb-OSTCBStat OS_STAT_RDY) { OSRdyGrp | bity; /* 回就绪表 */ OSRdyTbl[y] | bitx; } pevent-OSEventTbl[y] ~bitx; /* 从等待表摘除 */ if (pevent-OSEventTbl[y] 0) { pevent-OSEventGrp ~bity; } }注意中间那行OSTCBTbl[((y 3) x)]这是直接通过数组索引定位TCB。因为uC/OS-II每个优先级只允许一个任务所以优先级号就是任务在OSTCBTbl[]里的下标。这也是uC/OS-II和FreeRTOS的显著差别uC/OS-II同一优先级下不能有多个任务简化了“查最高优先级任务”这件事代价是灵活性受限。函数把等待者从事件等待表里摘除后还要检查它现在是不是完全就绪了如果OSTCBStat OS_STAT_RDY说明任务不等待任何其他事件了放回就绪表如果它还等着别的资源就算被这个Post唤醒了也不能放入就绪表免得它被调度器选中后又发现自己还缺别的资源导致空转。这个细节特别容易被忽略面试时如果被问到“一个任务同时等信号量和邮箱会发生什么”答案就在这里。4.3 Post之后为什么不一定会立刻切换OSSemPost里OS_EventTaskRdy执行完后马上调OSSched()。很多人以为这种“马上调度”就是立刻切到新任务其实不是。OSSched()的逻辑是如果当前不在中断里、并且没有锁定调度器并且OSTCBHighRdy不等于OSTCBCur才触发OS_TASK_SW。如果你的当前任务优先级比被唤醒的任务更高或者调度器被OSLockNesting锁住了这次Post就不会导致任何切换。如果Post发生在中断处理函数里那情况更像“欠着一笔账”中断里Post只是把任务放回就绪表真正切换发生在中断退出时OSIntExit的调度逻辑里而且中断嵌套时还要等所有中断都退出后才切。这是RTOS的经典做法中断上下文不该做上下文切换否则栈和嵌套状态会乱。你在项目中如果发现“中断里Post了外面任务怎么还没跑”先检查是不是有别的更高优先级任务占着CPU再检查中断嵌套计数最后再怀疑信号量本身。4.4 计数器上限与二值信号量行为当等待表为空时OSSemPost会对OSEventCnt加1但有个保护OSEventCnt 65535u才加。因为OSEventCnt是16位无符号整数超过上限会回绕成0这在资源计数场景下是灾难性的。所以写应用代码时如果你用信号量数资源要保证“Post不会比Pend多”反过来二值信号量因为初始值是1Post一次后计数变成1再Post多少次都还是1这种“重复Post不积累信号”的特性有时候是便利有时候是坑取决于你的场景。5. 源码之外常见问题与RTOS面试考点实录5.1 我把调试中踩过的几个经典故障整理成了一张表故障现象排查思路根因方向中断里Post任务没有立刻执行先查是否有更高优先级任务在跑、调度器是否被锁、是否中断嵌套期望“立即切换”一般就是要改设计Post后只能保证“尽快”切换一个任务Pend后永远醒不来打印或调试器查看该任务OSTCBStat、OSTCBPendTO确认它到底卡在哪个事件检查Post方是否真的执行了、事件指针是否传递正确、timeout是否为0且Post方被别的任务卡死创建多个信号量后系统启动异常检查OS_MAX_EVENTS是否够用、ECB空闲链表是否被越界写坏通常不是信号量问题而是ECB附近内存被踩了用MPU或写保护辅助定位信号量计数变成诡异的大数字全局搜索这个ECB被哪些任务/中断读写检查是否存在重复Post二值信号量被多次Post或者Multicore如果做了扩展下共享变量没保护Pend在中断里导致系统崩溃看是否有非法调用OSSemPend的路径uC/OS-II明确不允许在ISR里Pend需要改成Post 任务里Pend第一类问题最常见。很多从裸机开发转过来的朋友天然觉得“我Post了你就要马上跑”但RTOS里的调度是优先级驱动的不是事件驱动的。如果你确实需要“中断后立刻处理某件高优先级的事”应该把中断里处理的事情交给中断里能做的部分把耗时逻辑放到底层任务里并用信号量通知它。第二类问题我要多说一句调试任务卡死时我一般会先看任务在当前状态下OSTCBStat的值。0x00是就绪带OS_STAT_SEM0x02就是正在等信号量带OS_STAT_MBOX0x04就是正在等邮箱带OS_STAT_SUSPEND0x08是被挂起。从数值反推状态比在代码里到处加打印高效得多。5.2 RTOS面试题用源码知识回答才不会被问倒很多面试题表面在问概念实际在问源码实现信号量和互斥量的区别是什么面试官想听的不只是“互斥量有优先级继承”。你要能说出来uC/OS-II里普通信号量OSSemPost唤醒的只是“最高优先级等待者”但持有信号量的低优先级任务可能被高优先级任务抢占导致中优先级任务饿死这就是优先级反转。互斥量OSMutexPend内部实现了优先级提升低优先级任务持有互斥量时会临时把自己的优先级提到等待者中最高的那个等释放后再降回来。为什么OSSemPend不能在中断里调用因为阻塞需要把当前任务从就绪表摘除并让出CPU中断没有独立的“任务上下文”而且中断处理应该短小精悍不应该有等待语义。源码层面OSSemPend后半段的OSSched()和OS_TASK_SW依赖任务栈做上下文切换在中断里做这件事会破坏中断嵌套机制。uC/OS-II如何快速找到最高优先级任务答案不再是“查就绪表”而是OS_LowestBitPos查表法。先查OSRdyGrp最低置位得到组号y再查OSRdyTbl[y]最低置位得到x最高优先级 (y 3) x。整个过程是查表 位运算没有循环。为什么说uC/OS-II的优先级是静态的因为TCB数组下标直接等于优先级同一个优先级不允许有两个任务。这在设计上牺牲了灵活性换来了确定性和极简实现。5.3 读uC/OS-II源码时推荐的两条主线如果你刚拿到源码不知道怎么读我建议按这两条主线走。第一条主线是“一个任务的一生”OSInit→OSTaskCreate→OSStart→OSSched→ 任务A运行 →OSSemPend→OS_EventTaskWait→ 任务B运行 →OSSemPost→OS_EventTaskRdy→ 任务A继续跑。把这条路径上的函数全部打开全局变量每次变化都记下来你基本就掌握了内核大多数联动关系。第二条主线是“一个事件的一生”OSSemCreate→ 空闲链表摘节点 →OSEventCnt初始化 →OSSemPend挂任务 →OSSemPost唤醒任务 →OSSemDel删除事件 → 归还空闲链表。这条线能帮你理解为什么ECB必须用完后清理干净为什么OSEventType能当校验字段用。我自己读的时候习惯在纸上画两个框左边画OSRdyGrp/OSRdyTbl右边画OSEventGrp/OSEventTbl每次碰到OS_EventTaskWait就往左框划一条箭头到右框碰到OS_EventTaskRdy就往回划。画完一遍你就不会再把这两张表搞混了。6. 给真正想动手验证的人一个小实验理论讲再多不如自己动手跑一遍。你可以拿任意一块支持uC/OS-II移植的开发板建两个任务任务A优先级10任务B优先级11。任务B启动后执行OSSemPend(sem, 0, err)也就是永远等待。然后在任务A里每隔一段时间执行OSSemPost(sem)。在任务B被唤醒的那个瞬间用调试器把OSEventGrp、OSEventTbl[0]、OSRdyGrp、OSRdyTbl[0]四个变量全部拉出来看一眼。你会发现一个很有意思的现象任务B在等待时OSEventTbl[0]对应的位是1OSRdyTbl[0]对应的位是0被唤醒后两个位完全反过来。等你能不看源码就预判出这几个bit的状态时信号量这部分你就算真读懂了。再把timeout设成5个时钟节拍把Post方注释掉观察任务醒来时OSTCBPendTO的值从0变1的过程。这个实验做一遍比背二十道面试题都管用。我在实际项目里体会最深的一件事是uC/OS-II的API文档把所有函数都写得明明白白但真正决定你系统稳不稳的往往是那些文档里不会写的东西——两个位图的切换时机、Pend和Post在不同上下文里的调度差异、超时状态机的三个分支。这些东西不在API层而在源码层。所以我才一直建议组里的新人别急着在项目里堆功能先把内核源码啃一啃不用全懂把任务管理和信号量这两块读懂应付日常开发绰绰有余。下一篇文章如果你还愿意看我会顺着ECB这条线继续往里挖把邮箱和消息队列的实现和信号量做一次对比。你会发现uC/OS-II的邮箱和队列骨干逻辑和信号量几乎是一个模子刻出来的只是在OSEventPtr上多了个消息指针的操作而已。内核这东西一旦找到规律后面就是举一反三的事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价