恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

RTOS调度器源码拆解:uC/OS-II就绪表、抢占切换与信号量同步

  • 首页
  • 资讯中心
  • /
  • RTOS调度器源码拆解:uC/OS-II就绪表、抢占切换与信号量同步

相关资讯

Open Headunit UserExitHotspotPolicy:用户手动退出热点后的系统行为设计指南 2026/9/18 17:22:05
Hugo 图片滤镜应用指南:images.Filter 函数与 Resource.Filter 方法 2026/9/18 17:22:05
基于STM32的半导体制冷片PID温控与物联网远程监控系统 2026/9/18 17:22:05

最新资讯

SolidWorks智能小车底板设计:草图图片定标与尺寸链闭环全流程
ChatGPT 提示 Unable to locate Codex CLI?TaoToken 这样改 Codex 的模型通道
《神经网络与深度学习》蒲公英书:12周学习路线与代码推导指南
Redis基础教程:从数据类型到Spring Boot实战
AI芯片设计从入门到不放弃:矩阵乘加、脉动阵列与FPGA实战
Redis高可用三大模式选型指南:主从、哨兵、Cluster实战决策

今日推荐

2026年AI设计工具在PPT制作中的核心应用与评测
Matlab手写逻辑回归:从数学原理到多变量概率预测模型实现
高值医用耗材研报PDF:用Python完成字段抽取、清洗与趋势预测

本周热门

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化
Flutter应用改名全指南:从Android到iOS的配置与工具实践

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

RTOS调度器源码拆解:uC/OS-II就绪表、抢占切换与信号量同步

发布时间:2026/9/18 17:22:05
RTOS调度器源码拆解:uC/OS-II就绪表、抢占切换与信号量同步 1. 第10篇为什么死磕调度器把 RTOS 的心跳扒开看翻 uC/OS-II 内核源码翻到第 10 篇如果还没把调度器吃透那前面几篇基本算白读了。uC/OS-II 全部内核 C 代码摊开也就六千多行刨掉注释、空行和一堆条件编译真正干活的核心逻辑更少。但就在这有限的行数里OSSched、OSIntExit、OS_SchedNew、OS_TASK_SW 这几个函数决定了整个系统里几十个任务谁先跑、谁等一等、谁被抢占。可以这么讲前面讲的时钟节拍、任务创建、TCB 链表都是给调度器准备食材真正下锅炒菜的就是调度这一层。这篇要解决的核心问题很直接一个只有 8 位变量的就绪表凭什么能在几个机器周期内找出最高优先级任务占先式调度到底在哪一行代码触发的任务级切换和中断级切换为什么要写成两套汇编入口我把这些问题一个个拆开配上 uC/OS-II 的真实源码片段和 GD32F103 上的实测现象让你看完能自己在板子上验证一遍调度行为。适合已经有 C 语言和单片机基础、正在啃 RTOS 内核源码或者准备 RTOS 相关面试的嵌入式同行。新手也别怕我会把位运算、查表法这些东西用生活化的例子讲清楚保证你能跟上。2. 就绪表8 位变量撑起 64 个任务的调度底座2.1 OSRdyGrp 与 OSRdyTbl 的位图设计uC/OS-II 支持最多 64 个任务OS_LOWEST_PRIO 63优先级数字越小优先级越高。它管理哪些任务当前可以运行的方式不是搞一个数组一个个扫而是用位图bitmap。核心就两个东西INT8U OSRdyGrp; /* 每个 bit 代表一组 8 个优先级里有没有就绪任务 */ INT8U OSRdyTbl[OS_RDY_TBL_SIZE]; /* 每组 8 个优先级的就绪状态 */把 64 个优先级切成 8 组每组 8 个。OSRdyTbl[0]管优先级 0~7OSRdyTbl[1]管 8~15以此类推。某个任务就绪了就把对应位置 1。举个例子优先级 11 的任务就绪组号 11 3 1所以操作OSRdyTbl[1]位号 11 0x07 3所以把OSRdyTbl[1]的 bit3 置 1同时OSRdyGrp的 bit1 也要置 1表示第 1 组里有就绪任务这就像一栋楼有 8 层每层 8 个房间OSRdyGrp是楼下大厅的指示灯告诉你哪几层有人OSRdyTbl是每层的走廊灯告诉你具体哪个房间有人。要找最高优先级任务先看大厅哪层灯亮了组号最小的再看那层哪个房间灯亮了位号最小的拼起来就是最高优先级。这个设计最妙的地方是查找最高优先级的时间是固定的跟任务数量无关这正是实时系统最在意的东西。2.2 OSUnMapTbl 查表法5 毫秒讲明白它为什么快如果让你用循环去找一个字节里最低的置 1 位你可能得写个 for 循环扫 8 次。uC/OS-II 不这么干它用一张256 字节的常量表 OSUnMapTbl一次查表搞定。表长这样INT8U const OSUnMapTbl[256] { 0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x00 ~ 0x0F */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x10 ~ 0x1F */ /* ... 中间省略规律一直延续到 0xFF */ };这张表的意思很简单OSUnMapTbl[n]返回的是 n 这个字节里最低有效置 1 位的位置从 0 数起。比如OSUnMapTbl[0x01] 0OSUnMapTbl[0x02] 1OSUnMapTbl[0x04] 2OSUnMapTbl[0x08] 3OSUnMapTbl[0x03] 0因为 0x03 的二进制是 0000 0011最低置 1 位在第 0 位。于是找最高优先级变成两步查表y OSUnMapTbl[OSRdyGrp]; /* 先找出哪个组 */ x OSUnMapTbl[OSRdyTbl[y]]; /* 再找出组里哪个位 */ prio (INT8U)((y 3) x); /* 拼成最终优先级 */我第一次看到这段的时候觉得有点绕后来想通了OSUnMapTbl就是用 256 字节的 ROM 空间换掉了循环和分支判断带来的不确定性。对 RTOS 来说ROM 便宜、时间贵这笔买卖划算。而且每次调度只查两次表加起来几个周期实时性直接拉满。你也可以自己动手生成这张表写个小脚本把 0~255 每个数跑一遍求最低置 1 位打印成 C 数组这个过程本身就能帮你彻底理解位图。2.3 就绪表操作宏加入和删除就绪任务uC/OS-II 把就绪表的操作封装成三个宏藏在OS_CORE.C里。任务就绪时调用#define OS_RDY_TBL_SET(prio) do { \ OSRdyGrp | OSMapTbl[(prio) 3]; \ OSRdyTbl[(prio) 3] | OSMapTbl[(prio) 0x07]; \ } while (0)这里又出现一张表OSMapTbl它是OSUnMapTbl的反向用途——给你一个位号 0~7返回对应的独热码只有该位为 1 的字节。OSMapTbl[3] 0x08OSMapTbl[1] 0x02。两个宏配合一个管组位一个管组内位。删除就绪任务则是反过来#define OS_RDY_TBL_CLR(prio) do { \ if ((OSRdyTbl[(prio) 3] ~OSMapTbl[(prio) 0x07]) 0) { \ OSRdyGrp ~OSMapTbl[(prio) 3]; \ } \ } while (0)注意那个if判断只有在某个组里的最后一位被清掉时才去清OSRdyGrp对应的组位。这个细节很关键忘了写会导致OSRdyGrp和OSRdyTbl状态不一致调度器就会算出个根本不就绪的优先级然后系统跑飞。我当年移植时就踩过这个坑任务挂起后偶尔会莫名其妙切到一个死任务上查了半天才发现是就绪表删除逻辑写漏了同步。说实话理解就绪表的核心就一句话用位运算把查找最高优先级从 O(n) 降到 O(1)。这是 uC/OS-II 能在低端单片机上跑得飞起的关键设计之一也是它跟很多任务链表轮询式轻量调度器的本质区别。3. 调度器 OSSched 与中断级 OSIntExit 源码逐行拆解3.1 OSSched 的三大关卡中断、锁、优先级比较任务级调度的入口是OSSched。源码不长但每一行都有讲究void OS_Sched (void) { OS_ENTER_CRITICAL(); if (OSIntNesting 0) { /* 关卡一不在中断里 */ if (OSLockNesting 0) { /* 关卡二调度没被锁住 */ OS_SchedNew(); /* 算出最高优先级任务 */ OSTCBHighRdy OSTCBPrioTbl[OSPrioHighRdy]; if (OSPrioHighRdy ! OSPrioCur) { /* 关卡三确实需要切换 */ OS_TASK_SW(); /* 触发任务级切换 */ } } } OS_EXIT_CRITICAL(); }三个关卡依次回答三个问题现在能调度吗调度被禁了吗换不换OSIntNesting是中断嵌套计数只要大于 0 说明当前在中断服务程序里任务级调度必须让路等中断退出时由OSIntExit来管。OSLockNesting是调度锁计数很多内核函数在操作临界资源时会临时禁调度防止中途被打断。OS_SchedNew干的就是上一节那套查表活void OS_SchedNew (void) { INT8U y OSUnMapTbl[OSRdyGrp]; OSPrioHighRdy (INT8U)((y 3) OSUnMapTbl[OSRdyTbl[y]]); }算出来的OSPrioHighRdy通过OSTCBPrioTbl[]数组直接映射到任务控制块指针OSTCBHighRdy。这个OSTCBPrioTbl是个按优先级索引的指针数组用空间换时间避免了遍历链表。最后一句if (OSPrioHighRdy ! OSPrioCur)也很重要如果即将运行的还是当前任务那就别折腾了省下一次上下文切换的开销。每一次无谓的切换都是浪费 CPU 周期在实时系统里这种优化积少成多。3.2 任务级 OSCtxSw 与中断级 OSIntCtxSw 为何分两套很多初学者会问都是切换任务为啥要搞OSCtxSw和OSIntCtxSw两个汇编入口答案藏在栈里。OS_TASK_SW()是个宏通常在 ARM 上映射成一条软中断指令Cortex-M3 上是SVC。执行到它时当前任务的现场还在 CPU 寄存器里需要在OSCtxSw里手动把 R4~R11、PSP 等压栈保存再去恢复新任务的栈。而OSIntCtxSw是从OSIntExit里调用的此时硬件已经在进中断时自动压了一部分寄存器Cortex-M3 自动压 xPSR、PC、LR、R12、R3~R0现场已经有一部分在栈上了。所以OSIntCtxSw不能再重复压一遍它要做的只是把剩下的寄存器补上然后直接切换栈指针、恢复新任务。一句话总结任务级切换是干净的切换中断级切换是脏的切换现场已经被动压过一部分处理方式不同。这也是为什么很多人在 Cortex-M3 上移植时直接用 PendSV 统一处理两种切换——把两者都挂到 PendSV 异常上借用它的延迟切换特性能巧妙地避开在中断里切任务带来的时序麻烦。但原理上你得分得清这两个入口各自的职责。3.3 优先级查找的一个边界坑OSRdyGrp 为 0 时这里有个容易被忽略的边界如果OSRdyGrp 0说明一个就绪任务都没有那OSUnMapTbl[0]返回 0OSRdyTbl[0]大概率也是 0OSUnMapTbl[0]又是 0最后算出OSPrioHighRdy 0。这在正常系统里不会发生因为空闲任务OS_TaskIdle永远处于就绪态兜底保证至少有一个就绪任务。但如果你把空闲任务也删了或者在系统还没启动时就调用调度就会踩这个坑表现为系统莫名切到优先级 0 的任务上然后跑飞。我在调试一个裁剪版内核时为了省空间把空闲任务的栈开得极小结果某个场景下空闲任务栈溢出把OSRdyGrp附近的内存踩了OSRdyGrp变成 0系统直接挂死。空闲任务不是可有可无的摆设它是调度器的安全网别随便动它。4. 信号量同步从 OSSemPend 到触发调度的全过程4.1 事件控制块 ECB 与信号量结构uC/OS-II 里信号量、邮箱、消息队列、事件标志组共用同一个结构——事件控制块 OS_EVENTtypedef struct os_event { INT8U OSEventType; /* 事件类型信号量/邮箱/队列... */ INT8U OSEventGrp; /* 等待任务组位图 */ INT16U OSEventCnt; /* 信号量的计数值 */ void *OSEventPtr; /* 邮箱/队列用的消息指针 */ INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待任务位图 */ } OS_EVENT;看到没OSEventGrp和OSEventTbl又是一对位图跟就绪表一模一样的套路。为什么因为**等待某个信号量的任务集合本质上和就绪任务集合是同构的数据结构**都是在一堆任务里找出优先级最高的那个。uC/OS-II 把这套位图查找逻辑复用了两遍一遍用于调度一遍用于事件等待。理解了就绪表事件等待表就是换个马甲。信号量的核心就是OSEventCnt这个计数器。它大于 0 表示有可用资源等于 0 表示没资源任务得排队等。二值信号量计数只有 0 和 1计数信号量可以到 65535。4.2 OSSemPend 源码拿不到资源就挂起自己OSSemPend的逻辑可以分成顺利拿到和拿不到排队两部分void OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *perr) { if (pevent-OSEventType ! OS_EVENT_TYPE_SEM) { *perr OS_ERR_EVENT_TYPE; return; } OS_ENTER_CRITICAL(); if (pevent-OSEventCnt 0) { /* 有资源直接拿走 */ pevent-OSEventCnt--; OS_EXIT_CRITICAL(); *perr OS_ERR_NONE; } else { /* 没资源排队挂起 */ OSTCBCur-OSTCBStat | OS_STAT_SEM; OSTCBCur-OSTCBDly timeout; OS_EventTaskWait(pevent); /* 把自己塞进等待位图 */ OS_EXIT_CRITICAL(); OS_Sched(); /* 主动让出 CPU切走 */ OS_ENTER_CRITICAL(); /* 被唤醒后清理等待状态 */ OSTCBCur-OSTCBStat (INT8U)~OS_STAT_SEM; OSTCBCur-OSTCBDly 0; OS_EXIT_CRITICAL(); *perr OSTCBCur-OSTCBStat; } }关键那一句OS_Sched()任务发现自己拿不到信号量主动调用调度器把自己换下去让别的任务跑。这就是 RTOS 跟裸机轮询的本质区别——不浪费 CPU 空转直接切走。被唤醒的时机是别人调用了OSSemPost释放信号量或者超时时间到了被时钟节拍唤醒。4.3 OSSemPost 源码释放资源后立刻触发调度INT8U OSSemPost (OS_EVENT *pevent) { OS_ENTER_CRITICAL(); if (pevent-OSEventGrp ! 0x00) { /* 有人在等 */ OS_EventTaskRdy(pevent, (void *)0, OS_STAT_SEM); /* 唤醒最高优先级等待者 */ OS_EXIT_CRITICAL(); OS_Sched(); /* 立刻调度可能发生抢占 */ return OS_ERR_NONE; } if (pevent-OSEventCnt 65535) { /* 没人在等计数1 */ pevent-OSEventCnt; OS_EXIT_CRITICAL(); return OS_ERR_NONE; } OS_EXIT_CRITICAL(); return OS_ERR_SEM_OVF; /* 计数溢出 */ }OS_EventTaskRdy里那套查找逻辑跟OS_SchedNew如出一辙先用OSUnMapTbl找出等待任务里优先级最高的把它从等待位图移到就绪位图设置OSTCBStat。然后OSSemPost立刻调OS_Sched()——如果被唤醒的任务优先级比当前任务高这里就会发生抢占式切换。这里就是优先级反转问题的高发区。假设低优先级任务 L 拿着信号量高优先级任务 H 在等这个信号量中优先级任务 M 就绪了。M 优先级比 L 高于是 M 一直抢 L 的 CPUL 没法执行、没法释放信号量H 就一直等——H 被 M 活活饿死明明它优先级最高。uC/OS-II 的解决办法是互斥量Mutex里的优先级继承OSMutexPend在发现资源被占时会把占用者的优先级临时提升到跟自己一样高让占用者尽快执行完释放资源。信号量OSSemPend本身不提供这个机制这也是为什么保护临界资源要用互斥量而不是普通信号量——这是个面试高频考点。5. 实操在 GD32F103 上移植并验证调度行为5.1 移植清单与堆栈初始化要点GD32F103 是 Cortex-M3 内核跟 STM32F103 的移植路径基本一致。需要你动手写的只有三个文件文件作用关键内容OS_CPU.H数据类型、临界区宏、栈增长方向OS_STK_GROWTH1向下增长、开关中断宏OS_CPU_C.C任务栈初始化、钩子函数OSTaskStkInit造任务第一次运行的假现场OS_CPU_A.ASM上下文切换汇编OSStartHighRdy、OSCtxSw、OSIntCtxSw、OSTickISROSTaskStkInit是移植的重头戏。它要在任务栈里提前摆好一份现场让任务第一次被调度时出栈恢复寄存器的动作能无缝跳进任务函数。Cortex-M3 上大概是这样一段OS_STK *OSTaskStkInit (void (*task)(void *pd), void *p_arg, OS_STK *ptos, INT16U opt) { OS_STK *stk ptos; *stk-- (OS_STK)0x01000000uL; /* xPSRThumb 位必须置 1 */ *stk-- (OS_STK)task; /* PC任务入口地址 */ *stk-- (OS_STK)0xFFFFFFFDuL; /* LR返回地址故意填个非法值 */ *stk-- (OS_STK)0x12121212uL; /* R12 */ *stk-- (OS_STK)0x03030303uL; /* R3 */ *stk-- (OS_STK)0x02020202uL; /* R2 */ *stk-- (OS_STK)0x01010101uL; /* R1 */ *stk-- (OS_STK)p_arg; /* R0任务参数 */ /* 后面继续压 R4~R11 */ ... return stk; }那个0x01000000的 xPSR 值特别关键Thumb 位bit24必须为 1否则 Cortex-M3 一跳过去就进 HardFault。我第一次移植时忘了这一位程序一启动就跑飞单步调试看了半天才反应过来。这不是 uC/OS-II 的问题是 Cortex-M 架构的强制要求。5.2 用 GPIO 翻转实测任务切换理论讲再多不如上板子看现象。我常用的验证方法是在两个任务里各翻转一个 GPIO用示波器或者逻辑分析仪抓波形void Task_High (void *p_arg) { while (1) { GPIO_BOP(GPIOC) GPIO_PIN_13; /* 拉高 PC13 */ GPIO_BC (GPIOC) GPIO_PIN_13; /* 拉低 PC13 */ OSTimeDly(1); /* 延时 1 个节拍 */ } } void Task_Low (void *p_arg) { while (1) { GPIO_BOP(GPIOA) GPIO_PIN_0; /* 拉高 PA0 */ GPIO_BC (GPIOA) GPIO_PIN_0; /* 拉低 PA0 */ OSTimeDly(1); } }如果调度正常你会看到两个方波交替出现切换周期基本等于你的 SysTick 节拍周期。把Task_High优先级设得比Task_Low高改成两个任务同时就绪都不延时你会看到Task_High的方波占满整个时间轴Task_Low一点都不出——这就是占先式调度的直观体现高优先级任务不吃让 CPU。反过来做对比实验把Task_High里的OSTimeDly去掉改成死循环你会发现Task_Low完全饿死。这个现象能让你对优先级抢占建立起肌肉记忆比看一百遍源码都管用。5.3 关键参数计算与配置移植时几个参数得算清楚配错了实时性会崩#define OS_TICK_PER_SEC 1000 /* SysTick 每秒中断 1000 次 */ #define OS_TASK_TMR_PRIO (OS_LOWEST_PRIO - 2) /* 软件定时器任务优先级 */ #define OS_TASK_STAT_EN 1 /* 打开统计任务方便看 CPU 利用率 */OS_TICK_PER_SEC决定了时间精度。1000Hz 意味着每个节拍 1msOSTimeDly(1)就是延时 1ms。节拍频率越高时间粒度越细但中断开销越大。GD32F103 跑 108MHz1kHz 节拍下中断处理占用 CPU 不到 1%完全扛得住。但如果你把它设成 10kHz光 SysTick 中断就可能吃掉 5% 以上 CPU对低功耗场景很不友好。OS_TASK_STAT_EN打开后统计任务会定期算 CPU 利用率调试时通过看OSCPUUsage变量就能知道系统忙不忙。这个功能本身要消耗一点 CPU大概 1%产品发布时一般关掉。6. 常见问题与排查技巧实录6.1 常见问题速查表现象可能原因排查方法上电就进 HardFault任务栈 xPSR 的 Thumb 位没置 1检查OSTaskStkInit首字任务切过去就飞栈空间不够压栈踩到别的内存加大栈、用OSTaskStkChk查剩余某个任务永远不运行优先级被别的任务挡死、或忘了OSTimeDly让出用 GPIO 转翻测、查就绪表信号量释放后不切换OSSemPost里OSIntNesting非 0确认是在任务里而不是中断里调用系统偶尔卡死就绪表增删没同步OSRdyGrp单步查OSRdyGrp/OSRdyTbl一致性抢占不及时OSLockNesting忘了解锁检查所有OSSchedLock是否配对OSSchedUnlock6.2 独家避坑心得心得一临界区不要开太大。我见过有人为了方便把整个任务函数体用OS_ENTER_CRITICAL包起来结果中断全被关了系统跟裸机轮询没区别。临界区只应该包住真正操作共享数据的那几行能短则短。心得二栈大小按最坏情况给。局部数组、递归、浮点运算都会吃栈。我现在的习惯是先用OSTaskStkChk跑一遍极限工况量出实际用量再乘个 1.5 倍留余量别抠那点 RAM。心得三OSIntExit才是中断里真正的调度点。很多人以为OSSemPost在中断里调用后会立刻切换其实不会得等OSIntExit执行完OSIntNesting减到 0才触发OSIntCtxSw。理解这一点才能解释为什么中断里发信号量任务要等中断退出才切过去。心得四OSTimeDly是最简单的主动让出。一个死循环里不带任何阻塞调用的任务会把同优先级及以下的所有任务全部饿死。养成习惯每个循环体里至少有个OSTimeDly或OSPend之类的阻塞点。6.3 RTOS 面试高频问题串讲顺着这篇讲调度和信号量几个经典面试题可以顺手过一遍。RTOS 和 Linux 调度有什么区别——RTOS 是固定优先级抢占式追求的是确定性最坏情况响应时间可预测Linux 是 CFS 完全公平调度追求吞吐量和公平性实时性靠打了 PREEMPT_RT 补丁才能勉强够用。信号量和互斥量区别——信号量是同步机制可用于任务间或中断与任务间互斥量有优先级继承和所有权概念专门保护临界资源。为什么要用位图查最高优先级——因为要 O(1) 确定性查找不能有循环带来的抖动。这些问题答的时候能把内核里具体哪一行代码干什么说出来面试官基本就服了。我个人这几年啃 uC/OS-II 内核源码最大的体会是看懂调度器那一两百行比看完整本书都值。因为它是整个系统的决策中枢就绪表怎么组织、优先级怎么找、切换什么时候发生、信号量怎么触发调度全都绕着它转。你在 GD32F103 上把 GPIO 翻转实验跑通一遍再回头对照源码看OSSched和OSIntExit很多以前觉得玄乎的东西就落地了。下一篇我打算接着往下钻聊聊消息队列和内存分区管理那两块是很多人移植时更容易翻车的地方尤其是内存分区配上信号量做生产者消费者模型的时候坑特别多。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号