ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

uC/OS-II内核精读:事件控制块如何驱动信号量与互斥锁

uC/OS-II内核精读:事件控制块如何驱动信号量与互斥锁 这一篇文章我打算换一个切入点。前几篇我们反复在啃 uC/OS-II 的任务控制块、就绪表和调度器说白了就是在做同一件事把一个任务从“创建”推到“就绪表”再从“就绪表”推到 CPU 上跑起来。这一篇我们暂时把调度器放一边去看另一张表——事件控制块OS_EVENT的等待表。信号量、互斥锁、邮箱、消息队列这四类同步原语在源码里全都依赖这张表工作。你想真正看懂一个 RTOS而不是停留在 API 调用层面ECB 这一块绕不过去。“6736 行”这个数字很多人以为是官方给出的准确代码行数其实更像是我们手工统计版本时的一个口径。不同版本、不同条件编译下行数会有几十行甚至几百行的波动。我更在意的不是这个数字本身而是一个结论一个商业级 RTOS 的内核核心代码真的可以逐行读完。这篇文章就适合正在啃嵌入式内核源码、准备 RTOS 面试或者只写过 hello world 级别例程、想再往底层走一步的读者。1. 先从文件划分看懂 6736 行的组织逻辑1.1 核心内核、可选组件与移植层分别是谁拿到一份 uC/OS-II 源码第一眼最容易懵的不是代码而是文件太多。其实归类之后就三条线类别代表文件作用核心内核OS_CORE.C、OS_TASK.C、OS_TIME.C、OS_SEM.C、OS_MUTEX.C、OS_MBOX.C、OS_Q.C、OS_MEM.C、uC/OS-II.H任务管理、调度、同步互斥、时间管理、内存管理配置与头文件OS_CFG.H、INCLUDES.H、UCOS_II.H定义功能开关、事件数上限、优先级数移植层os_cpu.h、os_cpu_a.asm、os_cpu_c.c与具体 MCU 架构相关的底层实现可选组件OS_TMR.C、OS_FLAG.C、OS_STAT.C定时器、事件标志组、统计任务我统计的时候通常只算核心内核文件加必要头文件所以 6736 行这个数就是把 OS_CORE.C、OS_TASK.C、OS_SEM.C 这一串加起来的结果。如果你把移植层也加进去行数会明显变多如果把可选组件全打开又会再多出上千行。所以看源码先要有边界感问自己一句我到底要看哪一层1.2 OS_EVENT 在源码里是一条主线uC/OS-II 里最反常的设计就是信号量、互斥锁、邮箱和消息队列这四种看起来完全不同的同步对象底层用的却是同一个数据结构OS_EVENT。定义在 uC/OS-II.H 里真正操作这个结构体的基础函数只有三个OS_EventTaskWait()让当前任务进入事件等待表。OS_EventTaskRdy()把等待表中最高优先级任务唤醒并放回就绪表。OS_EventTO()超时时把任务从等待表中摘除。这三个函数都放在 OS_CORE.C 里而 OS_SEM.C、OS_MUTEX.C、OS_MBOX.C、OS_Q.C 这四个文件的所有逻辑几乎都是在围绕 OS_EVENT 做文章。所以你只要理解了 OS_EVENT 这个结构体再加上这三个基础函数就已经把四个源码文件的主线吃透了。这就是我建议源码精读按“结构体 → 基础函数 → 信号量 → 互斥锁”顺序推进的原因。2. OS_EVENT 结构体拆解一个结构体如何承载四种同步原语2.1 有了 TCB为什么还需要 ECB每个任务都有自己的 TCB任务控制块里面保存了栈指针、任务状态、优先级、延时值这些信息。TCB 解决的是“任务自己是什么状态”的问题。但任务之间要协作的时候一个任务需要知道“我正在等谁”“谁也在等同一个东西”这些信息跨任务存在TCB 记录不了于是就需要一个中间对象事件控制块。我习惯把 OS_EVENT 理解成咖啡店里的取餐号码。每个顾客是一个任务TCB 是顾客自己的信息而取餐号码牌把“这位顾客在等哪杯咖啡”这件事登记在公共台面上。当咖啡做好时店员看一眼排队号码喊下一个对应到内核里就是OS_EventTaskRdy()从等待表里挑出下一个任务。如果不用 ECБ用裸机编程里的全局标志位做任务同步会马上撞上几个问题任务只能轮询标志位白白烧 CPU。标志位只能表达“有没有”表达不了“有多少个资源可用”。多个任务等同一个资源时不知道谁先谁后优先级无法介入。超时等待基本实现不了。OS_EVENT 就是为了解决这些问题才存在的。2.2 结构体逐字段拆解先看这个结构体不同版本字段几乎一样我以最常见的定义来写typedef struct os_event { INT8U OSEventType; /* 事件类型 */ INT8U OSEventGrp; /* 等待任务组位图 */ INT16U OSEventCnt; /* 信号量计数 / 互斥锁状态 */ void *OSEventPtr; /* 指向扩展结构 */ INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待任务表 */ } OS_EVENT;OSEventType就是身份证标记这个事件是信号量、互斥锁、邮箱还是队列。常见值有OS_EVENT_TYPE_SEM、OS_EVENT_TYPE_MUTEX、OS_EVENT_TYPE_MBOX、OS_EVENT_TYPE_Q。它存在的意义不只是为了诊断很多内核代码靠它来判断参数合法性防止你把一个信号量句柄误传给了邮箱函数。OSEventGrp和OSEventTbl[]组合起来就是等待任务表本质是一个最大支持 64 个任务的紧凑位图。理解这张表和理解就绪表几乎一样因为设计思路完全相同。后面第三节专门展开。OSEventCnt是 16 位整型这是整个结构体最精华也最容易让人困惑的字段。在不同原语里它的含义完全不一样原语类型OSEventCnt 的用法OSEventPtr 的用法信号量当作计数值用每次 Pend 减一Post 加一不使用保持为 NULL互斥锁低 8 位表示锁是否可用高 8 位参与优先级恢复指向当前持锁任务的 TCB邮箱不使用指向消息的指针地址消息队列不使用指向 OS_Q 结构体OSEventPtr是一个通用指针内核通过它把事件对象和一个更复杂的数据结构关联起来。邮箱用它保存消息指针的地址队列用它指向 OS_Q互斥锁用它指向当前持有锁的 TCB。为什么用void*而不是直接写死类型因为代码复用的关键是类型无关内核只负责把这个指针存储下来具体解释权交给上层模块。还有一个容易被误解的点uC/OS-II 的事件标志组OS_FLAG_GRP不属于OS_EVENT体系。因为事件标志组要同时判断多个标志位的组合等待逻辑不是一个简单的位图能表达的所以单独设计了结构。面试时如果说“uC/OS-II 所有同步都用 OS_EVENT”这是不严谨的。3. 等待表怎么“找人”位图查询与三个底层函数3.1 等待表的读写规则uC/OS-II 设计时面向的是 8/16 位 MCU资源非常紧张所以它没有用链表来维护等待队列而是用位图。假设系统最大支持 64 个任务那么等待表就用一个 8 字节数组OSEventTbl[8]来表示 64 个 bitOSEventGrp用 8 个 bit 表示哪一组里至少有一个任务在等待。把当前任务挂进等待表的操作核心代码就两行pevent-OSEventGrp | OSTCBCur-OSTCBY; pevent-OSEventTbl[OSTCBCur-OSTCBY] | OSTCBCur-OSTCBBitX;OSTCBY和OSTCBBitX是 TCB 里早就算好的位图索引。OSTCBY表示任务优先级低 3 位之外的索引OSTCBBitX表示该任务在组内的位。这个设计跟就绪表OSRdyGrp/OSRdyTbl几乎一模一样所以我常说看懂一张表另一张表就白送了。从等待表摘除任务时有一个特别容易写错的地方。很多人只清OSEventTbl[]里的位清完发现OSEventGrp对应组位还挂着导致后续查找时走进一个看起来有人等、实际没人等的空组。正确做法是清完表的位后检查整组是否变成 0如果变成 0 就把OSEventGrp里的对应组位也清掉pevent-OSEventTbl[OSTCBCur-OSTCBY] ~OSTCBCur-OSTCBBitX; if (pevent-OSEventTbl[OSTCBCur-OSTCBY] 0u) { pevent-OSEventGrp ~OSTCBCur-OSTCBY; }这个检查丢了表面上程序还能跑但一旦某组最后一个等待者被移除后续所有查找都会读到脏数据表现成偶发性调度失败。这是移植 uC/OS-II 时最高频的隐性 Bug 之一。3.2 OSUnMapTbl一次查表找出最高优先级等待者要从等待表里找到优先级最高的任务uC/OS-II 没有用 for 循环扫描而是用了一张 256 字节的常量表OSUnMapTbl。查找代码长这样y OSUnMapTbl[pevent-OSEventGrp]; x OSUnMapTbl[pevent-OSEventTbl[y]]; prio (y 3) x;OSUnMapTbl的作用是给定一个 8 位字节返回这个字节里最低的置 1 位的位置。在 uC/OS-II 的优先级规则里数值越小优先级越高所以最低的置 1 位就是最高优先级任务所在的位置。为什么用查表而不是用编译器内置的位扫描指令原因很现实uC/OS-II 出生的年代各家编译器对__builtin_ctz这类内置函数的支持参差不齐而且早期 8 位 MCU 的指令集里也不一定有对应的位扫描指令。查表法虽然多占 256 字节 ROM但换来的是确定性的执行时间而且完全可移植。很多现代 RTOS 还采用位图 查表或者位图 CPU 指令加速思路一脉相承。3.3 三个基础函数的角色把这三个函数放在一起看就是一个完整的“等待与唤醒”闭环OS_EventTaskWait()是把当前任务放上等待表同时从就绪表里摘掉。要注意这个函数不会主动调度调用方在退出临界区后需要手动调用OSSched()让出 CPU。OS_EventTaskRdy()做的事情恰好相反从等待表里找出最高优先级任务把它从等待表摘除重新放回就绪表然后清掉它的等待状态。这个函数同样不负责调度真正的调度由 Post 函数在退出临界区后触发。OS_EventTO()处理的是超时场景。任务 Pend 一个事件并给了超时值但迟迟没有等到 Post系统节拍中断扫描到该任务超时了就要把任务从等待表里摘走并标记超时错误。摘除时的组位检查在这里尤其重要。我把这三个函数类比成机场航班的三个环节OS_EventTaskWait是进候机室登记OS_EventTaskRdy是登机口叫号OS_EventTO是航班延误后把没登机的旅客从候机名单上划掉。理清这个流程下面再看信号量的 Pend 和 Post就会觉得很顺。4. 信号量源码精读OSSemPend 与 OSSemPost 之间发生了什么4.1 创建与获取计数型信号量的两条分支信号量模块在整个内核里算是最好读的文件之一。创建函数的核心就是把事件类型设为信号量并把计数值写进OSEventCntOS_EVENT *OSSemCreate (INT16U cnt) { OS_EVENT *pevent NULL; // 从空闲事件块链表取一个 OS_EVENT pevent OSEventFreeList; OSEventFreeList (OS_EVENT *)pevent-OSEventPtr; pevent-OSEventType OS_EVENT_TYPE_SEM; pevent-OSEventCnt cnt; pevent-OSEventPtr NULL; return pevent; }真正复杂的是OSSemPend()。我尽量用接近源码的思路把它简化成关键逻辑void OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *perr) { OS_ENTER_CRITICAL(); if (pevent-OSEventCnt 0u) { pevent-OSEventCnt--; OS_EXIT_CRITICAL(); *perr OS_ERR_NONE; } else { OSTCBCur-OSTCBStat | OS_STAT_SEM; OSTCBCur-OSTCBEventPtr pevent; OS_EventTaskWait(pevent); OS_EXIT_CRITICAL(); OSSched(); // 调度回来后检查是被 Post 唤醒还是超时唤醒 if (OSTCBCur-OSTCBStat OS_STAT_SEM) { OS_EventTO(pevent); OS_EXIT_CRITICAL(); *perr OS_ERR_TIMEOUT; } else { OSTCBCur-OSTCBEventPtr NULL; OS_EXIT_CRITICAL(); *perr OS_ERR_NONE; } } }看到没有Pend 的逻辑就是两条分支计数大于 0直接减一返回计数等于 0任务登记到等待表并从就绪表摘除然后触发调度。这里特别值得展开的是临界区。OSEventCnt是 16 位变量在 8 位 MCU 上读写一个 16 位变量不可能一条指令完成如果不关中断可能读到“高字节已经变、低字节还没变”的半新半旧值。uC/OS-II 使用的锁就是最简单的关中断因为单核 MCU 上关中断就等于锁住了整个 CPU这是最简单也最可靠的方案。代价是临界区代码必须短否则会拉长中断响应时间。4.2 释放与唤醒为什么 Post 一次只叫醒一个任务OSSemPost()的逻辑也很有意思INT8U OSSemPost (OS_EVENT *pevent) { OS_ENTER_CRITICAL(); if (pevent-OSEventGrp ! 0u) { OS_EventTaskRdy(pevent, NULL, OS_STAT_SEM, OS_STAT_PEND_OK); OS_EXIT_CRITICAL(); OSSched(); return OS_ERR_NONE; } if (pevent-OSEventCnt 65535u) { pevent-OSEventCnt; OS_EXIT_CRITICAL(); return OS_ERR_NONE; } OS_EXIT_CRITICAL(); return OS_ERR_SEM_OVF; }这里最关键的是顺序先检查等待表有没有人如果有人直接唤醒最高优先级等待者只有当等待表全空时才把计数值加一。为什么不能先加一再去查等待表因为信号量代表可用资源的数量。当一个任务因为获取不到资源而进入等待时说明计数值已经是 0此时你 Post 一个资源应该直接把这个资源交给等待者而不是先把资源存起来让等待者继续等。打个比方食堂窗口只有一个菜位排队的人已经站满了厨师炒好一盘菜后应该直接递给排在最前面的人而不是先摆进橱窗再让那个人去拿。还有一点容易困惑计数型信号量 Post 一次只能唤醒一个任务不能理解成“计数值加一多个任务同时获得资源”。每个任务拿到资源后都会立刻从等待表摘除资源数和等待者是一一对应的。如果希望多个任务同时被放行应该考虑消息队列或事件标志组而不是靠信号量计数。4.3 超时回滚与中断里面的大坑Pend 函数里那个超时检测细节在任务从等待表摘除后。一个任务被设置成OSTCBDly超时值系统节拍中断OSTimeTick()会每个 tick 给这个值减一。减到 0 时如果任务还在等待信号量内核就调用OS_EventTO()把任务从等待表摘掉然后把它放回就绪表。所以 Pend 被调度回来之后需要检查OSTCBStat里是否还挂着OS_STAT_SEM如果还挂着说明这次唤醒来自超时要补做一次等待表清理如果不挂了说明是被 Post 唤醒的。信号量使用里最大的坑是有人尝试在中断服务程序里调用OSSemPend()。Pend 是可能阻塞的而中断上下文不能睡眠一旦信号量计数值为 0中断服务程序就会因为 Pend 而陷入无穷等待。反过来OSSemPost()是允许在中断里调用的因为它不会阻塞只是把等待者放回就绪表并在退出临界区后触发调度。这个“中断里可以 Post、不能 Pend”的规则是 uC/OS-II 的经典考点。另外一个容易忽略的语义如果把信号量当成纯事件通知用Post 发生在 Pend 之前计数会先变成 1后面 Pend 就会直接消费掉这个事件看起来像“事件没丢”。但如果本来想表达的是“通知到达后任务才开始干活”那这次 Post 之后还需要一次 Pend 才能让事件生效这中间的时间差可能改变程序行为。所以选信号量还是事件标志组取决于你到底需要“资源计数”还是“事件发生提醒”。5. 互斥锁的优先级继承OSEventCnt 高 8 位的玄机5.1 优先级反转RTOS 面试必问的“交通拥堵”先看一个经典场景。系统里有三个任务T1 高优先级、T2 中优先级、T3 低优先级。假设 T3 先获得了一把互斥锁进入临界区操作共享资源。此时 T1 就绪抢占了 T3也想获取同一把锁但锁被 T3 持有T1 只能进入等待。如果有个 T2 在这时变成就绪由于 T2 优先级高于 T3T2 会抢走 CPU 开始运行。问题来了T1 明明优先级最高却要被 T3 手里的锁拖着而 T3 又没法运行因为 T2 一直占着 CPU。T1 实际等待时间不再取决于 T3 什么时候用完资源而取决于 T2 什么时候跑完。这就是优先级反转。优先级反转的本质是低优先级任务持有资源导致高优先级任务被阻塞而中优先级任务又抢占了这个低优先级任务让高优先级任务的等待时间失控。没有防护机制的话最坏情况可能长达几十毫秒甚至几百毫秒在实时系统里是致命的。5.2 uC/OS-II 的解法把持有者临时“拎起来”uC/OS-II 采用优先级继承协议来缓解这个问题。思路很简单当高优先级任务因为互斥锁被低优先级任务持有而阻塞时内核临时把持锁任务的优先级提升到与请求者相同让持锁任务尽快运行并释放锁。锁释放后再把持锁任务恢复到原来的优先级。实现这个逻辑靠的就是OSEventCnt的高 8 位。我把关键流程写成简化代码void OSMutexPend (OS_EVENT *pevent, INT16U timeout, INT8U *perr) { if ((INT8U)(pevent-OSEventCnt OS_MUTEX_AVAILABLE) 0u) { // 锁已被占用 ptcb (OS_TCB *)pevent-OSEventPtr; if (ptcb-OSTCBPrio OSTCBCur-OSTCBPrio) { // 保存持锁任务当前优先级到 OSEventCnt 高 8 位 pevent-OSEventCnt 0x00FFu; pevent-OSEventCnt | (INT16U)(ptcb-OSTCBPrio 8); // 把持锁任务提升到请求者的优先级 ptcb-OSTCBPrio OSTCBCur-OSTCBPrio; // 同时调整持锁任务在就绪表里的位置 } OS_EventTaskWait(pevent); OSSched(); } else { // 锁可用直接取得锁 pevent-OSEventCnt (INT16U)~OS_MUTEX_AVAILABLE; pevent-OSEventPtr OSTCBCur; } }释放锁时OSMutexPost()会检查持锁任务当前优先级是否被临时改动过如果改过就根据高 8 位暂存的值把它恢复回去。整个过程中OSEventCnt的 16 位被分成了两部分低 8 位表达锁的可用状态高 8 位用来暂存优先级恢复值。这样一个字段在结构体生命周期里承担了两个职责第一次看源码的人很容易绕晕。需要说明的是不同版本的 uC/OS-II 在互斥锁的细节处理上略有差异我这里给的是理解主线用的通用骨架。真要调源码建议以手边版本为准重点观察三处一是高 8 位什么时候被写入二是持锁任务优先级被修改后如何同步更新就绪表三是 Post 释放锁后如何恢复优先级。5.3 互斥锁的三个使用提醒第一临界区代码要尽量短。互斥锁虽然解决了优先级反转但代价是把持锁任务临时提升到很高优先级这个阶段它会抢占很多中优先级任务。如果你在临界区里做大量耗时计算等于人为制造了新的抢占。所以互斥锁保护的是“短小精悍”的共享资源操作不是让你在里面跑业务逻辑。第二uC/OS-II 的互斥锁没有递归功能。同一个任务连续两次获取同一把互斥锁第一次成功第二次因为锁还是自己占着会进入等待结果永远等不到自己释放锁直接把自己锁死。这在裸机编程里不会有但在 RTOS 里特别容易踩尤其是函数嵌套调用路径很深的项目。第三创建互斥锁时传的优先级上限要认真选。按手册要求这个值必须大于等于所有可能使用该锁的任务的优先级数值上小于等于这些任务的优先级数值确保无论哪个任务获得锁都不会因为自身优先级低于那些可能等锁的任务而引发不可控反转。定得太低锁的继承保护范围就不够定得太高又会造成不必要的抢占。这个值需要设计阶段就定好而不是随手填一个。6. 动手验证两个任务用信号量保护共享输出6.1 最小工程怎么搭理论看再多不如搭个最小工程跑一遍。我以最常见的 STM32 裸机移植环境为例核心思路是创建两个任务共享一个串口输出用二值信号量保护避免日志乱掉。OS_EVENT *SemUart; void TaskLow (void *pdata) { INT8U err; for (;;) { OSSemPend(SemUart, 0, err); printf([low ] enter critical section\r\n); OSTimeDly(1); printf([low ] leave critical section\r\n); OSSemPost(SemUart); OSTimeDly(100); } } void TaskHigh (void *pdata) { INT8U err; for (;;) { OSSemPend(SemUart, 0, err); printf([high] enter critical section\r\n); OSTimeDly(1); printf([high] leave critical section\r\n); OSSemPost(SemUart); OSTimeDly(50); } }SemUart用OSSemCreate(1)创建初值为 1表示只有 1 个“串口使用权”。两个任务进入临界区前 Pend离开后 Post。注意我故意在临界区里放了一个OSTimeDly(1)目的是把临界区时间拉长好观察另一个任务是不是真的被挡住。如果不放这个延时printf 本身很快你很难看到明显的阻塞效果。6.2 从日志和调度看现象正常现象是日志里[low] enter和[high] enter永远不会交叉嵌套。因为无论哪个任务先拿到信号量另一个任务都必须等它 Post 后才能进入。我实测看到的是这样一组交替输出[low ] enter critical section [low ] leave critical section [high] enter critical section [high] leave critical section [high] enter critical section [high] leave critical section [low ] enter critical section因为 TaskHigh 优先级更高而且OSTimeDly(50)比 TaskLow 的 100 更短所以高频任务明显拿锁次数更多。这个现象恰恰说明调度和信号量是配合工作的就绪表决定谁优先跑信号量决定谁有资格进临界区。要注意我第一阶段测试里用 printf 直接输出实际串口效果偶尔还是会乱原因不是信号量失效而是 printf 到串口外设的字节输出没有等待发送完成高优先级任务可能在下一次发送还没结束时就开始打印。要严格验证信号量保护效果要么用逻辑分析仪拉 GPIO 电平要么把 printf 改成“等待串口发送完成后再返回”的版本不然你会误判内核有问题。6.3 这个实验里我踩过的坑第一次跑这个 demo 时我把OSSemPend的 timeout 填成了 0以为 0 是“马上返回”结果任务卡死了。后来看手册才发现uC/OS-II 的 Pend 超时值 0 表示无限等待不是不等待。如果你希望 Pend 有超时能力要填具体 tick 数比如OSSemPend(SemUart, 10, err)表示最多等 10 个 tick等不到就走超时分支。这个细节很多人最开始都会搞反。另一个坑是中断里 Post 的调度时机。我曾在定时器中断里 Post 信号量发现任务并没有立刻恢复运行。原因很简单中断上下文里不能直接OSSched()系统是通过OSIntExit()在中断退出时才完成调度。如果中断里反复 Post任务恢复运行的时机看起来像被“延迟”了其实这正是 uC/OS-II 为了保证中断退出流程统一而特意设计的。7. 事件控制块高频问题速查7.1 症状对照表症状可能原因排查方向任务卡死在 PendPost 永远唤不醒等待表被破坏或者 Post 与 Pend 未配对检查 OSEventGrp/OSEventTbl 是否一致检查信号量初值和 Post 次数Post 后高优先级任务没有立即运行在中断里调用了 Post调度要在 OSIntExit 中完成确认调用上下文必要时查看中断退出流程互斥锁获取后任务直接卡死同一个任务递归获取互斥锁检查代码路径里是否有嵌套调用同一个 Mutex任务超时后仍然占着等待表OS_EventTO 或手动摘除逻辑漏清 OSEventGrp 对应组位检查摘除时组位是否同时清空信号量计数异常变大或变小对 OSEventCnt 的操作没做临界区保护检查是否在中断和任务里同时对同一信号量操作修改 OS_MAX_EVENTS 后编译错误事件块数组大小与配置不对齐检查 OS_CFG.H 与 OS_EVENT_TBL_SIZE 相关宏7.2 三条独家经验追源码时别光靠脑补。把OSEventGrp和OSEventTbl的数值在调试器里实时观察跟OSRdyGrp/OSRdyTbl的变化对照很多时候一眼就能看出问题出在等待表还是就绪表。我调试信号量问题时几乎必看这两个变量的 Watch 窗口。不要轻信“互斥锁就是带优先级继承的二值信号量”这句话。在 uC/OS-II 里两者的内部实现差得很多互斥锁为了让OSEventCnt一个字段同时表达锁状态和优先级信息做了大量位运算。如果面试只说“Mutex 就是 Sem 优先级继承”会显得你没看过源码补一句“OSEventCnt 的高 8 位被复用了”才会让面试官觉得你是真读过代码。版本之间细节有差异。2.52、2.86、2.91 这几个常见版本里OS_EVENT 的字段基本没变但 OS_CFG.H 里的配置项和互斥锁相关宏有时会调整。我见过有人拿旧版笔记分析新版代码越分析越乱。建议源码精读前先确认自己手上是什么版本以实际代码为准。我自己在翻 uC/OS-II 的过程中最大的收获其实不是背住了某个函数而是看明白了老一代 RTOS 是怎么在资源极度受限的环境里用位图、查表和字段复用把性能挤出来的。OSEventCnt 那 16 位在不同原语里被反复解释这个设计放在今天看依然非常精巧。建议你也拿一份源码把本文提到的几个函数对着 OS_CORE.C 和 OS_SEM.C 过一遍看一遍和自己写一遍完全是两种感受。下一篇文章我打算顺藤摸瓜去拆 OS_Q把消息队列的存储结构和任务间指针传递讲清楚。
返回列表