线程的创建
接下来,我们的第一个目标就是创建一个线程并且让他运行起来。一个线程要开始运行,需要这些准备工作:
- 建立页表映射,需要包括以下映射空间:
- 线程所执行的一段指令
- 线程执行栈
- 操作系统的部分内存空间
- 设置起始执行的地址
- 初始化各种寄存器,比如
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
应当存储线程的入口地址(执行的函数地址)
- 执行
sstatus
spp
位按照用户态或内核态有所不同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
的值又是谁赋予的呢?