调度器与处理器管理
在操作系统内核中,调度器(Scheduler)负责决定”下一个运行谁”,而处理器管理单元(Processor)负责维护”当前谁在运行”。本节将结合 kernel/src/task/manager.rs 和 kernel/src/task/processor.rs,深入剖析这两个核心组件。
TaskManager & Processor
Section titled “TaskManager & Processor”TaskManager 负责管理所有处于 Ready(就绪) 状态的任务。在简单的实现中,我们可以使用一个双端队列来维护,即一个关于 Arc<TaskControlBlock> 类型的 VecDeque 队列,TASK_MANAGER 是其全局实例。
其核心功能包括:
add(task): 将一个任务加入就绪队列的队尾。fetch(): 从就绪队列队头取出一个任务进行调度。
目前默认实现的调度策略是 FIFO (First-In-First-Out),即先入先出。代码中也预留了 stride_fetch() 接口,用于后续扩展 Stride Scheduling 等更高级的算法。
对于 Processor 则由 current:Option<Arc<TaskControlBlock>> 和 idle_task_cx:TaskContext 组成,分别表示当前处理器上正在执行的任务和当前处理器上 idle 控制流的任务上下文。PROCESSOR 是其全局实例。
简单来说,Processor 结构体用于描述 CPU 当前的运行状态,特别是它正在执行哪个任务。
成员 current 指向当前占用 CPU 的任务 TCB。如果 CPU 处于空闲或调度状态,该值为 None。而 idle_task_cx 是调度器线程 (Processor Loop) 的上下文。当任务被切换出去时,CPU 会跳转回这里,继续执行调度循环。
调度主循环 run_tasks
Section titled “调度主循环 run_tasks”run_tasks 在任务管理器 TASK_MANAGER 取出 (fetch_task) 相应任务后,进行上下文切换。由于用 loop 包裹,将会循环取出任务并进行任务切换,不过 __switch 后会切出这个函数。对于 __switch 函数,内核先把 current_task_cx_ptr 中的寄存器值保存在当前指针的经过偏移的地址下,再将 next_task_cx_ptr 的寄存器恢复,由此实现各寄存器值的切换。
而 schedule 由 suspend_current_and_run_next 调用,当应用交出 CPU 使用权后,实施将当前进程挂起为 TaskStatus::Ready 后的上下文切换操作,将当前上下文保存并切换为idle_task_cx的上下文。切换回去之后,内核将跳转到 run_tasks 中 __switch 返回之后的位置,也即开启了下一轮的调度循环。
add_initproc 在 main 中被调用,作用是将全局的 INITPROC 加入 TASK_MANAGER 中.
总得来说,run_tasks 是内核启动后在每个 CPU 上执行的无限循环,首先从 TaskManager 中 fetch 一个就绪任务,然后更新状态将该任务的状态从 Ready 改为 Running。记录当前状态时将任务 TCB 写入 processor.current。执行切换时调用 __switch(idle_task_cx, task.task_cx),从调度器线程切换到目标任务。
当任务主动让出 CPU (yield) 或执行完毕退出 (exit) 时,__switch 会再次被调用(由 trap handler 发起),将控制流切回 idle_task_cx,也就是回到了 run_tasks 循环中,准备调度下一个任务。
孤儿进程处理
Section titled “孤儿进程处理”为什么需要将子进程过继给 INITPROC?
在 Linux/Unix 语义中,父进程负责回收子进程的资源(waitpid)。如果父进程先于子进程退出,那么子进程就会成为”孤儿进程”。如果没有人回收它们,它们变成僵尸进程后将永久占用 PID 和内核栈,导致资源泄漏。
因此,我们将所有孤儿进程挂靠到系统初始进程 INITPROC 下,由 INITPROC 负责定期调用 waitpid 清理这些孤儿。