rust_os_docs
Search
⌃K

线程管理模块

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

概述

线程管理模块的职责是完成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

  • 多核支持