基本概念
Trap通俗一点来讲,就是由用户程序触发的操作系统用户态与内核态的切换.
总体来说,导致用户态与内核态发生切换主要会有一下三种情况:
- 系统调用(system call)
- Excption, Page fault(缺页),运算时除以0的错误等
- Interrupt, 一个设备触发了中断使得当前程序运行需要响应内核设备驱动
而在这几种情况中,很明显的,1和2都属于trap的范畴,而3则是一个中断的典型例子。
切换的关键思路
trap涉及了许多小心的设计和重要的细节,这些细节对于实现安全隔离和性能来说非常重要。因为很多应用程序,要么因为系统调用,要么因为page fault,都会频繁的切换到内核中。所以,trap机制要尽可能的简单,这一点非常重要。
当需要转入内核态时,我们最关心的自然是系统当前的状态。我们必须将操作系统以及硬件正确的切换到内核态,也就是supervisor mode来执行相应的操作,不论是对应的系统调用也好,相应的trap handler也好。在这个过程中,硬件的状态将会非常重要,因为我们很多的工作都是将硬件从适合运行用户应用程序的状态,改变到适合运行内核代码的状态。
在整个系统的状态中,寄存器的状态最为关键。
涉及到的关键寄存器
- RISC-V中一共有32个用户态寄存器,我们必须保证他们在转入内核态时在正确的状态;
- CPU也会有一些控制其工作方式的寄存器,比如说之前介绍过的指向当前进程页表的SATP,以及对于处理trap非常重要的STVEC(指向的是处理trap的指令的地址,也就是之前说的trap handler的地址)。
- SEPC与SSRATCH寄存器,其中SEPC指向在处理trap时pc的值,有助于我们之后从trap中恢复回之前的状态。
- PC;
- 表明当前mode的标志位,这个标志位表明了当前是supervisor mode还是user mode。当我们在运行Shell的时候,自然是在user mode。
TRAP的处理流程
可以肯定的是,在trap的最开始,CPU的所有状态都设置成运行用户代码而不是内核代码。
在trap处理的过程中,我们实际上需要更改一些这里的状态,或者对状态做一些操作。
操作流程如下:
- 先保护现场,包括:
- 保存32个用户寄存器。我们想让用户觉得陷入内核态再返回这个流程就像没发生过一样,所以我们需要找一个地方保存32个用户寄存器。
- 保存pc。如上文所说,我们需要在回到用户态之后之后让程序继续在上一次停止的地方执行。
- 再进入内核态
- 修改mode,将其从user mode修改为supervisor mode,因为我们想要使用内核中的各种各样的特权指令,此时一些只有在supervisor mode下才可以执行的操作就可以运行了。
- 将SATP指向内核对应的页表。因为在此前,SATP一直指向的是用户的页表,所以操作系统并不知道如何去映射内核的地址,既然已经进入内核态,那么我们理所当然的应该加载内核对应的页表。
- 我们需要将堆栈寄存器指向位于内核的一个地址,因为我们需要一个堆栈来调用内核的C函数。
- 一旦我们设置好了,并且所有的硬件状态都适合在内核中使用, 我们需要跳入内核的C代码。
Supervisor mode可以做什么不一样的?
相比于user mode,supervisor mode可以多做的事情其实很有限,总结起来一共就两点:
- 一是可以读写控制寄存器了,包括前文提到的SATP,STVEC,SEPC等;
- 还有就是可以使用允许supervisor mode访问的页表了。之前的文章提到,每个页表都会有自己的权限也就是多个标志位,只有PTE_U这个标志位为0时,supervisor mode才可以使用,反之则是user mode使用。