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

【系列:uC/OS-II 内核源码精读:从 6736 行代码看懂一个 RTOS · 第 2 篇】 任务不是函数而是一段带栈、带优先级的死循环代码。本文基于 uC/OS-II 源码实测拆解 OS_TCB 的字段设计、OSTaskCreate 的完整流程与初始栈帧的伪装术并梳理任务状态机——为下一篇就绪表与 O(1) 调度提前埋好伏笔。上篇我们把 uC/OS-II 的六大内核对象认了个脸熟任务稳坐 C 位。今天跟着源码把任务从概念拽到实现看看它出生、登记、上岗的全过程。这篇读完后你会彻底明白三件事任务到底是什么内核怎么记录一个任务一个任务从创建到就绪中间走了哪几步任务不是函数是一段回不了头的死循环去看 os_task.c 开头的注释官方对任务的定义非常简单直白voidTask(void*p_arg){for(;;){任务代码;}}任务就是一个死循环函数 一块栈 一个优先级。循环让任务永远活着栈让任务有独立的运行现场优先级决定它和别的任务抢 CPU 时谁先上车。这里有一个新手最容易踩的坑任务函数不能 return。普通函数 return 是回家任务函数 return 是人间蒸发——uC/OS-II 没有给任务设计返回路径一旦让任务函数走到结尾最常见的结果是触发 HardFault或者任务栈被永久悬挂系统慢慢变得诡异。写个 LED 闪烁任务感受一下voidled_task(void*p_arg){while(1){LED_ON();OSTimeDly(500);/* 延时让出CPU */LED_OFF();OSTimeDly(500);}}这段代码从出生就注定不能回头只能靠 OSTaskDel 或系统停摆来结束。OS_TCB每个任务都有一本档案任务创建后内核凭什么认识它靠OS_TCBTask Control Block任务控制块——你可以把它理解成任务的档案袋所有信息归档在 ucos_ii.h 的 OS_TCB 结构体里。挑几组核心字段看OSTCBPrio优先级0 最高OSTCBStkPtr栈顶指针——上下文切换时第一步就是从这个指针恢复寄存器OSTCBNext / OSTCBPrev双向链表节点所有 TCB 被串成一条链内核遍历任务时就沿着这条链走OSTCBEventPtr指向等待的事件块不等待时为 0OSTCBMsg存放收到的邮箱/队列消息OSTCBDly是延时 tick 数或等待超时 tick 数OSTCBStat记录任务当前状态就绪/等待/挂起…OSTCBStatPend记录等待结果OK/超时/中止OSTCBY / OSTCBX / OSTCBBitY / OSTCBBitX这四个是就绪表位图的缓存索引下一篇的主角先记下名字扩展配置下还有栈底指针、栈大小、任务名、寄存器表等字段本质都是把任务的所有信息装进一个结构体。OSTaskCreate一次四步的流水线签约任务出生走的是OSTaskCreate()签名长这样INT8UOSTaskCreate(void(*task)(void*p_arg),void*p_arg,OS_STK*ptos,INT8U prio)四个参数函数指针、参数、栈顶指针、优先级。内部流程像一条传送带六个工位体检工位prio超范围直接返回OS_ERR_PRIO_INVALID。禁区闸机如果当前在中断服务程序里OSIntNesting 0直接拒绝返回OS_ERR_TASK_CREATE_ISR——任务不能在中断里创建。中断是临时借道里面没有完整的调度环境。占坑登记检查OSTCBPrioTbl[prio]是否为空确认优先级没被占用然后立刻把该位置为OS_TCB_RESERVED1。这个先占坑很关键防止并发环境下两个任务抢同一优先级。栈帧冲压调用psp OSTaskStkInit(task, p_arg, ptos, 0u)。这是移植层提供的函数由 CPU 架构决定。它干什么见下一节。TCB 装配OS_TCBInit(prio, psp, ...)把 TCB 填好并置位就绪表。这一步执行完任务正式可运行。调度放行如果系统已经跑起来OSRunning TRUE立刻调OS_Sched()——让调度器看一眼新任务说不定它优先级够高马上就能抢到 CPU。失败回滚任何一步出错释放占位优先级还回去。初始栈帧让 CPU 以为任务刚被打断这是整个创建流程里最值得琢磨的一环单独讲。栈Stack是任务私有的内存区用来保存局部变量、函数调用现场。而初始栈帧Initial Stack Frame是OSTaskStkInit在栈上预先摆好的一组数据。它做的事很心机把栈伪装成任务刚被中断打断的样子。想象一下CPU 在跑任务 A来了一个中断硬件自动把 A 的寄存器值压栈中断返回时再弹栈恢复现场。OSTaskStkInit就是把这个被打断的过程提前演练一遍——按 CPU 的寄存器顺序把精心构造的初始值压进新任务的栈里。其中最关键的一压任务函数的入口地址。这样第一次上下文切换时调度器执行恢复现场操作PC程序计数器弹出来的就是任务入口任务嗖地跑起来了——好像它一直就在那儿运行只是刚才被中断了一下。p_arg参数也由这个函数压栈因此任务第一次运行就能拿到参数。注意栈帧布局完全取决于 CPU。ARM 压 R0-R12、LR、PCMIPS、x86 各不相同。所以OSTaskStkInit一定在移植文件里由芯片厂商或社区实现内核本体不碰这些细节。任务的一生是一张火车运行图任务创建后开始了它的一生。uC/OS-II 用OSTCBStat的状态位来描述OS_STAT_RDY0x00/* 就绪 */OS_STAT_SEM0x01/* 等待信号量 */OS_STAT_MBOX0x02/* 等待邮箱 */OS_STAT_Q0x04/* 等待队列 */OS_STAT_SUSPEND0x08/* 挂起 */OS_STAT_MUTEX0x10/* 等待互斥量 */OS_STAT_FLAG0x20/* 等待事件标志组 */OS_STAT_MULTI0x80/* 等待多个对象 */把任务的一生画成一张运行图创建之后登上轨道就绪表置位在轨道上排队等 CPU调度器扳道岔让优先级最高的任务上车进入运行态遇到红灯等待信号量、延时停进车站侧线进入等待态事件到来重新回到轨道等下一次调度。有意思的是挂起状态可以叠加OSTaskSuspend挂起的任务等待信号量时状态是OS_STAT_SUSPEND | OS_STAT_SEM两个状态位同时置 1。这也是位图设计的优势一个字节能表达多种状态的组合。删除任务更是狠活任务可以删除别人甚至可以删除自己第 10 篇展开。写在结尾一张就绪表的伏笔细心的你可能发现了OS_TCBInit里有一行预计算OSTCBYprio3;OSTCBXprio0x07;OSTCBBitY1OSTCBY;OSTCBBitX1OSTCBX;这四行在任务创建时就把位置算好、缓存在 TCB 里调度时直接查表、置位省去现场计算。第 3 篇我们就啃这块硬骨头就绪表Ready Table的位图结构以及调度器如何用它们实现 O(1) 的查表调度。预告一个问题uC/OS-II 用一行OSRdyGrp | OSTCBBitY;就把任务标记为可运行那调度时如何在几十个任务中瞬间找到最高优先级答案是位图 查表法下篇见。你在调试 RTOS 时有没有遇到过任务神秘消失的情况欢迎在留言区聊聊你踩过的坑。觉得有用就点个关注后面 8 篇继续更新。