跳转到内容

调度器与处理器管理

在操作系统内核中,调度器(Scheduler)负责决定”下一个运行谁”,而处理器管理单元(Processor)负责维护”当前谁在运行”。本节将结合 kernel/src/task/manager.rskernel/src/task/processor.rs,深入剖析这两个核心组件。

TaskManager 负责管理所有处于 Ready(就绪) 状态的任务。在简单的实现中,我们可以使用一个双端队列来维护,即一个关于 Arc<TaskControlBlock> 类型的 VecDeque 队列,TASK_MANAGER 是其全局实例。

/// A array of `TaskControlBlock` that is thread-safe
pub struct TaskManager {
    ready_queue: VecDeque<Arc<TaskControlBlock>>,
}

其核心功能包括:

  1. add(task): 将一个任务加入就绪队列的队尾。
  2. fetch(): 从就绪队列队头取出一个任务进行调度。

目前默认实现的调度策略是 FIFO (First-In-First-Out),即先入先出。代码中也预留了 stride_fetch() 接口,用于后续扩展 Stride Scheduling 等更高级的算法。

对于 Processor 则由 current:Option<Arc<TaskControlBlock>>idle_task_cx:TaskContext 组成,分别表示当前处理器上正在执行的任务和当前处理器上 idle 控制流的任务上下文。PROCESSOR 是其全局实例。

简单来说,Processor 结构体用于描述 CPU 当前的运行状态,特别是它正在执行哪个任务。

/// Processor management structure
pub struct Processor {
    /// The task currently executing on the current processor
    current: Option<Arc<TaskControlBlock>>,
    /// The basic control flow of each core, helping to select and switch process
    idle_task_cx: TaskContext,
}

lazy_static! {
    pub static ref PROCESSOR: UPSafeCell<Processor> = unsafe { UPSafeCell::new(Processor::new()) };
}

成员 current 指向当前占用 CPU 的任务 TCB。如果 CPU 处于空闲或调度状态,该值为 None。而 idle_task_cx 是调度器线程 (Processor Loop) 的上下文。当任务被切换出去时,CPU 会跳转回这里,继续执行调度循环。

run_tasks 在任务管理器 TASK_MANAGER 取出 (fetch_task) 相应任务后,进行上下文切换。由于用 loop 包裹,将会循环取出任务并进行任务切换,不过 __switch 后会切出这个函数。对于 __switch 函数,内核先把 current_task_cx_ptr 中的寄存器值保存在当前指针的经过偏移的地址下,再将 next_task_cx_ptr 的寄存器恢复,由此实现各寄存器值的切换。

/// The main part of process execution and scheduling
/// Loop `fetch_task` to get the process that needs to run, and switch the process through `__switch`
pub fn run_tasks() {
    loop {
        let mut processor = PROCESSOR.exclusive_access();
        if let Some(task) = fetch_task() {
            ...
            unsafe {
                __switch(idle_task_cx_ptr, next_task_cx_ptr);
            }
        } else {
            warn!("no tasks available in run_tasks");
        }
    }
}

schedulesuspend_current_and_run_next 调用,当应用交出 CPU 使用权后,实施将当前进程挂起为 TaskStatus::Ready 后的上下文切换操作,将当前上下文保存并切换为idle_task_cx的上下文。切换回去之后,内核将跳转到 run_tasks 中 __switch 返回之后的位置,也即开启了下一轮的调度循环。

/// Suspend the current 'Running' task and run the next task in task list.
pub fn suspend_current_and_run_next() {
    ...
    task_inner.task_status = TaskStatus::Ready;
    ...
    add_task(task);
    schedule(task_cx_ptr);
}

/// Return to idle control flow for new scheduling
pub fn schedule(switched_task_cx_ptr: *mut TaskContext) {
    let mut processor = PROCESSOR.exclusive_access();
    let idle_task_cx_ptr = processor.get_idle_task_cx_ptr();
    drop(processor);
    unsafe {
        __switch(switched_task_cx_ptr, idle_task_cx_ptr);
    }
}

add_initprocmain 中被调用,作用是将全局的 INITPROC 加入 TASK_MANAGER 中.

/// Add init process to the manager
pub fn add_initproc() {
    add_task(INITPROC.clone());
}

总得来说,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 循环中,准备调度下一个任务。

为什么需要将子进程过继给 INITPROC

在 Linux/Unix 语义中,父进程负责回收子进程的资源(waitpid)。如果父进程先于子进程退出,那么子进程就会成为”孤儿进程”。如果没有人回收它们,它们变成僵尸进程后将永久占用 PID 和内核栈,导致资源泄漏。

因此,我们将所有孤儿进程挂靠到系统初始进程 INITPROC 下,由 INITPROC 负责定期调用 waitpid 清理这些孤儿。