• 当没有任何其他线程时,idle 线程运行并循环检测是否能从线程池中找到一个可运行的线程,如果能找到的话就切换过去;
    • 当某个线程被调度器决定交出 CPU 资源并切换出去(如它已运行了很久,或它运行结束)时,并不是直接切换到下一个线程,而是先切换回 idle 线程,随后同样进行上述的循环尝试从线程池中找到一个可运行线程并切换过去。

    ProcessorInner

    在介绍 idle 线程的实现之前,我们先要将 idle 线程所需的各种资源封装在一起:

    我们需要 能够被全局访问,因为启动线程和调度线程 idle 以及 idle 所管理的线程都会访问它。在处理这种数据的时候我们需要格外小心。

      Processor

    我们在第四章内存管理中介绍内存分配器时也曾遇到过同样的情况,我们想要实现 static mut 的效果使得多个线程均可修改,但又要求是线程安全的。当时我们的处理方法是使用 spin::Mutex 上一把锁。这里虽然也可以,但是有些大材小用了。因为这里的情况更为简单一些,所以我们使用下面的方法就足够了。

    1. // src/process/processor.rs
    2. pub struct Processor {
    3. inner: UnsafeCell<Option<ProcessorInner>>,
    4. }
    5. unsafe impl Sync for Processor {}
    6. // src/process/mod.rs
    7. use processor::Processor;
    8. static CPU: Processor = Processor::new();

    这里面我们将实例 CPU 声明为 static 。编译器认为 Processor 不一定能够安全地允许多线程访问,于是声明一个 static 实例是会报错的。

    那么 mut 又在哪里?注意到我们使用 UnsafeCell<T> 来对 ProcessInner 进行了包裹,UnsafeCell<T> 提供了内部可变性 (Interior mutability),即使它本身不是 mut 的,仍能够修改内部所包裹的值。另外还有很多种方式可以提供内部可变性。

    接下来首先来看 Processor 的几个简单的方法:

    idle 线程与其他它所管理的线程相比有一点不同之处:它不希望被异步中断打断!否则会产生很微妙的错误。

    尤其是时钟中断,设想一个线程时间耗尽,被切换到 idle 线程进行调度,结果还没完成调度又进入时钟中断开始调度。这种情况想必很难处理。

    为此,在 idle 线程中,我们要关闭所有的中断,同时在在适当的时机恢复中断。下面给出几个函数:

    1. // src/interrupt.rs
    2. #[inline(always)]
    3. pub fn disable_and_store() -> usize {
    4. unsafe {
    5. // 返回 clear 之前的 sstatus 状态
    6. asm!("csrci sstatus, 1 << 1" : "=r"(sstatus) ::: "volatile");
    7. }
    8. sstatus
    9. }
    10. #[inline(always)]
    11. pub fn restore(flags: usize) {
    12. unsafe {
    13. // 将 sstatus 设置为 flags 的值
    14. asm!("csrs sstatus, $0" :: "r"(flags) :: "volatile");
    15. }
    16. }
    17. #[inline(always)]
    18. pub fn enable_and_wfi() {
    19. unsafe {
    20. // set sstatus 的 SIE 标志位启用异步中断
    21. // 并通过 wfi 指令等待下一次异步中断的到来
    22. asm!("csrsi sstatus, 1 << 1; wfi" :::: "volatile");
    23. }
    24. }

    接下来,我们来看 idle 线程的最核心函数,也是其入口点:

    所以我们打开并默默等待中断的到来。待中断返回后,这时可能有线程能够运行了,我们再关闭中断,进入调度循环。

    接下来,看看如何借用时钟中断进行周期性调用Processortick方法,实现周期性调度。当产生时钟中断时,中断处理函数rust_trap会进一步调用super_timer函数,并最终调用到Processor的方法。下面是`tick``方法的具体实现。

    1. // src/process/processor.rs
    2. impl Processor {
    3. let inner = self.inner();
    4. if !inner.current.is_none() {
    5. // 如果当前有在运行线程
    6. if inner.pool.tick() {
    7. // 如果返回true, 表示当前运行线程时间耗尽,需要被调度出去
    8. // 我们要进入 idle 线程了,因此必须关闭异步中断
    9. // 我们可没保证 switch_to 前后 sstatus 寄存器不变
    10. // 因此必须手动保存
    11. let flags = disable_and_store();
    12. // 切换到 idle 线程进行调度
    13. inner.current
    14. .as_mut()
    15. .unwrap()
    16. .1
    17. .switch_to(&mut inner.idle);
    18. // 之后某个时候又从 idle 线程切换回来
    19. // 恢复 sstatus 寄存器继续中断处理
    20. restore(flags);
    21. }
    22. }
    23. }
    24. }

    从一个被 idle 线程管理的线程的角度来看,从进入时钟中断到发现自己要被调度出去,整个过程都还是运行在这个线程自己身上。随后被切换到 idle 线程,又过了一段时间之后从 idle 线程切换回来,继续进行中断处理。

    当然 idle 线程也会进入时钟中断,但这仅限于当前无任何其他可运行线程的情况下。我们可以发现,进入这个时钟中断并不影响 idle 线程正常运行。

    接下来,一个线程如何通过 Processor 宣称自己运行结束并退出。这个函数也是在该线程自身上运行的。

    至此我们说明了调度线程 idle 以及调度单元 Processor 。但我们之前还挖了一个坑,也就是上一节中,调度算法我们只提供了一个接口但并未提供具体实现。下一节我们就来介绍一种最简单的调度算法实现。