项目地址:WillOS/WillOS/Inc/allocator.h at master · LieWill/WillOS
如果说 Allocator 是“地产商”,Context 是“瞬移魔法”,那么 Scheduler(调度器) 就是整个操作系统的灵魂——它是一个高效、严谨的“项目经理”,决定了在成百上千个任务中,此时此刻究竟谁有资格占用 CPU 这个极其珍贵的“会议室”。
在 WillOS/Src/scheduler.c 中,任务从出生到消亡的每一个瞬间都受其统筹。
1. 核心机理:环形链表(Ring)与公平正义
绝大多数 RTOS 使用简单的线性链表。但 WillOS 采用了 环形双向链表 (TCB Ring) 结构。
1.1 为什么要用“环”?
在一个分时系统中,如果两个任务优先级相同,它们应该“平分”时间片。
- 机理:当
SchedulerSwitch被调用时,它只需执行Ready = Ready->next。 - 效果:通过简单的指针移动,物理上实现了“轮转调度(Round-Robin)”。不需要复杂的数组重排,性能开销极低且恒定(O(1))。
2. 状态机:任务的四种“生存姿态”
调度器并不只是简单地管理“谁在跑”,它通过四个不同的环(队列)管理着任务的一生:
- 就绪态 (Ready):当前的“冲刺者”。这些任务优先级最高,且随时可以运行。它们在环里循环轮转。
- 低优先级态 (Lowpriority):排队中的“旁听生”。这是一个有序表,只有当
Ready环为空时,调度器才会从这里挑选最高权重的任务顶替上去。 - 阻塞态 (Blocked):正在“午休”的任务。
- 机理:利用
ticktime戳进行排序。每当系统滴答(SysTick)触发,经理会检查这个环的头部。 - 唤醒机理:如果“由于时间到了”,任务会被瞬间踢回
Ready环。
- 机理:利用
- 挂起态 (Suspended):处于“待岗”状态。它们不占用 CPU 资源,除非被显式唤醒(Detach)。
3. 运行流程:从“滴答”到“切换”
上下文切换的宏观轨迹如下:
- System Tick (心跳):硬件定时器触发
OsSwitch。 - 标记请求:调用
os_yield挂起 PendSV(请回顾 Context 篇的异步机制)。 - 核心调度逻辑 (
SchedulerSwitch):- 清理:检查是否有刚刚“辞职(Delete)”的任务需要回收内存。
- 超时巡检:遍历
Blocked环,唤醒时间到的任务。 - 优先级裁决:如果发现了更高优先级的任务,立即把当前的
Ready环降级到Lowpriority,让高优先级任务上位。 - 轮转:如果优先级一样,就切到环的下一个。
4. 高级特技:线程克隆 (Thread Clone)
WillOS 调度器中有一个极其罕见的特性:类似于 Unix 的 fork() 机理。
- 机理:通过
ThreadClone,它能完整复制一个正在运行的任务。 - 魔法核心:它不仅复制了任务的 TCB,还调用了
memcpy复制了整个任务栈。 - 伪造返回:它精细地手动构造了一个异常栈帧,让“子任务”醒来时,仿佛自己刚刚从
ThreadClone函数中返回,并且拿到了返回值为 0 的结果。
5. 特色机理:优雅的任务退出与返回值捕获
在大多数 RTOS(如 FreeRTOS)中,任务函数是禁忌返回的。但在 WillOS 中,你可以像写普通 C 函数一样让任务执行完毕并返回。
5.1 为什么 FreeRTOS 任务不能返回?
在 FreeRTOS 中,如果你让一个任务函数 return,由于没有地方接收这个返回动作,CPU 会跑飞(跳转到不可预知的内存地址),通常会触发一个 configASSERT(pdFALSE)。因此你必须显式调用 vTaskDelete(NULL) 来“自杀”。
5.2 WillOS 的“自动善后”机理
WillOS 在初始化任务栈(InitTaskStack)时,玩了一个精妙的视觉欺骗:
- 伪造 LR(返回地址):它在栈里把任务的返回地址(LR 寄存器)改成了
TaskReturn函数的地址。 - 捕获返回值:根据 C 调用约定,函数的返回值存在
R0寄存器里。当任务函数结束跳转到TaskReturn时,我们通过一小段内联汇编捕获R0的值,存入 TCB。 - 自动回收:完成值捕获后,
TaskReturn会自动调用Scheduler_Delete将任务从调度环中摘除。
对比优势:
- 符合直觉:任务就像一个普通的线程或函数。
- 结果传递:主线程可以通过
ThreadGetReturn拿回子任务算出来的结果,这在执行复杂并行计算时非常方便。
6. 承上启下:从“任务状态”到“任务协作”
至此,调度器已经完美解决了“谁在跑”和“跑多久”的问题。它能让 CPU 在不同的任务间切换,也能让任务休眠。
但是,调度器还有一个致命的盲区:
它虽然知道任务在“等”,但它不知道任务在“等什么”。
- 如果两个任务想抢同一个打印机(互斥锁)怎么办?
- 如果任务 A 想给任务 B 发一封电子邮件(信号量/消息队列)怎么办?
调度器本身不负责处理这些错综复杂的“人际关系”。这些高级的同步与通信逻辑,被交给了下一层——Coordinator(协作器)。