线程的创建
接下来,我们的第一个目标就是创建一个线程并且让他运行起来。一个线程要开始运行,需要这些准备工作:
- 建立页表映射,需要包括以下映射空间:
- 线程所执行的一段指令
- 线程执行栈
- 操作系统的部分内存空间
- 设置起始执行的地址
- 初始化各种寄存器,比如
sp - 可选:设置一些执行参数(例如
argc和argv等 )
思考:为什么线程即便与操作系统无关,也需要在内存中映射操作系统的内存空间呢?
当发生中断时,需要跳转到
stvec所指向的中断处理过程。如果操作系统的内存不在页表之中,将无法处理中断。当然,也不是所有操作系统的代码都需要被映射,但是为了实现简便,我们会为每个进程的页表映射全部操作系统的内存。而由于这些页表都标记为内核权限(即
U位为 0),也不必担心用户线程可以随意访问。
执行第一个线程
因为启动线程需要修改各种寄存器的值,所以我们又要使用汇编了。不过,这一次我们只需要对 interrupt.asm 稍作修改就可以了。
在 interrupt.asm 中的 __restore 标签现在就能派上用途了。原本这段汇编代码的作用是将之前所保存的 Context 恢复到寄存器中,而现在我们让它使用一个精心设计的 Context,就可以让程序在恢复后直接进入我们的新线程。
首先我们稍作修改,添加一行 mv sp, a0。原本这里是读取之前存好的 Context,现在我们让其从 a0 中读取我们设计好的 Context。这样,我们可以直接在 Rust 代码中调用 __restore(context)。
os/src/interrupt/interrupt.asm
__restore:
mv sp, a0 # 加入这一行
# ...
那么我们需要如何设计 Context 呢?
- 通用寄存器
sp:应当指向该线程的栈顶a0-a7:按照函数调用规则,用来传递参数ra:线程执行完应该跳转到哪里呢?在后续系统调用章节我们会介绍正确的处理方式。现在,我们先将其设为一个不可执行的地址,这样线程一结束就会触发页面异常
sepc- 执行
sret指令后会跳转到这里,所以sepc应当存储线程的入口地址(执行的函数地址)
- 执行
sstatusspp位按照用户态或内核态有所不同spie位为 1
[info]
sstatus标志位的具体意义
spp:中断前系统处于内核态(1)还是用户态(0)sie:内核态是否允许中断。对用户态而言,无论sie取何值都开启中断spie:中断前是否开中断(用户态中断时可能sie为 0)硬件处理流程
- 在中断发生时,系统要切换到内核态。此时,切换前的状态会被保存在
spp位中(1 表示切换前处于内核态)。同时,切换前是否开中断会被保存在spie位中,而sie位会被置 0,表示关闭中断。- 在中断结束,执行
sret指令时,会根据spp位的值决定sret执行后是处于内核态还是用户态。与此同时,spie位的值会被写入sie位,而spie位置 1。这样,特权状态和中断状态就全部恢复了。为何如此繁琐?
- 特权状态:
中断处理流程必须切换到内核态,所以中断时需要用spp来保存之前的状态。
回忆计算机组成原理的知识,sret指令必须同时完成跳转并切换状态的工作。- 中断状态:
中断刚发生时,必须关闭中断,以保证现场保存的过程不会被干扰。同理,现场恢复的过程也必须关中断。因此,需要有以上两个硬件自动执行的操作。
由于中断可能嵌套,在保存现场后,根据中断的种类,可能会再开启部分中断的使能。
设计好 Context 之后,我们只需要将它应用到所有的寄存器上(即执行 __restore),就可以切换到第一个线程了。
os/src/main.rs: rust_main()
extern "C" {
fn __restore(context: usize);
}
// 获取第一个线程的 Context,具体原理后面讲解
let context = PROCESSOR.lock().prepare_next_thread();
// 启动第一个线程
unsafe { __restore(context as usize) };
unreachable!()
为什么 unreachable
我们直接调用的 __restore 并没有 ret 指令,甚至 ra 都会被 Context 中的数值直接覆盖。这意味着,一旦我们执行了 __restore(context),程序就无法返回到调用它的位置了。注:直接 jump 是一个非常危险的操作。
但是没有关系,我们也不需要这个函数返回。因为开始执行第一个线程,意味着操作系统的初始化已经完成,再回到 rust_main() 也没有意义了。甚至原本我们使用的栈 bootstack,也可以被回收(不过我们现在就丢掉不管吧)。
在启动时不打开中断
现在,我们会在线程开始运行时开启中断,而在操作系统初始化的过程中是不应该有中断的。所以,我们删去之前设置「开启中断」的代码。
os/interrupt/timer.rs
/// 初始化时钟中断
///
/// 开启时钟中断使能,并且预约第一次时钟中断
pub fn init() {
unsafe {
// 开启 STIE,允许时钟中断
sie::set_stimer();
// (删除)开启 SIE(不是 sie 寄存器),允许内核态被中断打断
// sstatus::set_sie();
}
// 设置下一次时钟中断
set_next_timeout();
}
小结
为了执行一个线程,我们需要初始化所有寄存器的值。为此,我们选择构建一个 Context 然后跳转至 interrupt.asm 中的 __restore 来执行,用这个 Context 来写入所有寄存器。
思考
__restore 现在会将 a0 寄存器视为一个 *mut Context 来读取,因此我们在执行第一个线程时只需调用 __restore(context)。
那么,如果是程序发生了中断,执行到 __restore 的时候,a0 的值又是谁赋予的呢?