基于 riscv32 的 OS 设计:切换上下文
Context
OS 中实际上并没有什么线程、进程,不过是一个个不同的上下文而已。
上下文非常重要,任务 A 和任务 B 的切换,核心就是保存 A 的上下文、恢复 B 的上下文。
在之前的 MIPS yieldOS 中,已经实现过上下文以及上下文的切换了,本质都是差不多的。
1 |
|
保存上下文
上下文本质上是当前hart的环境,也就是寄存器的值。但是不是所有的寄存器都是上下文的组成部分。
在 riscv 中,某些寄存器在上下文切换过程中值保持不变,因此就没有必要保存这些寄存器。这样的寄存器有 X0、gp、tp。
X0 是 0 寄存器,它是不变的。gp 是全局寄存器,通常指向全局数据区域,在大多数情况下,它在整个程序执行期间保持不变。至于 tp,它保存着 hartid,这是一个全局值,只要不涉及处理器核心的切换,在上下文切换期间通常不会改变。因此,没有必要在每次上下文切换时保存和恢复 tp。
另外,我们必须了解的是关于保存和恢复和回复上下文的事情:
- 我们用 mscratch 来指向当前 task 的上下文;
- 接着用 t6 寄存器 保存 mscratch 后,让它作为交换的 base;
- mscratch 是 csr 寄存器,它不能被当做 base,因为 load、store 指令没办法操作它。
因此,首先我们得把 mscratch 的值交换到 t6 中,接着以 t6 为 base,保存寄存器 reg_save t6
:
1 |
|
保存好了以后,就可以在切换时使用这些寄存器了,因为还没有保存 t6,而且 t6 原来的值还在 mscratch 中。
因此,需要将 t6 保存到 t5 中;然后将 mscratch 保存到 t6 中。 这样就恢复了 t6,接着将 t6 保存到上下文中。而 t5 就是刚下的 base。
这样就完美得保存了所有的上下文:
1 |
|
恢复上下文
在 riscv 中,a0 保存的是函数的返回值或者返回的参数。因此,当调度器调度的时候:
1 |
|
因此新的上下文的地址就保存在 a0 中。
因此,需要继续进一步将 a0 的值加载到 mscratch 中。然后将 a0 保存到 t6 中,并以 t6 为 base,恢复上下文:
1 |
|
恢复的时候,会将所有的寄存器中的值更新,包括 t6:
1 |
|
接着调用 ret 指令返回到新的 PC。
整个过程的代码如下:
1 |
|
上面的 ret 指令其实是 jalr x0, x1, 0
,也就是说,它会返回到 x1 也就是 ra。如果设计过 riscv CPU,就会知道其实它的硬件逻辑大概是:
1 |
|
而且这里也没有很复杂的 csr 寄存器访问,也就是一个 mscratch。
新的上下文初始化以及切换
首先需要初始化一个task,这个 task 有必要的栈区、以及返回地址(ret 指令必须从这返回)。
还得有一个上下文,这是一个全局变量,保存在 OS 的栈区:
1 |
|
接着就可以调用 schedule 了:
1 |
|
至于 kernel:
1 |
|
就可以运行了。可以预见的是,它会不断打印 Task 0: Running...
:
1 |
|
这就是一个在启动后切到特定 task 的最简单的 OS 了。实现了切换上下文,那么跑多任务就水到渠成了。