十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

uC/OS-II内核源码解析:从任务调度到上下文切换的完整实现

uC/OS-II内核源码解析:从任务调度到上下文切换的完整实现 1. 为什么从这一篇开始我们才真正进入uC/OS-II的心脏我先把话说在前面如果你只是想会调用RTOS的API比如OSTaskCreate、OSSemPend这些函数那看官方手册就够了完全没有必要啃源码。但如果你想知道一个任务从被创建到被调度运行再到被切换出去这整个生命周期里内核到底在背后做了哪些事那源码是绕不开的。uC/OS-II的源码体量在主流RTOS里属于小而精的类型——核心文件加上移植层也就几千行而标题里提到的6736行基本覆盖了完整内核功能。我当时读这套代码时有个很直接的感受它不像FreeRTOS那样因为兼容各种编译器和硬件而夹杂了大量条件编译uC/OS-II的代码读起来更像一本教科书线性逻辑非常清晰。这也是为什么很多高校的嵌入式课程会拿它当教学内核。第1篇里我把工程结构、编译环境、内核对象之间的总体关系过了一遍整条主线是从main函数如何把硬件初始化、OSInit初始化内核数据结构、再通过OSTaskCreate建立第一个任务最后启动多任务调度这条路径展开的。这一篇我们换个完全不同的切入角度——直接钻进内核最核心的任务调度器里面去。你可能会想既然上一篇已经讲了任务的创建那这一篇难道不是应该讲OSTimeDly或者信号量我的想法是优先把调度器讲透。道理很简单RTOS的实时二字最终是靠调度器来兑现的。你哪怕把信号量、消息队列、互斥锁全背得滚瓜烂熟不理解任务之间是如何切换的、就绪表是怎么在O(1)时间内找到最高优先级任务的那遇到调度延迟、优先级翻转这类问题照样一头雾水。而uC/OS-II最值得读的、也是最有技术含金量的部分恰恰就是它那张经典的位图就绪表和两段上下文切换代码。2. 任务控制块TCB内核眼中一个任务的全部家当2.1 从OS_TCB的结构体布局看设计思路动手读源码的朋友我建议你翻开uC/OS-II的uCOS_II.H文件找到OS_TCB这个结构体。在2.91版本里它大致长这样typedef struct os_tcb { OS_STK *OSTCBStkPtr; struct os_tcb *OSTCBNext; struct os_tcb *OSTCBPrev; INT8U OSTCBId; INT32U OSTCBCnt; void *OSTCBMsg; OS_EVENT *OSTCBEventPtr; INT8U OSTCBStat; INT8U OSTCBPrio; INT8U OSTCBStatPend; INT8U OSTCBEventMultiPtr; ... OS_STK *OSTCBStkBase; INT32U OSTCBStkSize; INT32U OSTCBCtxSw; INT32U OSTCBCtxSwN; ... } OS_TCB;注意不同版本的uC/OS-II字段略有差异我这里只是为了说明问题。第一眼看过去这个结构体字段不少但你只要抓住一条主线就不会乱OS_TCB保存的是一个任务的完整运行现场和资源挂钩信息。它可以分为几组栈相关OSTCBStkPtr是当前栈顶指针任务切换时CPU寄存器就是压入这个位置OSTCBStkBase和OSTCBStkSize是栈底和栈大小用于栈溢出检测和统计。链表节点OSTCBNext和OSTCBPrev把一个双向链表串起来内核遍历所有任务控制块就靠它。事件与消息挂钩OSTCBEventPtr、OSTCBMsg这些字段指向任务正在等待的事件控制块或消息后面讲信号量、邮箱时它们会登场。状态与优先级OSTCBStat表示任务当前状态就绪、挂起、延时等OSTCBPrio是这个任务唯一的优先级编号。我当年第一次读到这个结构体时最困惑的点是为什么TCB里面不直接保存CPU寄存器的值后来想明白了——寄存器不保存在TCB里而是保存在任务的栈里TCB的OSTCBStkPtr只负责记住栈在哪。这个设计的好处是任务切换时只需要把当前CPU寄存器全部压栈然后从下一个任务的栈里恢复寄存器TCB本身不需要为每个寄存器预留字段内存占用更紧凑。2.2 OS_TCBInit初始化流程优先级怎么被写进TCB任务创建时OSTaskCreate会先分配一个TCB节点然后调用OS_TCBInit来做各项字段的初始化。这里面有几个关键步骤的顺序是有讲究的从空TCB链表中摘一个空闲节点或者动态分配一个新节点把任务的栈指针OSTCBStkPtr指向任务栈的某个位置这个位置是OSTaskStkInit处理过的伪栈帧里面已经按特定顺序“预置”好了CPU寄存器的初始值设置OSTCBPrio和OSTCBStat把任务置为就绪态把TCB节点挂入已建立任务的双向链表。这里我强烈建议你用调试器跟着走一遍。我当时在Keil里给OS_TCBInit下断点单步执行每执行一步就查看TCB里各个字段的变化。你会发现OSTCBStkPtr指向的内存区域里面每一个字都对应着某个CPU寄存器的初始值——包括程序计数器、状态寄存器、通用寄存器。一旦多任务启动第一次任务切换就会从这个伪栈帧里把这些初值弹出到真实的CPU寄存器里任务就像“曾经被切换出去过一样”开始运行。这种“伪造一个已经存在的中断现场”的思路是整个RTOS任务切换能够成立的根基。理解了这一步后面读OSCtxSw、OSIntCtxSw时会非常顺畅。3. 就绪表与位图算法为什么查找最高优先级任务能做到O(1)3.1 一张表如何装下64个任务的就绪状态uC/OS-II支持最多64个优先级对应8组就绪状态。这是整个内核调度最核心的数据结构定义在OS_CORE.C里typedef struct { INT8U OSRdyGrp; INT8U OSRdyTbl[OS_RDY_TBL_SIZE]; } OS_RDY_TBL;从逻辑上理解OSRdyGrp有8位每一位对应一组8个优先级。第n位为1说明第n组里有任务处于就绪态。OSRdyTbl[n]这个字节的每一位对应一个具体的优先级——位0对应这组里优先级最低的那个位7对应优先级最高的那个。举个例子如果优先级为33的任务就绪了。33的二进制是0b0100001它属于第4组33/84在该组内的偏移是3371。这时内核会做两件事OSRdyGrp | OSMapTbl[4]; // 第4组的标志位置1 OSRdyTbl[4] | OSMapTbl[1]; // 组内第1位置1OSMapTbl的值是固定的( \text{OSMapTbl}[i] 1 i )即1、2、4、8、16、32、64、128。用查表代替移位运算无非是嫌移位在8位MCU上多几条指令。这个数据结构漂亮在哪儿它把“64个任务的就绪态”压缩成了8816个字节。很多小Flash的单片机RAM本来就捉襟见肘16个字节换取O(1)的调度查找时间这买卖非常划算。3.2 置位与清位的完整逻辑当一个任务变为就绪态时代码里会执行OSRdyGrp和OSRdyTbl的置位上面已经写过了。那当一个任务因延时、等待信号量而要退出就绪态时就要反向操作if ((OSRdyTbl[prio 3] ~OSMapTbl[prio 0x07]) 0) { OSRdyGrp ~OSMapTbl[prio 3]; }注意这里的顺序先清组内位再判断这个组是不是已经空了只有组内为空了才把OSRdyGrp里对应的组标志也清掉。如果你先操作OSRdyGrp再清OSRdyTbl那在中间可能会有一个短暂不一致的状态——虽然实际运行中因为临界区的存在不太容易出问题但这种代码习惯不值得学。我见过有人自己写RTOS时用的是一个长度为64的数组每个元素是布尔值来表示任务是否就绪。这种写法也没错但每次查找最高优先级任务时你得从0到63线性扫描最坏情况要扫64次。uC/OS-II用位图这个技巧就是不想干这种线性扫描的傻事。3.3 优先级判定表OSUnMapTbl查表取代循环的经典找最高优先级任务的逻辑是这样的最高优先级任务就是数值最小的优先级编号。所以我们只要看OSRdyGrp里第一个为1的位最低位在哪一组再看这一组里OSRdyTbl对应字节中第一个为1的位在哪一位。低效的做法是写个循环一位一位地测试。uC/OS-II的做法是直接查表y OSUnMapTbl[OSRdyGrp]; // 找到最低的置1位确定组号 x OSUnMapTbl[OSRdyTbl[y]]; // 确定组内偏移 prio (y 3) x; // 合成最终优先级OSUnMapTbl是一个256字节的表OSUnMapTbl[n]返回n的最低置1位的位置从0开始。比如OSUnMapTbl[0x41]对应二进制0b0100_0001最低的置1位是0查表结果是0而OSUnMapTbl[0x10]对应0b0001_0000最低置1位是4查表结果是4。这个表是硬编码生成的。如果你手头没有表又不理解它是怎么来的可以想想这个规律对于任何1字节的数值n其最低位为1的索引要么是0n是奇数、要么在n/2的求解结果上加1n是偶数且不为0。核心思路就是二分消去高位的零。查询是O(1)的插入、删除也是O(1)的。这就是为什么uC/OS-II敢自称调度器的时间是确定的——它跟系统里有多少个任务无关。3.4 我在读代码时踩过的理解误区第一个误区很多人以为OSRdyTbl是按优先级顺序排的其实它跟TCB链表没有天然关系。OSRdyTbl只是就绪状态的一张位图谁来查这张表就能迅速知道当前该跑谁。TCB链表解决的是内核手里有哪些任务这个管理问题跟调度是两回事。第二个误区以为最高优先级是以数字大者为高。uC/OS-II里恰恰相反数字越小优先级越高。这个约定如果你搞反了读代码时会处处别扭。优先级0留给系统里最重要的任务优先级63默认给空闲任务。4. 调度器OSSched与任务切换OSCtxSw一次现场转移的全过程4.1 OS_Sched入口临界区保护与最高优先级任务的挑选看调度器实现时我建议你带着两个问题去读一、它是在什么时机被调用的二、它凭什么确保不会被中断打断。OSSched的源码很短核心逻辑如下void OS_Sched(void) { INT8U y; OS_ENTER_CRITICAL(); y OSUnMapTbl[OSRdyGrp]; OSPrioHighRdy (y 3) OSUnMapTbl[OSRdyTbl[y]]; if (OSPrioHighRdy ! OSPrioCur) { OSTCBHighRdy OSTCBPrioTbl[OSPrioHighRdy]; OSCtxSwCtr; OS_TASK_SW(); } OS_EXIT_CRITICAL(); }注意OSSched先进入临界区然后找出最高优先级任务接着判断找出来的任务跟当前正在运行的任务是不是同一个。如果不同才执行切换。如果相同那直接退出临界区继续跑当前任务。这段代码里有两个细节值得琢磨第一OS_ENTER_CRITICAL()在不同处理器上有不同的实现。有些平台是关中断有些平台只是把CPU状态字保存起来。实现方式各异但目标一致保护这一段代码在访问共享数据时不被其他任务或中断打扰。但如果你在中断服务程序里调用OSSchedOS_ENTER_CRITICAL是会闯祸的——因为中断本来就已经隐式地屏蔽了同级或低级中断再嵌套关中断可能导致中断响应延迟过高。uC/OS-II专门为中断场景准备了另一个调度函数OSIntExt后面我会单独讲。第二OSTCBPrioTbl[priority]这个指针数组是内核从优先级号定位到TCB的关键。它跟TCB双向链表是平行的两个索引结构。你要记住uC/OS-II里“有多少个有效的优先级号就可能有多少个TCB指针”这个数组的下标和优先级号是严格一一对应的。4.2 OSCtxSw汇编代码逐行拆解明白了调度器的思路接下来就是本篇文章的重头戏——真正的上下文切换。以我手边的ARM7移植为例OS_TASK_SW()一般是一个宏展开后是一个软件中断指令或者直接的函数跳转。在ARM内核上它触发的向量是OSCtxSw。OSCtxSw的汇编实现核心做的事情有三件把当前任务的现场压栈把当前栈指针保存到当前TCB的OSTCBStkPtr里从新任务的TCB取栈指针把新任务的现场弹栈然后跳转到新任务的上下文继续运行。对应到ARM汇编通常长这样我简化了部分细节保留关键逻辑OSCtxSw ; 保存当前任务现场到它的栈 STMFD SP!, {R4-R12, LR} ; 压入低寄存器组之外的寄存器 MRS R0, CPSR ; 读取当前程序状态寄存器 STMFD SP!, {R0} ; 状态寄存器也压栈 ; 把当前SP保存到当前任务的TCB LDR R1, OSTCBCur LDR R1, [R1] STR SP, [R1] ; OSTCBCur-OSTCBStkPtr SP ; 切换到新任务 LDR R0, OSTCBHighRdy LDR R2, [R0] LDR SP, [R2] ; SP OSTCBHighRdy-OSTCBStkPtr ; 弹出新任务的现场 LDMFD SP!, {R0} MSR CPSR, R0 LDMFD SP!, {R4-R12, PC} ; 返回地址直接弹到PC实现跳转没写过汇编的朋友看到这段可能会头皮发麻但其实拆开看每一行都非常直白。STMFD是“满递减栈”的压栈操作一次压入多个寄存器SP自动向下移动。LDMFD是弹栈一次从栈中弹出多个寄存器。关键在最后一条LDMFD SP!, {R4-R12, PC}——把栈里保存的返回地址直接弹到程序计数器PCCPU下一步就会跳到新任务上次被中断的指令继续执行。这就是“任务切换”的本质不是真的在运行什么魔法而是把CPU这套寄存器的“运行上下文”从一套换成另一套。4.3 中断级切换OSIntCtxSw为什么它比OSCtxSw少了一段保存工作有中断参与的任务切换走的是另一条路。当某个中断服务程序执行完毕发现有更高优先级的任务因此就绪时不能直接返回被中断的任务而应该切换到那个更高优先级的任务。这个场景的切换代码是OSIntCtxSw。跟OSCtxSw最大的区别在于进入中断服务程序时CPU上下文已经被压栈了OSIntCtxSw不需要再重复压一次。它会做的仅仅是修正栈指针让栈顶指向正确的位置然后跟OSCtxSw一样保存当前SP到当前TCB、取新任务SP、恢复现场。这种优化看似只是省了几行汇编但在高频率中断系统里减少重复压栈就减少了内存写操作这对实时性直接有好处。很多自研RTOS在这块处理得不够干净要么中断现场重复保存导致栈越用越深要么漏了某些寄存器。uC/OS-II在这一点上做得非常规矩。我在读这段代码时做了个实验手动在两个任务里各加了一个全局计数器一个挂钟任务每1毫秒打印一次一个按键扫描任务每次按键打印一次。然后在中断里触发任务切换。通过调试器查看SP的变化我清楚地看到了被中断任务的栈顶在哪、新任务的栈顶在哪。这种“看着SP跳来跳去”的体验比我读十遍理论都管用。强烈建议你也试试。5. 时间管理OSTimeTick如何把延时任务一批批唤醒5.1 从OSTimeDly到延时链表的流转实时系统离不开时间管理。uC/OS-II的心脏节拍来自硬件定时器的周期中断每次中断调用一次OSTimeTick。这个函数的工作之一是遍历所有处于延时状态的任务把每个任务的延时计数器减1减到0的就把它重新置为就绪态。如果你要创建一个延时10个时钟节拍的任务调用OSTimeDly(10)后内核做的事情是把当前任务从就绪表中移除在任务TCB里记下延时计数OSTCBDly 10触发一次调度让出CPU。在到达延时期限之前这个任务就“不存在”于就绪表中自然不会被调度器选中。这就是“延时”在RTOS里的本质不是CPU在那傻等而是任务被暂时踢出调度候选名单。5.2 散列表与链表多个延时任务如何高效管理uC/OS-II为了管理不同延时时长的任务维护了一个以时钟节拍为模的哈希链表。每个时钟节拍到来时OSTimeTick会沿着当前节拍对应的链表把链表里所有任务的OSTCBDly减1减到0的就加入就绪表。如果延时时间跨度超过了一轮时钟节拍数量任务会被挂到合适的桶里每到一轮结束换个桶直到延时到期。我建议你阅读时重点看OSTimeTick里那两个分支一个是OSTCBDly 0的任务处理一个是OSTCBDly ! 0的处理。前者说明任务已到延时结束可以唤醒后者说明还没结束继续挂起。有个常见的坑是任务在OSTimeDly期间收到事件被提前唤醒但OSTimeTick并不知道它仍然会在遍历链表时访问这个任务节点。uC/OS-II在TCB里用OSTCBStat和OSTCBStatPend等字段来标记任务当前状态如果任务已经被其他途径唤醒OSTimeTick会通过状态判断跳过它。我自己在实际项目里遇到过一个问题把时钟节拍频率从100Hz调到1000Hz后某些任务莫名其妙地“跑快了”。排查到最后才发现是某个驱动里硬编码的“延时100个节拍”本来设计为1秒在1000Hz下变成了100毫秒。这里提醒各位凡是牵涉到节拍计数的宏命名里一定要带单位或者注释里写清基准频率不然迟早埋雷。5.3 空闲任务与统计任务调度器之外的隐形守护者uC/OS-II还有一个专门的任务叫空闲任务IDLE优先级最低当没有其他任务就绪时它就运行。它干的事情简单到可怜——给一个计数器加1。但这个计数器被系统启动时的一项统计功能拿来测量CPU使用率统计任务每秒钟采样空闲计数器的增量跟最大计数值一比就能得到“空闲时间比例”反推就是CPU占用率。有些人会觉得这个任务纯属浪费其实它是一个系统健康度观察哨。我在线上环境里就靠这个数字判断过任务负载是否过高、是否需要调整任务优先级或节拍频率。不要小看这两行看起来像“死循环”的代码。6. 信号量与事件控制块任务通信的枢纽是怎么运作的6.1 OS_EVENT结构体等待任务如何被挂到事件上任务与任务之间可以用信号量、互斥锁、消息邮箱等方式通信。uC/OS-II用一个统一的结构OS_EVENT来管理各种事件对象里面有个OSEventTbl数组用于记录“哪些任务正在等待这个事件”。先看一个信号量的等待过程任务A调用OSSemPend(sem, timeout)发现信号量计数为0内核把任务A从就绪表移除把任务A对应的事件等待标志位置到sem-OSEventTbl里触发调度让出CPU。当任务B调用OSSemPost(sem)释放信号量时内核会在OSEventTbl里找“最高优先级的等待任务”把它从等待队列移除然后恢复就绪状态。这里找最高优先级等待任务用的又是那张OSUnMapTbl查找表。所以你会发现只要会了就绪表的查找逻辑事件等待表的逻辑就是顺手的事。6.2 优先级翻转与互斥锁对优先级继承的实现这是很多RTOS面试爱问的题低优先级任务持锁高优先级任务等锁中等优先级任务把CPU抢走结果最高优先级的任务反而等了最久。这就是优先级翻转。uC/OS-II的解决方式是提供OSMutex互斥信号量并实现了优先级继承——当高优先级任务等待一个被低优先级任务持有的互斥锁时内核会临时把低优先级任务的优先级提升到高优先级任务的级别让持有锁的任务尽快运行并释放锁从而减少高优先级任务的等待时间。读OSMutexPend和OSMutexPost的源码时重点看两个点一是OSTCBPrio如何被临时改写二是在OSMutexPost里如何恢复原来的优先级。这两个修改点都会牵扯到就绪表的重算——TCB的优先级变了它所在的就绪位也要跟着迁移。这里最能体现“数据结构设计得好代码怎么写都顺畅”的道理。如果你用的是自己写的裸机调度器想支持优先级继承我建议先仔细读一遍uC/OS-II的OSMutexPost。它会先判断“当前持有者是否被提过优先级”再决定是直接恢复原优先级还是保持当前优先级这部分逻辑分支很多自己要造轮子时非常容易漏条件。6.3 一个排查死锁的实际案例我之前接手过一个产品两个任务A和B通过两个信号量互相配合但偶尔会出现“卡死”现象。当时全系统没有看门狗死锁后只能断电重启用户投诉很严重。后来我读了OSSemPend的实现试着画出两个任务各自持有的资源和等待的资源很快发现典型的循环等待任务A持有信号量1等信号量2任务B持有信号量2等信号量1。这就是教科书级的死锁。但代码层面怎么定位的呢我在OSSemPend里临时加了一个打印——打印当前任务优先级和等待的事件指针。卡死时把打印信息拉出来一看A、B两个任务的打印交替出现然后就停在各自的等待处真相一目了然。这件事给我的教训是不要等到用户反馈了才开始查调度问题而是应该在项目初期就在OS核心的几个入口处埋好轻量级的调试钩子。uC/OS-II允许在OS_CFG.H里打开OS_DEBUG相关的选项你甚至可以在自己的工程里为OSSemPend包一层宏统一做入口和出口日志这在量产问题的定位中能省下大量时间。7. 移植层源码的注意事项换一块MCU时哪些代码必须动7.1 与CPU相关的三个文件到底在干什么uC/OS-II的移植层通常涉及三个文件OS_CPU.H、OS_CPU_A.ASM、OS_CPU_C.C。初学者往往一头雾水不知道这三者的分工。我一句话帮你理清OS_CPU.H定义数据类型的宽度如INT8U到底对应unsigned char还是其他类型、临界区宏、栈增长方向、OS_TASK_SW的触发方式OS_CPU_A.ASM是真正干上下文切换的汇编代码包括OSCtxSw、OSIntCtxSw、OSTickISROS_CPU_C.C里是OSTaskStkInit和几个钩子函数负责初始化任务栈帧。如果你只是想在STM32这类Cortex-M内核上跑拿来主义地复制一份现成移植即可但如果你要迁移到其他CPU架构OS_CPU_A.ASM几乎是要完全重写的。7.2 栈帧初始化OSTaskStkInit的摆放顺序必须和切换代码一致这是移植过程中最容易踩坑的地方。OSTaskStkInit负责在任务栈里按指定顺序预置寄存器初值。这个顺序必须与OSCtxSw里弹栈的顺序完全一致。比如说OSCtxSw先弹R4-R12再弹PC那OSTaskStkInit压栈时就得先把PC的地址放在栈底依次放好其他寄存器最后让OSTCBStkPtr指向最低地址处。很多人在移植时改了其中一个却忘了改另一个结果任务第一次切换就跑飞。我的习惯是移植完成后写一个两个任务的demo任务1点亮LED任务2点亮另一颗LED然后反复切换。如果两个LED都能正常交替闪烁说明最基本的上下文切换是没问题的。这比写再多理论验证都直接。7.3 Cortex-M3/M4与经典ARM7在中断入口上的差异在Cortex-M系列上硬件会自动压栈一部分寄存器R0-R3、R12、LR、PC、xPSR这点跟ARM7要软件显式压栈完全不同。所以Cortex-M上的OSCtxSw实现看起来会“简单”一些——它甚至可以不碰R0-R3这些硬件已压栈的寄存器只需要保存R4-R11和几个特殊寄存器即可。这种情况下OSTaskStkInit里预置的伪栈帧也只需要考虑那些软件保存的寄存器。注意如果你拿的是网上某个Cortex-M移植版本一定要确认它是否在OSTaskStkInit里为硬件压栈的那些寄存器预留了位置。预留多了浪费RAM预留少了跑飞。这里没有标准答案一切以软硬件约定为准。8. 从6736行提炼出来的读码方法论我的个人经验8.1 别从头到尾线性读按“调用链”读很多人拿到源码喜欢按文件顺序从头翻到尾结果翻了几天还在头文件里打转。我的做法是反向读先找一个常用API比如OSSemPend然后用IDE的“跳到定义”功能一步步往里追。每条调用链追下去你会很自然地看到它在哪里调用了调度器、在哪里操作了就绪表、在哪里可能阻塞。这种“以API为入口、以调度器为终点”的读法能快速建立系统感。8.2 用调试器验证而不是只靠眼睛读读源码时顺手在关键函数名下断点观察变量的实时变化这比逐行分析要高效得多。比如OS_TASK_SW切换那一刻你把两个任务的OSTCBStkPtr打印出来和栈顶地址比对你会直观看到“SP怎么从A栈跳到了B栈”。我强烈建议新手做两个实验第一个实验创建三个不同优先级的任务各打印一个字符设不同的OSTimeDly观察打印顺序是否符合优先级规律。第二个实验在两个任务之间触发切换在OSCtxSw里断点手动查看CPU寄存器的变化。做这两个实验后你对调度器的理解会有一个质变。8.3 读老代码的正确心态少批评多问“为什么这么设计”uC/OS-II毕竟是上个世纪的老代码它有些写法在今天看来不够优雅全局变量用得很多、封装层次不高、某些地方用宏来模拟“面向对象”。但把它放到当时8位MCU的背景下这些设计都是合理的取舍。我在读它时学会的最重要的一件事是评价一套代码要看它当时的约束条件和目标场景。uC/OS-II追求的是在极有限的内存里提供确定性调度因此它选择位图就绪表、选择手动初始化栈帧、选择用汇编直接操作寄存器。这些选择都是为了性能与可预测性服务的。今天你在Cortex-M7上写RTOS内存和算力都宽裕了那完全可以用更工程化的方式做调度器——但底层那套“保存现场、切换栈、恢复现场”的思维模型永远不过时。如果你也想从业余爱好者往内核方向深入一步我建议顺着这条线继续往下读任务就绪表 → 调度器 → 时间管理 → 信号量/互斥锁 → 消息队列 → 内存管理。每读完一个模块回到裸机环境手写一个最小实现哪怕只有两个任务加一个延时也比看十遍源码更有收获。毕竟看懂一个内核最好的方式就是亲手重写一个缩水版的内核。
返回列表