内核线程初始化

回忆一下我们如何进行启动线程的初始化?无非两步:设置栈顶地址、跳转到内核入口地址。从而变为启动线程的初始状态,并准备开始运行。

其他线程的初始化也差不多。事实上我们要构造一个停止的线程状态,使得一旦其他的进程切换到它,就立刻变为我们想要的该线程的初始状态,并可以往下运行。

构造线程状态信息

首先是要新建一个内核栈,然后在栈上压入我们精心构造的线程状态信息。

  1. // src/context.rs
  2. impl ContextContent {
  3. // 为一个新内核线程构造栈上的初始状态信息
  4. // 其入口点地址为 entry ,其内核栈栈顶地址为 kstack_top ,其页表为 satp
  5. fn new_kernel_thread(
  6. entry: usize,
  7. kstack_top: usize,
  8. satp: usize,
  9. ) -> ContextContent {
  10. let mut content = ContextContent {
  11. ra: __trapret as usize,
  12. satp,
  13. s: [0; 12],
  14. tf: {
  15. let mut tf: TrapFrame = unsafe { zeroed() };
  16. tf.x[2] = kstack_top;
  17. tf.sepc = entry;
  18. tf.sstatus = sstatus::read();
  19. tf.sstatus.set_spp(sstatus::SPP::Supervisor);
  20. tf.sstatus.set_spie(true);
  21. tf.sstatus.set_sie(false);
  22. tf
  23. }
  24. };
  25. content
  26. }
  27. }

首先

内核线程初始化 - 图1

switch_to 中被正确设置。这里

内核线程初始化 - 图2

的值为 __trapret ,因此当 switch_to 使用 ret 退出后会跳转到 __trapret 。而它是我们在中断处理返回时用来恢复中断上下文的!实际上这里用 __trapret 仅仅是利用它来设置寄存器的初始值,而不是说它和中断有什么关系。

switch_to 返回之后,原栈顶的

内核线程初始化 - 图3

被回收掉了。因此现在栈顶上恰好保存了一个中断帧。那么我们从中断返回的视角来看待:栈顶地址会被正确设置为 kstack_top ,由于将中断帧的

内核线程初始化 - 图4

设置为线程入口点,因此中断返回后会通过 sret 跳转到线程入口点。

注意中断帧中

内核线程初始化 - 图5

的设置:

  • 内核线程初始化 - 图6

    设置为 Supervisor ,使得使用 sret 返回后 CPU 的特权级为 S Mode 。

  • 设置

    内核线程初始化 - 图7

    ,这里的作用是 sret 返回后,在内核线程中使能异步中断。详情请参考RISC-V 特权指令集文档

我们还希望能够给线程传入参数,这只需要修改中断帧中的

内核线程初始化 - 图8

(即参数

内核线程初始化 - 图9

)即可,__trapret 函数可以协助完成参数传递。

  1. // src/context.rs
  2. impl Context {
  3. pub unsafe fn new_kernel_thread(
  4. entry: usize,
  5. kstack_top: usize,
  6. satp: usize
  7. ) -> Context {
  8. ContextContent::new_kernel_thread(entry, kstack_top, satp).push_at(kstack_top)
  9. }
  10. pub unsafe fn append_initial_arguments(&self, args: [usize; 3]) {
  11. let contextContent = &mut *(self.content_addr as *mut ContextContent);
  12. contextContent.tf.x[10] = args[0];
  13. contextContent.tf.x[11] = args[1];
  14. contextContent.tf.x[12] = args[2];
  15. }
  16. }
  17. impl ContextContent {
  18. // 将自身压到栈上,并返回 Context
  19. unsafe fn push_at(self, stack_top: usize) -> Context {
  20. let ptr = (stack_top as *mut ContextContent).sub(1);
  21. *ptr = self;
  22. Context { content_addr: ptr as usize }
  23. }
  24. }

创建新线程

接下来就是线程的创建:

  1. // src/process/structs.rs
  2. impl Thread {
  3. // 创建一个新线程,放在堆上
  4. pub fn new_kernel(entry: usize) -> Box<Thread> {
  5. unsafe {
  6. let kstack_ = KernelStack::new();
  7. Box::new(Thread {
  8. // 内核线程共享内核资源,因此用目前的 satp 即可
  9. context: Context::new_kernel_thread(entry, kstack_.top(), satp::read().bits()), kstack: kstack_,
  10. })
  11. }
  12. }
  13. // 为线程传入初始参数
  14. pub fn append_initial_arguments(&self, args: [usize; 3]) {
  15. unsafe { self.context.append_initial_arguments(args); }
  16. }
  17. }

下一节我们终于能拨云见日,写一个测试看看我们的线程实现究竟有无问题了!