线程的切换
回答一下前一节的思考题:当发生中断时,在 __restore
时,a0
寄存器的值是 handle_interrupt
函数的返回值。也就是说,如果我们令 handle_interrupt
函数返回另一个线程的 *mut Context
,就可以在时钟中断后跳转到这个线程来执行。
修改中断处理
在线程切换时(即时钟中断时),handle_interrupt
函数需要将上一个线程的 Context
保存起来,然后将下一个线程的 Context
恢复并返回。
注 1:为什么不直接 in-place 修改
Context
呢?这是因为handle_interrupt
函数返回的Context
指针除了存储上下文以外,还提供了内核栈的地址。这个会在后面详细阐述。注 2:在 Rust 中,引用
&mut
和指针*mut
只是编译器的理解不同,其本质都是一个存储对象地址的寄存器。这里返回值使用指针而不是引用,是因为其指向的位置十分特殊,其生命周期在这里没有意义。
os/src/interrupt/handler.rs
/// 中断的处理入口
#[no_mangle]
pub fn handle_interrupt(context: &mut Context, scause: Scause, stval: usize) -> *mut Context {
/* ... */
}
/// 处理 ebreak 断点
fn breakpoint(context: &mut Context) -> *mut Context {
println!("Breakpoint at 0x{:x}", context.sepc);
context.sepc += 2;
context
}
/// 处理时钟中断
fn supervisor_timer(context: &mut Context) -> *mut Context {
timer::tick();
PROCESSOR.lock().park_current_thread(context);
PROCESSOR.lock().prepare_next_thread()
}
可以看到,当发生断点中断时,直接返回原来的上下文(修改一下 sepc
);而如果是时钟中断的时候,我们执行了两个函数得到的返回值作为上下文,那它又是怎么工作的呢?
线程切换
让我们看一下 Processor
中的这两个方法是如何实现的。
(调度器 scheduler
会在后面的小节中讲解,我们只需要知道它能够返回下一个等待执行的线程。)
os/src/process/processor.rs: impl Processor
/// 保存当前线程的 `Context`
pub fn park_current_thread(&mut self, context: &Context) {
self.current_thread().park(*context);
}
/// 在一个时钟中断时,替换掉 context
pub fn prepare_next_thread(&mut self) -> *mut Context {
// 向调度器询问下一个线程
if let Some(next_thread) = self.scheduler.get_next() {
// 准备下一个线程
let context = next_thread.prepare();
self.current_thread = Some(next_thread);
context
} else {
// 没有活跃线程
if self.sleeping_threads.is_empty() {
// 也没有休眠线程,则退出
panic!("all threads terminated, shutting down");
} else {
// 有休眠线程,则等待中断
/* ... */
}
}
}
上下文 Context
的保存和取出
在线程切换时,我们需要保存前一个线程的 Context
,为此我们实现 Thread::park
函数。
os/src/process/thread.rs: impl Thread
/// 发生时钟中断后暂停线程,保存状态
pub fn park(&self, context: Context) {
// 检查目前线程内的 context 应当为 None
assert!(self.inner().context.is_none());
// 将 Context 保存到线程中
self.inner().context.replace(context);
}
然后,我们需要取出下一个线程的 Context
,为此我们实现 Thread::prepare
。不过这次需要注意的是,启动一个线程除了需要 Context
,还需要切换页表。这个操作我们也在这个方法中完成。
os/src/process/thread.rs: impl Thread
/// 准备执行一个线程
///
/// 激活对应进程的页表,并返回其 Context
pub fn prepare(&self) -> *mut Context {
// 激活页表
self.process.inner().memory_set.activate();
// 取出 Context
let parked_frame = self.inner().context.take().unwrap();
// 将 Context 放至内核栈顶
unsafe { KERNEL_STACK.push_context(parked_frame) }
}
思考:在 run
函数中,我们在一开始就激活了页表,会不会导致后续流程无法正常执行?
内核栈?
现在,线程保存 Context
都是根据 sp
指针,在栈上压入一个 Context
来存储。但是,对于一个用户线程而言,它在用户态运行时用的是位于用户空间的用户栈。而它在用户态运行中如果触发中断,sp
指针指向的是用户空间的某地址,但此时 RISC-V CPU 会切换到内核态继续执行,就不能再用这个 sp
指针指向的用户空间地址了。这样,我们需要为 sp 指针准备好一个专门用于在内核态执行函数的内核栈。所以,为了不让一个线程的崩溃导致操作系统的崩溃,我们需要提前准备好内核栈,当线程发生中断时可用来存储线程的 Context
。在下一节我们将具体讲解该如何做。
小结
为了实现线程的切换,我们让 handle_interrupt
返回一个 *mut Context
。如果需要切换线程,就将前一个线程的 Context
保存起来换上新的线程的 Context
。而如果不需要切换,那么直接返回原本的 Context
即可。