线程状态与保存

如果将整个运行中的内核看作一个内核进程,那么一个内核线程只负责内核进程中执行的部分。虽然我们之前从未提到过内核线程的概念,但是在我们设置完启动栈,并跳转到 rust_main 之后,我们的第一个内核线程——内核启动线程就已经在运行了!

线程的状态

想想一个线程何以区别于其他线程。由于线程是负责“执行”,因此我们要通过线程当前的执行状态(也称线程上下文,线程状态,Context)来描述线程的当前执行情况(也称执行现场)。也就包括:

  • CPU 各寄存器的状态:

    简单想想,我们会特别关心程序运行到了哪里:即

    线程状态与保存 - 图1

    ;还有栈顶的位置:即

    线程状态与保存 - 图2

    当然,其他所有的寄存器都是一样重要的。

  • 线程的栈里面的内容:

    首先,我们之前提到过,寄存器和栈支持了函数调用与参数传递机制;

    其次,我们在函数中用到的局部变量其实都是分配在栈上的。它们在进入函数时被压到栈上,在从函数返回时被回收。而事实上,这些变量的局部性不只限于这个函数,还包括执行函数代码的线程。

    这是因为,同个进程的多个线程使用的是不同的栈,因此分配在一个线程的栈上的那些变量,都只有这个线程自身会访问。(通常,虽然理论上一个线程可以访问其他线程的栈,但由于并无什么意义,我们不会这样做)

    与之相比,放在程序的数据段中的全局变量(或称静态变量)则是所有线程都能够访问。数据段包括只读数据段

    线程状态与保存 - 图3

    ,可读可写的

    线程状态与保存 - 图4

    。在线程访问这些数据时一定要多加小心,因为你并不清楚是不是有其他线程同时也在访问,这会带来一系列问题。

线程状态的保存

一个线程不会总是占据 CPU 资源,因此在执行过程中,它可能会被切换出去;之后的某个时刻,又从其他线程切换回来,为了线程能够像我们从未将它切换出去过一样继续正常执行,我们要保证切换前后线程的执行状态不变

其他线程不会修改当前线程的栈,因此栈上的内容保持不变;但是 CPU 跑去执行其他代码去了,CPU 各寄存器的状态势必发生变化,所以我们要将 CPU 当前的状态(各寄存器的值)保存在当前线程的栈上,以备日后恢复。但是我们也并不需要保存所有的寄存器,事实上只需保存:

  • 返回地址

    线程状态与保存 - 图5

  • 页表寄存器

    线程状态与保存 - 图6

    (考虑到属于同一进程的线程间共享一个页表,这一步不是必须的)

  • 被调用者保存寄存器

    线程状态与保存 - 图7

这与线程切换的实现方式有关,我们到时再进行说明。

线程的实现

首先是线程在栈上保存的内容:

  1. // src/context.rs
  2. // 回忆属性 #[repr(C)] 是为了让 rust 编译器以 C 语言的方式
  3. // 按照字段的声明顺序分配内存
  4. // 从而可以利用汇编代码正确地访问它们
  5. #[repr(C)]
  6. pub struct ContextContent {
  7. pub ra: usize,
  8. satp: usize,
  9. s: [usize; 12],
  10. tf: TrapFrame,
  11. }

前三个分别对应

线程状态与保存 - 图8

,那最后为什么还有个中断帧呢?实际上,我们通过中断帧,来利用中断机制的一部分来进行线程初始化。我们马上就会看到究竟是怎么回事。

  1. // src/context.rs
  2. #[repr(C)]
  3. pub struct Context {
  4. pub content_addr: usize,
  5. }

对于一个被切换出去的线程,为了能够有朝一日将其恢复回来,由于它的状态已经保存在它自己的栈上,我们唯一关心的就是其栈顶的地址。我们用结构体 Context 来描述被切换出去的线程的状态。

随后开一个新的 process mod ,在里面定义线程结构体 Thread

  1. // src/process/structs.rs
  2. pub struct Thread {
  3. // 线程的状态
  4. pub context: Context,
  5. // 线程的栈
  6. pub kstack: KernelStack,
  7. }

Thread里面用到了内核栈 KernelStack

  1. // src/consts.rs
  2. pub const KERNEL_STACK_SIZE: usize = 0x80000;
  3. // src/process/structs.rs
  4. pub struct KernelStack(usize);
  5. impl KernelStack {
  6. pub fn new() -> Self {
  7. let bottom = unsafe {
  8. alloc(Layout::from_size_align(KERNEL_STACK_SIZE, KERNEL_STACK_SIZE).unwrap()) as usize
  9. };
  10. KernelStack(bottom)
  11. }
  12. }
  13. impl Drop for KernelStack {
  14. fn drop(&mut self) {
  15. ......
  16. dealloc(
  17. self.0 as _,
  18. Layout::from_size_align(KERNEL_STACK_SIZE, KERNEL_STACK_SIZE).unwrap(),
  19. );
  20. ......
  21. }
  22. }

在使用 KernelStack::new 新建一个内核栈时,我们使用第四章所讲的动态内存分配,从堆上分配一块虚拟内存作为内核栈。然而 KernelStack 本身只保存这块内存的起始地址。其原因在于当线程生命周期结束后,作为 Thread 一部分的 KernelStack 实例被回收时,由于我们实现了 Drop Trait ,该实例会调用 drop 函数将创建时分配的那块虚拟内存回收,从而避免内存溢出。当然。如果是空的栈就不必回收了。

因此,我们是出于自动回收内核栈的考虑将 KernelStack 放在 Thread 中。另外,需要注意压栈操作导致栈指针是从高地址向低地址变化;出栈操作则相反

下一节,我们来看如何进行线程切换。