线程管理模块

【部分内容已经过时】模块经过了重构,支持多核调度

概述

线程管理模块的职责是完成CPU资源的管理。它建立在硬件提供的中断及上下文切换机制之上,提供线程的管理和调度功能。此模块功能相当于ucore lab4。

注意它不负责管理线程使用的其他资源(如内存、文件),也不了解用户态和内核态的切换(由使用者构造合适的上下文来解决)。目前的设计基于单核CPU,未来扩展到多核时可能需要重新调整。

具体而言,使用者只需实现底层的上下文切换过程,即可享用内核线程的功能。此模块提供了直接操作线程的接口,如fork、exit、wait,用于实现系统调用。在此基础上,还封装了和std::thread完全相同的上层接口,使得用户态的Rust多线程程序可以方便地迁移到内核中。

实现

此模块作为独立的crate存在,代码位于:crate/process

src目录结构及各文件功能如下:

.
├── lib.rs          根模块
├── processor.rs    一个CPU核的管理器
├── scheduler.rs    调度算法
├── thread.rs       实现std::thread接口
├── event_hub.rs    简单事件管理器
└── util.rs         其它工具

理论上可以在Linux用户态实现一个模拟的上下文切换,进而编写单元测试,但这个想法尚未实现。

上下文接口

位于processor.rs,只有一个很简单的trait:

processor.rs
pub trait Context: Debug {
    /// 将当前CPU切换到另一个上下文
    unsafe fn switch(&mut self, target: &mut Self);
    
    /// 构造一个新的内核态上下文
    /// 只用于实现thread::spawn
    fn new_kernel(entry: extern fn(usize) -> !, arg: usize) -> Self;
}

这个上下文实际上是保存在内核栈上的寄存器和中断帧TrapFrame,切换switch过程实际就是一段保存和恢复寄存器的汇编代码。

读者可分别查看它在x86_64和RISCV32下的实现,了解具体使用方法:

  • arch/x86_64/interrupt/trapframe.rs

  • arch/riscv32/context.rs

为了创建一个新的线程,使用者需要分配好一个内核栈,并精心构造栈顶的初始数据,使得switch后能正确设置CPU状态。Context的构造函数需由使用者自己定义,不会在接口中体现。RustOS中实现了三种构造函数,分别用于创建内核线程、用户线程、fork已有线程。

这套接口是面向软件上下文切换设计的,如果CPU支持硬件切换上下文,可能实现会更简单。

关于上下文切换的细节和原理,可参考 第一个进程 | xv6 中文文档

调度器

位于scheduler.rs,首先定义了接口:

scheduler.rs
/// 调度器接口
/// 本质是一个就绪态线程的优先队列
pub trait Scheduler {
    /// 添加一个就绪线程
    fn insert(&mut self, pid: Pid);
    /// 移除一个就绪线程
    fn remove(&mut self, pid: Pid);
    /// 选出下一个要执行的线程,但不移除它
    fn select(&mut self) -> Option<Pid>;
    /// 每隔固定时间调用此函数,更新当前线程的时间片信息
    /// 返回是否该线程时间片用完,需要调度
    fn tick(&mut self, current: Pid) -> bool;   // need reschedule?
    /// 设置线程优先级
    fn set_priority(&mut self, pid: Pid, priority: u8);
}

之后提供了两个调度算法的实现:时间片轮转(RR),Stride。

Processor

位于processor.rs,其中包含以下结构:

  • Status:枚举类型,保存线程状态

  • Event:枚举类型,描述未来的调度事件

  • WaitResult:枚举类型,作为返回值描述线程的等待状态

  • Process:线程控制块

  • Processor:完成实际CPU管理的主体

Processor的完整定义如下:

Processor_<T: Context, S: Scheduler>

使用时需指定T和S的实现类型,然后定义一个全局实例:

process/mod.rs
type Processor = Processor_<Context, StrideScheduler>;

// Once用来支持稍后手动初始化
// 这里的Mutex实际是一个修改后的SpinNoIrqLock
// 即在lock期间同时关闭中断的自旋锁
// 目的是防止中断时再中断,导致死锁
static PROCESSOR: Once<Mutex<Processor>> = Once::new();

// 初始化示意
fn process_init() {
    PROCESSOR.call_once(|| Mutex::new(
        // 构造一个实例
        Processor::new(
            // 当前第一个线程的Context,内容为空
            unsafe { Context::new_init() },
            StrideScheduler::new(5),
        )
    ));
}

创建新线程时,首先自己构造Context,然后add:

// 定义idle线程
extern fn idle(arg: usize) -> ! {
    loop {}
}
// 构造Context并添加到Processor
processor.add(Context::new_kernel(entry: idle, arg: 0));

Processor目前的设计是让【修改线程状态】和【执行线程切换】两种操作尽量分离。

具体而言,【执行线程切换】只能通过schedule函数进行,它会判断当前线程状态是否为Running,若不是,委托调度器选出下一个目标线程,执行切换,等到下次切换回来时退出schedule函数。

而其它大部分函数(除current_wait_for),只会【修改线程状态】,而不会执行线程切换。其中exit/kill/sleep/wakeup/yield_now函数,会调用set_status进行显式的状态修改;而tick函数,会根据当前时刻的调度事件,进行隐式的状态修改。

在原版ucore中,每个CPU核有一个专门的调度线程,进行线程调度时,首先切换到调度线程,然后再切换到下一个线程。而在RustOS中是没有调度线程的,源线程直接调用schedule函数切换到下一个线程,能减少一次上下文切换。这两种实现方式哪种更好,欢迎大家一起讨论。

std::thread接口

位于thread.rs,首先定义了ThreadSupport接口提供底层支持,然后定义了一个空结构ThreadMod<S: ThreadSupport>实现和std::thread相同的上层接口。

由于这部分仅仅是在Processor外面又套了一层,因此所谓的ThreadSupport接口只需能访问到全局Processor即可。对使用者而言,需要进行以下操作:

// 首先我们已经有了一个Processor的全局实例
type Processor = Processor_<Context, StrideScheduler>;
pub static PROCESSOR: Once<Mutex<Processor>> = Once::new();

// 实现ThreadSupport接口
pub struct ThreadSupportImpl;
impl ThreadSupport for ThreadSupportImpl {
    type Context = Context;
    type Scheduler = StrideScheduler;
    type ProcessorGuard = MutexGuard<'static, Processor>;
    // 上面都是铺垫,下面才是重点
    fn processor() -> Self::ProcessorGuard {
        PROCESSOR.try().unwrap().lock()
    }
}

// ‘实例化’ThreadMod,并假装它是一个mod
#[allow(non_camel_case_types)]
pub type thread = ThreadMod<ThreadSupportImpl>;

// 然后就可以把它当作`std::thread`来用了
use thread;
let t = thread::current();

// 然而假的毕竟是假的……
let t: thread::Thread;   // Compile ERROR!

之所以用这样一种奇怪的实现方式,是为了这个需求:我希望实现一些全局静态函数,同时希望它们的底层依赖是可替换的。std::thread本身的实现方式是用条件编译,对不同系统使用不同的底层函数,然而我们这个没法这么搞。另一种可能的实现方式是用一个大宏把整个包起来,使用时再展开,不过这样IDE就无法实时分析这部分代码了……

至于thread本身的实现,除了spawn之外都比较简单,读者可查看源码了解实现细节。

TODO

Last updated