1. 第 7 篇为什么挑事件控制块和信号量下手
前面六篇把任务控制块、就绪表、调度器、时钟节拍、任务挂起恢复这些东西基本啃完了。任务能跑起来、能按优先级抢占,这套骨架算是立住了。但真到实际项目里,光有任务调度是远远不够的。多个任务同时去操作一个串口、一起往一个环形缓冲区里塞数据、一个任务等另一个任务采集完成再去做计算——这些场景如果没有一套可靠的同步机制,程序就会时不时地出现一些你复现都复现不了的怪现象。
我自己在早期用 RTOS 的时候吃过这个亏。当时做的是一个 GD32F103 上的采集板,三个任务:一个负责 ADC 采样,一个负责协议打包,一个负责串口发送。当时图省事,就用了几个全局标志位加 volatile,配合简单的 if 判断来协调。小数据量的时候看着挺稳,一旦采样率提上去,偶尔就会打包出半包数据。查了两天,最后发现是标志位判断和缓冲区操作之间没有原子性保证,任务切换正好切在那个窗口里。那之后我才老老实实去啃 uC/OS-II 的信号量源码。
这一篇聚焦的就是 uC/OS-II 里最核心的同步基础设施——事件控制块(Event Control Block,简称 ECB)以及架在它上面的信号量(Semaphore)。uC/OS-II 很有意思的一点是,信号量、互斥量、消息邮箱、消息队列这四种东西,底层全部共用同一个OS_EVENT结构体,共享同一套等待表管理和任务唤醒逻辑。你把这一个结构体和围绕它的几个核心函数吃透,等于一次性拿下了四种通信机制的地基。这也是我为什么把这一块单独拎出来作为一篇的原因——它的性价比实在是太高了。
这篇适合谁看?如果你已经能把一个 uC/OS-II 或者类似 RTOS 移植到自己的板子上、任务也能正常跑了,但每次一到“这个信号量该在哪里 Post”“为什么这个 Pend 一直超时”就犯迷糊,那这篇就是给你写的。如果你是在准备 rtos 相关的面试题,想去讲清楚“rtos 信号量是怎么实现的”,这篇也能给你提供从源码层面的支撑材料。我尽量按“讲清为什么这么设计”的路子来,不满足于告诉你函数怎么调用。
2. OS_EVENT 结构体逐字段拆解:四个通信原语共用一块内存
uC/OS-II 里所有事件相关的内存,都是从一个全局的空闲事件链表OSEventFreeList上分配的。系统初始化的时候,OSInit()会按照OS_MAX_EVENTS这个配置项一次性把这么多OS_EVENT结构体串成一个单向链表,谁要用谁就去链表头上摘一个。这个设计思路和任务控制块的分配是一样的——静态分配、运行时零malloc,这在嵌入式里是个非常重要的原则,因为动态内存在长期运行的系统里容易产生碎片,一旦碎片化严重,后面某个时刻就会申请失败,而这种失败往往发生在系统已经跑了很久之后,极难排查。
OS_EVENT的定义长这样(以 uC/OS-II v2.86 经典版本为例,不同版本字段顺序略有差异,新版还多了个事件名字段):
typedef struct os_event { INT8U OSEventType; /* 事件类型 */ void *OSEventPtr; /* 消息指针,或互斥量拥有者 */ INT16U OSEventCnt; /* 信号量计数,或互斥量的优先级信息 */ OS_PRIO OSEventGrp; /* 等待该事件的任务分组 */ OS_PRIO OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待该事件的任务位图表 */ } OS_EVENT;我逐个字段说清楚它到底干什么,因为这几个字段的身份在不同事件类型下是会变的,这也是初学者最容易绕晕的地方。
2.1 OSEventType 与 OSEventPtr 在不同原语下的分工
OSEventType是个类型标签,取值有OS_EVENT_TYPE_SEM、OS_EVENT_TYPE_MUTEX、OS_EVENT_TYPE_MBOX、OS_EVENT_TYPE_Q这么几种。它的作用是在每个 API 入口处做参数合法性校验。比如你调用OSSemPend(),函数进去第一件事就是检查pevent->OSEventType != OS_EVENT_TYPE_SEM,如果不匹配就直接返回OS_ERR_EVENT_TYPE。这个校验看起来简单,但非常关键。我见过有人把一个消息队列的指针误传给了信号量函数,如果没有这层类型校验,程序会直接去解释那块内存,跑起来就是玄学崩溃。有了这个标签,至少能立刻在错误码上暴露出来。
OSEventPtr这个指针字段就更有意思了,它的含义完全取决于事件类型。对于消息邮箱,它指向那条消息;对于消息队列,它指向OS_Q结构体;对于信号量,它一般是空指针,用不上;而对于互斥量,它指向当前持有该互斥量的任务的 TCB。所以你看,同一个字段,在不同原语下扮演着完全不同的角色,这是一种典型的空间复用手法。在内存极度紧张的 8 位、16 位单片机上,这种复用能省下不少 RAM,代价就是可读性会稍微差一点,得靠事件类型来判断当前该按哪种语义去解读。
2.2 OSEventCnt 的双重身份与它的边界
OSEventCnt是个 16 位无符号整数,它的双重身份是这个结构体里最需要留意的点。对信号量而言,它就是信号量的计数值,OSSemCreate(1)创建出来的二进制信号量,初始计数就是 1,每 Pend 一次减一,每 Post 一次加一。对互斥量而言,它被拆成了高 8 位和低 8 位来用——低 8 位保存的是“互斥量当前是否可用”的标记(OS_MUTEX_AVAILABLE就是 0xFF),高 8 位用来在发生优先级继承时,保存互斥量原本的拥有者优先级。这个设计挺巧妙的,优先级继承的临时优先级信息就藏在这半个字段里,不用额外开变量。
为什么信号量计数值用的是 16 位无符号?因为OSSemPost()里有个判断:如果pevent->OSEventCnt < 65535u就加一,否则返回OS_ERR_SEM_OVF(溢出错误)。也就是说信号量最多计到 65535。这个上限其实是防止你在没有等待者的情况下无脑 Post、把计数堆到溢出回绕。老实讲,正常业务里用到几百的计数就已经很少见了,但知道这个边界的存在,能帮你在排查“Post 返回溢出错误”时有个方向——多半是 Post 次数远多于 Pend 次数,属于逻辑写反了。
2.3 OSEventGrp 与 OSEventTbl:等待表其实是一张位图
这是整个事件控制块里最精髓的部分。所有正在等待这个事件的任务,不是用链表串起来的,而是用优先级位图表记录的。OSEventGrp是一个 8 位的分组字节,OSEventTbl[]是若干字节,每个字节的每一位对应一个优先级。当某个优先级为prio的任务挂到这个事件上等待时,就往这张位图表里把对应位置 1。
具体来说,优先级prio被拆成两部分:高 3 位是组号y,低 3 位是组内位号x。规则是OSEventGrp的第y位置 1,OSEventTbl[y]的第x位置 1。举个例子,优先级 26,二进制是011010,高 3 位011就是 3,低 3 位010就是 2。那么就是OSEventGrp |= (1 << 3),也就是第 3 位;OSEventTbl[3] |= (1 << 2),也就是该字节的第 2 位。等要找最高优先级的等待任务时,只要从这张位图里从小到大找出最低的那个置位位置就行,这比遍历链表快得多。
这个位图机制就是 uC/OS-II 能保证“唤醒总是唤醒优先级最高的那个等待任务”的底层原因,也是它作为一个硬实时 RTOS 的重要特征。你不用怀疑“为什么我 Post 了以后是它被唤醒”,答案永远是:所有等待者里优先级最高的那个。
3. 优先级位图算法:OSUnMapTbl 凭什么比链表快
上一节讲了位图表怎么存。这一节讲讲它怎么查,也就是 uC/OS-II 里那个经典的OSUnMapTbl查表法。这个算法在 stm32、gd32 这些 Cortex-M 平台上跑起来是常数时间,也是很多 RTOS 面试题喜欢问的点。搞懂它,你再看 liteos 或者别的 RTOS 的就绪表、事件表实现,会发现思路是共通的。
3.1 为什么要用位图而不是链表
先说清楚设计动机。假设用链表保存等待任务,你要找最高优先级任务,最坏情况得把链表从头到尾遍历一遍,复杂度是 O(n)。而且如果是按优先级排序插入,插入本身又得遍历找位置。在有几十个任务、频繁同步的系统里,这种开销累积起来不容忽视。
位图方案把“找最高优先级”变成了“找一个字节里最低位的置 1 位”,这个操作可以做到固定步数完成。任务数再多,查找步数也不变,这就是硬实时系统需要的确定性。你要是翻 rtos 相关的面试题,经常会遇到“rtos 和 linux 的区别”这种题,一个很关键的差异点就在这——RTOS 追求的是最坏情况下的确定性,而通用操作系统追求的是平均吞吐。位图查表法就是这种设计哲学的一个缩影。
3.2 OSUnMapTbl 的查表原理与手工算例
OSUnMapTbl本质是一张 256 字节的常量表,下标是一个字节的值,输出是这个字节里最低置 1 位的位置。比如值为 0x04(二进制00000100)时,最低置 1 位在 bit2,表里对应输出 2。值为 0x06(00000110)时,最低置 1 位在 bit1,输出 1。
它还额外处理了 0 的情况,OSUnMapTbl[0]定义为 0。这在正常流程里用不到(因为查之前一定保证分组字节非零),但作为防御性设计保留着。
有了这张表,找最高优先级等待任务就三步:
y = OSUnMapTbl[pevent->OSEventGrp]; /* 先找出分组号 */ x = OSUnMapTbl[pevent->OSEventTbl[y]]; /* 再找出组内位号 */ prio = (y << 3) + x; /* 拼回完整优先级 */我拿一个具体数字走一遍。假设OSEventGrp = 0x0C,也就是00001100,只有第 2、3 组可能有任务等待。OSUnMapTbl[0x0C]找最低置 1 位,bit2 是 1,所以y = 2。再看OSEventTbl[2],假设它是0x30,也就是00110000,第 4、5 位有任务。OSUnMapTbl[0x30]最低置 1 位是 bit4,x = 4。最终prio = (2 << 3) + 4 = 20。也就是说优先级 20 的任务是所有等待者里最高的。整个过程三次内存访问加两次加法,跟任务数量无关。
3.3 挂表、摘表、超时摘除的四个核心函数
围绕这张位图表,uC/OS-II 提供了四个核心操作函数,它们都不对外暴露,属于内核内部工具。
| 函数名 | 作用 | 调用场景 |
|---|---|---|
OS_EventWaitListInit | 把整张位图表清零 | 事件创建时初始化 |
OS_EventTaskWait | 把当前任务挂到等待表,同时从就绪表摘除 | Pend 时无资源可用 |
OS_EventTaskRdy | 从等待表找出最高优先级任务并唤醒 | Post 时有人等待 |
OS_EventTO | 把当前任务从等待表摘除(超时路径) | Pend 超时返回 |
OS_EventTaskWait做的事是“两头操作”:一边把当前任务从就绪表里摘掉,让它不再参与调度;另一边把它挂到事件等待表里。这两步必须在临界区内一次完成,中间不能被打断,否则任务切换可能发生在两者之间,导致任务既不在就绪表也不在等待表,彻底失联。
OS_EventTaskRdy则是反过来,从等待表里挑出最高优先级任务,把它从等待表摘掉、重新放进就绪表,同时清掉它的等待标志和延时计数。OS_EventTO处理的是超时这种情况——任务等不及了要从等待表里自己摘出来。这四个函数是整个事件机制的中枢,信号量的 Pend、Post、超时全都绕不过它们。理解这四个函数,你就理解了 uC/OS-II 同步机制的全部底层动作。
4. OSSemPend 与 OSSemPost 源码逐段精读
有了前面的结构体和位图基础,现在可以正式进到信号量的源码。uC/OS-II 的信号量实现集中在OS_SEM.C里,核心就五个函数:OSSemCreate、OSSemPend、OSSemPost、OSSemAccept、OSSemQuery,后面还有个删除函数。我挑最常用的三个讲透。
4.1 OSSemCreate 初始化里的几个细节
OSSemCreate(INT16U cnt)从空闲事件链表摘一个OS_EVENT,然后做三件事:设置类型为OS_EVENT_TYPE_SEM、把计数设为传入的cnt、调用OS_EventWaitListInit把等待位图表清零。返回这个事件块的指针,之后所有操作都拿这个指针当句柄。
这里有个细节值得说。摘链表这一步是在临界区里做的,因为空闲链表是个全局共享资源,多个任务可能同时创建信号量。而后续设置字段的这一步其实已经出了临界区(经典版本如此),看似有风险,但实际上刚摘下来的这个事件块还没有被任何其他任务或 ISR 引用到,所以此刻是安全的。这种“尽量缩短临界区”的写法在 uC/OS-II 里到处都是,是一种激进但经过验证的优化习惯。
注意:
cnt传 1 得到的就是常说的二进制信号量,传 0 得到的是“初始不可用”的信号量,常用于“等某个事件发生”的场景。传大于 1 的值则是计数信号量,用于管理有限数量的同类资源,比如一个池子里有 4 个缓冲区。
4.2 OSSemPend:计数值递减背后的判断逻辑
OSSemPend(pevent, timeout, perr)的流程我按顺序拆一下。进入临界区后先是几道校验关:事件类型对不对、是不是在中断里调用(OSIntNesting > 0报OS_ERR_PEND_ISR)、调度器是不是被锁了(OSLockNesting > 0报OS_ERR_PEND_LOCKED)。这几道关卡都是防御性的,直接对应现实中常见的误用。
接下来是核心判断:
if (pevent->OSEventCnt > 0) { pevent->OSEventCnt--; *perr = OS_ERR_NONE; return; }计数大于零,说明资源可用,直接减一拿走,函数正常返回,任务根本不会挂起。这就是信号量高效的地方——没有竞争时,Pend 的开销就是一次判断加一次减一。
如果计数已经是零,说明资源被别人占着,这时候才进入“挂起等待”的重量级路径:设置任务的等待标志OS_STAT_SEM、记录超时值、调用OS_EventTaskWait把任务挂到等待表、退出临界区、调用OS_Sched触发一次调度让出 CPU。等任务被唤醒回来后,再根据OSTCBStatPend判断是被正常唤醒还是超时。
这里有个关键点:超时值 timeout 传 0 表示无限等待,一直等下去直到有人 Post。这个语义一定要记牢。我见过有人以为 0 表示“不等待、立即返回”,结果写了个OSSemPend(sem, 0, &err)想试试能不能拿到,最后任务卡死在那一行。想“立即判断能不能拿到”,要用的是OSSemAccept,不是OSSemPend。
OSSemPend还有个返回值上的坑。它的返回类型是 void,成功失败都通过perr指针带回。调用完一定要检查*perr,特别是在等待被唤醒之后,因为可能是超时唤醒,此时资源并没有真正拿到。不检查就往下操作共享资源,等于没同步。
4.3 OSSemPost:谁会被唤醒
OSSemPost(pevent)的流程更简洁,但逻辑同样精妙。进临界区、校验类型,然后:
if (pevent->OSEventGrp != 0x00) { /* 有人在等 */ prio = OS_EventTaskRdy(pevent, (void *)0, OS_STAT_SEM); OS_Sched(); } else { /* 没人等 */ if (pevent->OSEventCnt < 65535u) { pevent->OSEventCnt++; } }注意这个判断顺序:先看有没有人等待,再决定是唤醒任务还是给计数加一。如果等待表非空,说明有任务在等这个信号量,那么这次 Post 的“成果”直接转交给那个优先级最高的等待者,计数不动(这个信号量在挂起等待的那一刻,计数已经是 0 了,资源直接移交,不需要经过计数)。如果没人等,才把计数加一,留给以后 Pend 的人。
为什么是这个顺序?你想象一下,信号量本质是“一张资源券”。有人等着用,你的 Post 就是把券直接递给他;没人等,你的 Post 就是把券放回票池里。这两种情况互斥,逻辑上就不可能同时发生。这个设计保证了信号量的计数永远是“当前可用资源数”,语义清晰。
唤醒时,OS_EventTaskRdy内部就是前面讲的位图三步走,找出最高优先级等待者,把它从等待表摘掉放进就绪表等待调度。之后OS_Sched会检查这个被唤醒任务的优先级是否高于当前任务,如果是就直接切过去。所以一个高优先级任务被 Post 唤醒的瞬间,可能立刻就抢占了你。
实操心得:ISR 里发信号量要用
OSSemPost,这个函数是允许在中断里调用的;但 Pend 绝对不能在中断里用,中断里想“尝试获取”应该用OSSemAccept。这个区分在很多 rtos 项目里被弄混,导致中断里 Pend 报错或者行为异常。
4.4 OSSemAccept 与 OSSemQuery 的用途
OSSemAccept(pevent)是“非阻塞版 Pend”。它进临界区看一眼计数,大于零就减一并返回剩余计数,否则直接返回 0,绝不挂起任务。这个函数特别适合在 ISR 里用,也适合“我顺便看看有没有,有就拿,没有就干别的”这种场景。
OSSemQuery(pevent, p_sem_data)则是调试和监控利器。它把信号量的当前计数、等待任务列表等状态复制到一个数据结构里给你看,不改变任何状态。在排查“到底是哪个任务在等这个信号量”的时候非常有用。
5. 互斥量和优先级继承:OSEventCnt 的两副面孔
信号量讲完,接下来是它的近亲——互斥量。互斥量的接口长得跟信号量很像,OSMutexCreate、OSMutexPend、OSMutexPost,用起来也像。但它的内部实现有一个信号量没有的东西:优先级继承。这是它存在的唯一理由,也是它比信号量复杂得多的原因。
5.1 为什么普通信号量在共享资源保护上会翻车
先说清楚互斥量要解决什么问题。假设有三个任务:高优先级任务 H、中优先级任务 M、低优先级任务 L。L 先拿到了一个信号量,进入临界区操作共享资源。这时候 H 就绪了,要抢 CPU。常规调度会让 H 抢占 L 运行。但 H 也需要那个信号量,于是 Pend 挂起等 L 释放。这时候 M 就绪了,因为 M 优先级高于 L,M 把 L 抢占,自己跑起来。结果就是:L 迟迟得不到 CPU 来释放信号量,H 一直被 M 间接地卡着。
这个现象叫优先级反转。H 明明优先级最高,却因为 M 的插队而被拖住了。在实时系统里,这个延迟可能是不可预测的,严重时会导致控制周期超时之类的硬故障。
用普通信号量保护共享资源,就天然带着这个隐患。这也是为什么 uC/OS-II 专门整出个互斥量类型——它就是为了解决这个问题而生的。
5.2 OSMutexPend 里的优先级继承是怎么实现的
互斥量解决优先级反转的办法是优先级继承。当你 Pend 一个已经被别人持有的互斥量时,内核会临时把持有者的优先级提升到跟你一样高,这样持有者就能尽快跑完、尽快释放,不会再被中间优先级的任务无谓地拖住。释放之后,持有者恢复原来的优先级。
具体到源码,OSMutexCreate时会把OSEventCnt的高 8 位设成 0,低 8 位设成OS_MUTEX_AVAILABLE(0xFF)。当一个任务成功拿到互斥量时,拥有者 TCB 被记到OSEventPtr里,同时OSEventCnt的低 8 位清零表示“已被占用”。
OSMutexPend里那段优先级继承的核心逻辑大致是这样:如果互斥量已被占用,先把当前任务挂到等待表,然后用OS_EventCnt的高 8 位保存拥有者的原优先级,再把拥有者的优先级改写成当前任务的优先级,并从就绪表里把它摘出来、按新优先级重新挂回去。这样下一次调度时,持有者会带着那个被抬高的优先级运行,尽快释放互斥量。
到了OSMutexPost,如果曾经发生过优先级继承(高 8 位非零),就从这个字段里读出原来的优先级,把拥有者恢复回去,同时把字段清干净。整个过程能量守恒,进出对称。
注意:优先级继承是“继承”,不是“提升到最高”。它只把持有者提到跟等待者里最高优先级的那个一样高。如果有多个不同优先级的任务等着,会逐步继承到最高的那个。另外 uC/OS-II 的优先级继承只处理一层,不支持嵌套继承到最坏情况的完整链路——严格的实时理论里还有优先级天花板等更完备的方案,但 uC/OS-II 选了实现简单、覆盖大多数场景的这条路线。
5.3 使用互斥量的几条硬规矩
互斥量虽然好用,但有几条规矩必须守住,否则它会给你带来新的麻烦。
第一,谁 Pend 谁 Post。互斥量是有归属的,只有当前持有者才有资格释放它。OSMutexPost会检查OSTCBCur == pevent->OSEventPtr,不是持有者就返回OS_ERR_NOT_MUTEX_OWNER。这条检查避免了 A 任务拿了锁、B 任务糊里糊涂把它放掉的荒唐情况。
第二,不要在中断里 Pend 互斥量。中断里没有任务可以挂起,而且互斥量的优先级继承依赖任务 TCB,中断上下文根本没有对应的 TCB。中断里想同步,用信号量。
第三,别嵌套锁同一个互斥量。uC/OS-II 的互斥量不是递归锁。同一个任务对同一个互斥量 Pend 两次,第二次会把自己挂起等待自己,直接死锁。如果需要递归的共享资源保护,要么避免嵌套,要么用计数信号量另做设计。
第四,临界区里别做耗时操作。互斥量保护的临界区越短越好。你要是拿锁之后去跑个几百毫秒的算法,那就算有优先级继承,别人等锁的时间也照样很长。锁是保护“操作原子性”的,不是保护“操作很慢”的。
6. 移植到 GD32F103 与调试排坑实录
把源码看明白是一回事,真正让它在你自己的板子上跑对又是另一回事。这一节我把这些年在 gd32f103、stm32 这些 Cortex-M 平台上移植和调试 uC/OS-II 时踩过的坑和常见问题梳理一下。热词里有个“gd32f103 移植 rtos”,用这颗片子的朋友应该不少,我就拿它当例子说。
6.1 GD32F103 上移植时事件相关的坑
Cortex-M3 的移植里,事件控制块相关的问题往往不是出自 uC/OS-II 源码本身,而是出自移植层的临界区保护和中断优先级配置。OS_ENTER_CRITICAL和OS_EXIT_CRITICAL在 Cortex-M3 上通常用CPSID I/CPSIE I实现,也就是关全局中断。如果临界区开得过大,或者任务里用信号量包裹了太长的代码,边关中断边跑会导致系统节拍丢拍,表现出来就是时间相关行为不准。
中断优先级的配置更是重灾区。Cortex-M 里内核的 PendSV、SysTick 这些异常的优先级,必须比任何会调用 RTOS API 的外部中断都要“低”(数值大)。如果你把一个发信号量的外部中断优先级配得比 PendSV 还高,就可能在实际调度切换的中途把中断嵌套进来,破坏内核数据结构。轻则偶发异常,重则直接 HardFault 复位。
提示:移植后先不做任何业务,只在两个任务里互相
OSSemPost/OSSemPend跑一天,确认稳定再上真实逻辑。这一步能把绝大多数移植层的问题挡在业务之外,省下大量“到底是业务 bug 还是移植 bug”的扯皮时间。
6.2 信号量用错导致的典型现象速查表
很多时候问题的现象很有迷惑性,下面这张表是我自己总结的“现象到根因”对照,遇到问题可以先照这张表排查一遍。
| 现象 | 可能根因 | 排查动作 |
|---|---|---|
| 任务永久卡在 Pend 不动 | 忘记 Post,或 timeout 传了 0 | 检查成对的 Post,确认是否要传超时 |
| Pend 立刻返回超时错误 | 计数一开始就是 0,且无人 Post | 确认OSSemCreate的初值 |
Post 返回OS_ERR_SEM_OVF | Post 次数远多于 Pend,计数溢出 | 查逻辑,多半 Post 写反 |
| 偶发拿不到信号量又没报错 | 没检查*perr,超时唤醒被忽略 | 每次 Pend 后强制检查 perr |
| 中断里 Pend 报错 | 用了OSSemPend而非OSSemAccept | 中断里改用 Accept |
| 低优先级任务迟迟不释放锁 | 优先级反转,用了信号量而非互斥量 | 共享资源保护换 OSMutex |
这张表我建议打印出来贴在工位上。信号量相关的 bug 看起来千奇百怪,实际根因来来回回就那么几种。掌握了源码层面的机制,你排查起来就不再是靠猜,而是能顺着“计数、等待表、唤醒路径”这三条线索快速定位。
6.3 从源码精读里真正学到的东西
啃完这一块源码,我个人最大的收获不是记住了几个函数签名,而是理解了一套同步机制该怎么设计。uC/OS-II 用一张位图表统一管理等待任务,用同一套唤醒逻辑服务四种通信原语,用优先级继承来对抗反转——每一步选择背后都有明确的取舍。这种“用最少的代码解决最多的问题”的思路,比单纯会用 API 有价值得多。
后来我再看其他 rtos,比如 liteos 的 rtos 信号量实现、或者做一些 rtos 项目时选型,就能很快判断出它的同步机制大概是哪种实现路子、在什么场景下会退化。这种判断力,是靠逐行读源码一点点攒出来的。6736 行代码听起来不少,但真正核心的也就是调度、时钟、通信这么几块,每一块啃透一层,你对整个系统是怎么运转的认知就上一个大台阶。下一篇我打算把消息邮箱和消息队列合在一起讲,那块的事件指针和数据搬运逻辑又是另外一套玩法,我们下篇见。