2.1 前文回顾
上一章,笔者带领大家迅速过了一下一个hello world程序的运行流程,然后粗略介绍了操作系统的概念和作用,接着介绍了dummy-xv6项目,以及RISC-V的通用寄存器和它的调用规范,最后介绍了怎么编译dummy-xv6项目。上一章读者们编译得出了一个名为kernel的可执行文件,细心的读者可能会问,这个kernel是否就代表操作系统?如果不是,它和操作系统是什么关系呢?本章将就操作系统进行更深入的探讨。
第一章编译出的kernel(操作系统内核文件)目前也只是一个仅能在裸机上跑的程序,它目前不具备操作系统的任何功能。在进一步搞清楚它是什么、主要负责操作系统的什么工作之前,先让笔者给各位读者介绍一下操作系统的整体结构。
2.2 整体视角
2.2.1 操作系统结构一览
要理解操作系统的结构,首先要从宏观上将应用进程、操作系统和硬件作为一个整体来看,然后从整体中划分它们的层次,图1则展示了这一点。图中,用户进程就是我们平时生活中常用的应用程序的运行时实例,它们是直接和用户打交道的,所以被称为用户进程。当然有些后台进程不直接和用户交互,也属于用户进程的范畴。
图1
往下一层就是操作系统,它负责管理用户进程,以及负责直接操控硬件设备,协调用户进程使用硬件资源等。再往下一层就是硬件层,包括CPU、内存、磁盘、输入设备(键盘和鼠标等)和输出设备(显示器、打印机等)等。通常这些硬件设备会被装到主板上,主板负责在硬件层面联通各个硬件设备。
除了上述信息,图1还划分了用户空间(user space)和内核空间(kernel space)。用户进程运行的空间被称为用户空间,操作系统内核(kernel)所运行的空间被称为内核空间[1]。一般而言,一个用户进程不能直接访问其他用户进程的内存,也不能直接访问硬件资源,要做到这些,都需要通过内核来实现。这么设计是为了确保用户进程之间的隔离性和防御性,避免自己在运行的时候,莫名其妙被其他进程破坏自己的运行环境。
用户进程如果要主动使用硬件资源,就需要通过系统调用(system call)的方式,从用户模式(又称用户态)切换到内核模式(又称内核态)[2]后,通过运行内核中相关的处理逻辑使用。此外,如果用户进程在运行过程中发生了异常(如除0、缺页等),也会暂停用户进程当前在执行的流程,在保存完上下文之后,从用户模式,切换到内核模式,并且由内核来执行处理异常的逻辑。
操作系统内核包含了很多硬件驱动,这些驱动就是直接操作硬件的程序,由于硬件的种类繁多,各个硬件厂商有各自的规格,由内核来整合并提供统一的系统调用的API,能够极大方便用户进程使用硬件资源。
当用户进程在运行时,它们会占用CPU,不过如果此时有硬件事件(比如网卡收到数据包,键盘上的按键被按下,鼠标被移动等),如何让正在运行用户进程指令的CPU去相应这些事件呢?答案是通过中断机制,当硬件事件发生时,硬件设备会向CPU发送一个中断信号(irq请求),每个硬件都会被编上中断id,当中断发生时会将该id一起发送给CPU。CPU收到中断事件之后,会更新pc寄存器为内核中,处理中断事件的程序地址,CPU在执行完当前指令之后,就跳转到中断程序中去执行。完成中断处理之后,CPU的pc寄存器会被更新到中断发生前的下一条指令的地址,从而使其继续执行后续的指令。
CPU会记录自己当前处于哪种模式。当系统处于用户模式时,此时一般处于用户空间之中,用户程序如果执行了内核模式才能执行的机器指令,那么CPU会拒绝执行这些指令。因此要执行这些指令,只能先将CPU的模式从用户模式切换到内核模式,然后再跳转到内核执行对应的指令,此时CPU因为已经切换到内核模式,因此内核中的指令它是不会拒绝执行的。由此可知,正确运行用户模式和内核模式,需要硬件和软件的相互配合才行。也就是说,当CPU处于用户模式时,运行的程序应当是用户进程。当CPU处于内核模式时,运行的指令一定是在内核内。两种模式需要在合适的时机相互切换。
实现用户模式和内核模式的目标是限制用户的权限,避免用户越权访问机器资源或者是取得高权限后,对其他用户进程进行非法操作。这是操作系统防御性的体现[3]。
2.2.2 内核与操作系统的关系
接下来还有一个问题,内核(kernel)是否就等同于操作系统?严格来说,内核属于操作系统的一部分,内核需要整合设备驱动、进程管理、内存管理、系统调用等。而操作系统则是更宽泛的概念,操作系统除了内核,往往也包括了用户服务,比如shell、gui和工具等等[4]。
2.2.3 宏内核和微内核
最后一个读者们比较关系的问题则是宏内核(monolithic kernel)和微内核(microkernel)操作系统有什么异同。其实概念没那么复杂,读者们已知内核包括了设备驱动、进程管理、内存管理和系统调用等等,宏内核就是将这些程序代码一起整合到内核中,它们被作为内核代码的一部分,运行任何一段它们的代码,就是在运行内核程序。这样做的好处是,各个模块都是聚合在一起,调用起来比较方便,同时也无需额外从用户模式切换到内核模式,或者反过来。这种方式运行效率比较高,但是坏处是内核代码量过于庞大,bug不易被发现,可能会被利用。
微内核和宏内核不太一样,微内核中只保留少量的功能,很多服务甚至是文件系统,都可以以用户进程的方式来运行。不过这样做带来了一些新的问题,由于微内核将宏内核中不同的功能按类别切分,并且以用户进程的方式去运行,因此它们之间交互只能通过微内核来转发,这样会增加用户模式和内核模式切换的频率,从而降低了运行效率。当然它的好处也很明显,就是内核逻辑会简单很多,bug也会少很多,并且变得没那么致命,因为更少的代码运行在内核模式了。
图2
2.3 操作系统的硬件支持:CPU的特权架构
2.3.1 RISC-V特权等级简介
上一节从宏观上介绍了操作系统的结构和层次,本节将介绍CPU如何支撑上一节所阐述的体系。
dummy-xv6只能运行在RISC-V架构的CPU上,因此本节也只涉及RISC-V架构。上一节已经介绍了用户模式下,应用进程在执行指令时,如果执行了更高级别的指令,由于CPU此时时处于用户模式,因此它会拒绝执行相关指令。事实上,绝大多数指令都可以在用户模式下,在用户进程里执行的。这些指令一般是类似加减乘除、load和store等的指令。需要高权限才能执行的指令其实很少,在继续深入这些内容之前,先介绍一下RISC-V处理器的特权架构。
RISC-V架构的CPU一共有3种模式,分别是用户模式(user mode)、监管模式(supervisor mode)和机器模式(machine mode)。其中用户模式的权限最低,监管模式权限中等,机器模式权限最高。特权级别高的模式,可以运行特权等级低的模式的指令,但是反过来不行。在dummy-xv6系统中,监管模式就是内核模式,并且计算机系统在加载完内核,并完成初始化之后,就不太用得着机器模式了,因此在操作系统运行期间,完全可以忽略机器模式,只保留用户模式和监管模式。
前文提到过,用户模式能够访问绝大多数的常规指令,由于指令过多,本文就不逐一演示。只能运行在监管模式和机器模式下的指令,被称为特权指令,RISC-V的特权指令主要有sret(supervisor-mode return)、mret(machine-mode return)、sfence.vma(supervisor-mode fence.virtual memory address)和wfi(wait for interrupt)[5]。
尽管dummy-xv6项目中,常用的特权指令不是很多,但这里并不打算一一列举它们的功能,随着内容的推进,本系列会在合适的时机对它们进行详细的介绍。
2.3.2 CSR寄存器简介
本节要介绍的是特权模式的切换。上一章已经完成了kernel可执行文件的编译,它被存在了磁盘当中。当机器启动时,kernel文件会从磁盘中被读取到内存,并且从_entry处开始执行。执行_entry的逻辑时,CPU此时处于机器模式,kernel的初始化逻辑(后续会详细介绍)会完成硬件层面的初始化,然后从机器模式切换到内核模式(又称监管模式),切换到内核模式后,会执行kernel程序中关于内核初始化的逻辑,最后在完成第一个进程创建和初始化之后,再切换到用户模式。机器模式只在最开始的时候出现过,后续只是在用户模式和内核模式之间切换。
在操作系统运作的过程中,会频繁地进行模式切换,那么在RISC-V的架构中,它具体是采用哪些指令来切换这些状态呢?在开始讨论这些内容之前,读者们不妨先了解一下RISC-V架构中的CSR(control status register)寄存器。上一章介绍了RISC-V的32个通用寄存器,现在一起来看看CSR寄存器是什么,以及它们的作用是什么。CSR寄存器是记录RISC-V架构的CPU的内部状态的寄存器。由于RISC-V架构中的CSR寄存器种类繁多,这里不会全部介绍,只会介绍和本章相关的CSR寄存器,其他的,读者感兴趣的可以查阅官方文档。
首先要介绍的是CSR指令,它的编码格式定义如图3所示:
图3
图中,指令方框上面的数字,表示对应的部分占据了哪些比特位,而方框下的数字表示它一共占多少位。比如opcode,它占据指令的第0到第6位,一共占用7个bit。opcode的具体值,读者可以通过查阅《RISC-V Reference》[6]来查看,图四展示了部分opcode文档信息。
图4
图3中的rd和rs分别代表dest register和source register,它们主要是RISC-V中的32个通用寄存器。func3表示具体对CSR寄存器做哪些操作,主要内容是对CSR寄存器进行读写,具体内容可以查看官方文档(riscv-spec-v2.2[7] P22)。剩下的12比特主要用来指定CSR寄存器。由于指令中给CSR寄存器保留了12个比特位,因此至多可以有4096个CSR寄存器,实际上目前并没有这么多,图5和图6展示了常见的内核模式和机器模式使用的CSR寄存器[8]。图中的number值就是CSR指令中,那12比特的csr域保存的值。
图5
图6
与本章相关的CSR寄存器主要有mie、mstatus、mip、mtvec、mcause、mideleg、medeleg、sie、sstatus、sip、stvec、scause等。接下来通过一个ecall调用、一个异常和一个中断的例子来结合起来论述他们。
2.3.3 ecall指令
在了解完CSR寄存器之后,现在来了解一下本章第一个要介绍的改变特权等级的指令–ecall。ecall是一个提升特权等级的命令。ecall指令在RISC-V特权文档中的定义如下:
The ECALL instruction is used to make a request to the supporting execution environment. When executed in U-mode, S-mode, or M-mode, it generates an environment-call-from-U-mode exception, environment-call-from-S-mode exception, or environment-call-from-M-mode exception, respectively, and performs no other operation.
ECALL generates a different exception for each originating privilege mode so that environment call exceptions can be selectively delegated. A typical use case for Unix-like operating systems is to delegate to S-mode the environment-call-from-U-mode exception but not the others.
简单来说,在用户模式下调用ecall指令,则会产生environment-call-from-U-mode异常。如果是在内核模式下调用ecall,那么会产生environment-call-from-S-mode的异常。如果是在机器模式下调用ecall指令,则产生environment-call-from-M-mode异常。这里产生environment-call-from-?-mode产生异常的意思是,除了发生异常处理流程,还会将上述的三个类型写入到cause寄存器中,默认是写入mcause寄存器中,如果将异常和中断处理委托到内核模式,则将这三类原因写入scause寄存器中。稍后笔者会介绍异常、中断和委托的内容。
上文说道的,发生异常和中断时,CPU会从任何模式直接切换到机器模式是有依据的,在RISC-V特权文档中[9]中有这么一段话:
By default, all traps at any privilege level are handled in machine mode, though a machine-mode handler can redirect traps back to the appropriate level with the MRET instruction (Section 3.3.2).
上述引用内容的意思是,在默认情况下,在任何特权等级的模式下发生陷入(陷入是traps,发生异常或中断时,会产生陷入操作)都会进入机器模式,因为CPU可以从机器模式,通过MRET指令重定向到任何合适的模式中。
接下来看一个调用ecall的例子,本节即将展示ecall调用和mret返回的完整范例。这里涉及到了几个关键的CSR寄存器,它们分别是:
- PC寄存器:保存下一个要执行的指令的地址。
- mcause寄存器:当异常或者中断发生时,会将异常或者中断的类型信息写入到mcause寄存器中。当没有将异常和中断委托给内核模式时,当它们发生时是直接进入到机器模式的。图7展示了可以被写入到mcause或scause寄存器的异常或中断原因。mcause寄存器最高位为1时,代表此时是中断事件,保存中断的原因。当mcause最高位为0时,表示此时是异常事件,保存的是异常的原因。图7第一列表示mcause最高位的值,第二列表示中断/异常原因的编码,第三列描述了具体的中断/异常原因[10]。当异常或中断处理委托给内核模式时,CPU将使用scause寄存器取代mstatus寄存器的功能。
图7 - mepc寄存器:这个是异常PC寄存器。异常或中断事件发生时,mepc寄存器保存的是触发异常事件的指令的虚拟地址[11]。切换到机器模式才使用mepc寄存器。相应的,当程序从用户模式进入内核模式时,使用的就是sepc寄存器了。
- mstatus寄存器:状态寄存器。这个寄存器比较复杂,它的结构如图8所示,不同的域有不同的作用。图8展示了mstatus寄存器32和64位的结构,其中有三个域最重要:MIE指代machine interrupt enable,它是一个使能控制位,如果为1则允许在机器模式下的中断响应,如果为0则不允许。MPIE指代machine previous interrupt enable,当切换到机器模式时,用来保存MIE的值,并将MIE置0,当从机器模式恢复到原模式时,会将MPIE的值赋值给MIE。MPP指代machine previous privileged,它用来保存异常或中断发生前,CPU所处的模式,用于异常或中断事件处理完之后恢复之前的模式用。如果将异常和中断响应委托给内核模式处理,那么使用的域为SIE、SPIE和SPP。到这里读者可能会疑问,这种情况CPU操作的难道不是sstatus寄存器么?如果是,那么mstatus中的SIE、SPIE和SPP域又有什么用呢?这个问题,笔者在stackoverflow找到说法了,sstatus寄存器和mstatus寄存器的布局大体相等,sstatus寄存器除了没有机器模式相关的使能位外,其他的使能位和mstatus寄存器完全一致。mstatus寄存器和sstatus寄存器保留了相同的视图,这样方便统一软件层的处理逻辑。每种模式,只需要处理自己模式专属的CSR寄存器,逻辑上更加清晰[12。
图8 - mtvec寄存器:这个寄存器是用来保存处理异常和中断事件函数的虚拟地址。当异常或中断发生时,在Direct模式下的mtvec的值会赋值给PC寄存器。在Vectored模式下的mtvec,发生异常时也是直接将mtvec的值赋值给PC寄存器,但发生中断,则是将mtvec + mcause * 4的值赋值给PC寄存器。CPU执行完当前的指令后,会直接执行mtvec所指向的地址的指令,相当于跳转。当异常或中断事件委托给内核模式时,CPU使用功能和它相同的stvec寄存器。
在完成几个与本章有关联的CSR寄存器的介绍之后,现在来看一下ecall调用的例子,如图9所示,此时有一个程序运行在用户模式之下,当程序运行到ecall指令时,触发environment-call-from-U-mode的异常,并将该值写入mcause寄存器。与此同时,mepc寄存器赋值为ecall指令所在的地址,mstatus.MPIE赋值为mstatus.MIE,记录当前是否允许中断,然后将mstatus.MIE赋值为0,处理异常或中断时,该核心禁止接受新的中断通知,如果此时有新的中断触发,会被缓存到PLIC中,等待当前的中断事件处理完,恢复中断接收之后再继续。接着就是mstatus.MPP赋值为User Mode,保存中断发生前CPU所处的模式。然后将mtvec寄存器的值赋值给PC寄存器,CPU就会跳转到异常/中断处理函数里,去执行对应的指令。
图9
图10展示了异常发生后的结果,接下来要执行“陷入”的逻辑,所谓的陷入逻辑一般是位于异常或中断处理逻辑内的。比方说缺页异常,执行“陷入”逻辑后,就会为用户进程分配真实的物理内存,然后再恢复回之前的模式。CPU会顺着PC寄存器所指的位置,一路执行下去,直至碰到mret指令。mret指令会使CPU从机器模式恢复到之前的用户模式。
图10
mret指令在CPU层面的处理包括:
- 将mepc寄存器保存的虚拟地址赋值给PC寄存器。
- mstatus.MIE赋值为mstatus.MPIE,如果在处理异常或中断事件之前,CPU被允许处理中断(mstatus.MPIE=1),那么此时的mstatus.MIE=mstatus.MPIE就是恢复接受中断。然后CPU恢复到mstatus.MPP保存的模式,在本例中是用户模式,最后得到图11的结果。
图11
通过上面的例子,读者们应该可以看到,调用ecall指令就是触发异常,让CPU进入机器模式,同时执行异常事件逻辑,完成之后,异常事件逻辑里通过调用mret指令来恢复到原来的用户模式。其他异常的情况和直接调用ecall指令类似,只是它们不会显式调用任何指令。中断是异步触发的,但是触发用户模式到机器模式转变的逻辑大体也是不变的。
在dummy-xv6中,在完成最初的初始化之后,会将异常和中断事件处理委托给内核模式来进行,而不是经过机器模式通过修改mstatus.MPP为内核模式来间接转换。委托之后,在用户模式下发生异常或中断,则CPU会直接进入内核模式,完成事件处理后通过sret指令返回用户模式。其他流程都和图9-图11的情况类似。
2.4 异常和中断的概念
上一节反复提到异常和中断,但是异常和中断具体的概念是什么呢?本节将深入这个概念进行探讨。首先,中断是一个宽泛的概念,在CMU 18447课程中,中断分为两类,一类是同步中断(又称为异常),另一类是异步中断(又成为中断)[13]。为了方便描述它们,本系列将采用异常和中断的专有名词。
先来看看异常的情况,什么时候会发生异常操作?发生异常操作时会发生哪些处理?为什么要这样做?当用户程序碰到类似除0、访问内存碰到缺页以及碰到TLB缓存不命中等的情况时,异常就发生了。如图12所示,左边的i1~i3是用户程序的指令(运行在用户模式中),而右边的ih1~ihN是异常处理函数中的指令(运行在内核模式中)。
图12
异常具有如下特征:
- 异常触发的条件和特定的指令绑定(比如ecall指令,比如除0操作,比如访问内存时缺页)。
- 故障指令无法被完成(比如除以0的操作,会导致当前程序流程中断)。
- 必须被立即处理。
一般来说,异常是不可预测的,当异常发生时,当前程序的执行流会立即被打断。在dummy-xv6中,发生异常会直接使得CPU从用户模式切换到内核模式,并且跳转到内核的异常处理流程,然后根据异常处理的具体逻辑,判定是否会返回原来的程序流,继续执行剩下的命令。
异常发生时,用户进程不需要自行进行任何处理,整个异常处理的过程对用户程序是透明的。操作系统对异常处理的支持是必要的,读者们也不希望自己在所有的除法指令里,判断一下被除数是否为零是吧。
接下来讲的是中断,中断一般是硬件发生事件时(比如键盘按键被按下,鼠标滚轮、按键被按下、有新的网络数据包到达网卡等),IO设备向CPU发送中断指令,使其暂停当前在运行的用户进程,并且先响应中断事件后返回用户进程,并继续执行接下来的指令。这个过程是异步的,如果没有这种机制,那么CPU需要主动定时轮询(polling)IO设备,这将极大降低操作系统的运行效率和响应效率。中断具备如下特征:
- 中断由IO设备向CPU异步发起,不和任何指令绑定。
- 处理中断时要有一定的灵活性。
- 不需要立即执行,但是不能无限制拖延。
RISC-V架构中,有两种中断,一种是局部中断(local interrupt),比如软件中断和定时器中断。另一种则是外部中断(external interrupt),或者叫全局中断(global interrupt)。
局部中断(又称为CLINT:Core Level Interrupt)一般是核内实现,每个hart都有自己的局部中断。它有如下特征[14]:
- 直接和hart连接,每个hart都有自己的软件和定时器中断组件。
- 不需要仲裁者来协调中断服务。
- 直接通过xcause这个CSR寄存器就能确定中断源。
- RISC-V标准中,局部中断只有软件中断(software interrupt,由其他hart通知的中断事件)和定时器中断(timer interrupt)。
相对而言,全局中断(又称外部中断)具有如下特征:
- 所有的全局中断信号,先发往PLIC(Platform Level Interrupt Controller)中。
- PLIC负责在多个需要处理中断的hart之间进行仲裁。
- 通过读取内存映射寄存器确定中断源。
由上文可知,PLIC是接受IO设备中断信号的中转站,除了转发,它还负责不同IO设备之间的中断信号协调。其内部结构如图13所示,PLIC有两类重要组成部分,分别是PLIC Gateway和PLIC Core。每个IO设备都有一个中断ID,并且每个设备都与一个独立的PLIC Gateway连接,进而向PLIC Core转发中断信号。PLIC Core和hart是连接的,最后的中断处理是PLIC Core和hart来交互执行。
图13
已知中断处理默认是由机器模式来受理,不过可以在初始化阶段委托到任意模式中。图14展示了,如果将中断委托到用户模式(User-Mode),那么PLIC通知过来的中断事件就将在用户模式中处理,但是一般不会这么做。如果将中断委托到监管模式中(Supervisor-Mode,又称内核模式),那么PLIC通知过来的事件将在监管模式中处理,依次类推。如图14所示,Hart中的U表示用户模式、S表示监管模式(或内核模式),H表示(超级监管模式,不过dummy-xv6中不使用),M表示机器模式。
图14
接下来要看一下,PLIC具体是如何通知hart的,并且hart是如何与PLIC协调响应中断事件的。[15]
- IO设备触发IO事件之后,会产生一个中断信号,通过PLIC的Gateway向PLIC Core发送这个信号,每个IO设备都有自己的中断ID,这些ID会随中断信号一起带给PLIC。
- PLIC接收到中断信号后,会向所有的hart广播中断通知。
- 当至少有一个hart能处理该中断时。
- 收到信号,且能受理的hart向PLIC Core发送一个索取中断消息的请求(interrupt claim),通过它获取中断ID。
- PLIC获得首个请求后,向请求的hart返回对应的中断ID,这个过程叫索取回应(claim response),与此同时其他hart就不可以响应对应的中断了。
- hart执行中断处理逻辑,完成后向PLIC的Gateway发送中断完成信号(interrupt completion),此时新的中断又可以通过PLIC Core通知所有的hart了。
- 当所有的hart都在处理中断,暂时禁止处理新的中断事件时。
- 此时,所有的中断信号处于待处理状态,等待hart发送interrupt completion的信号。[16]
- hart发送interrupt completion信号后,PLIC可以重新向所有hart广播。
- 当至少有一个hart能处理该中断时。
图15展示了上述流程。
图15
以上阐述的流程,默认RISC-V架构的CPU允许处理中断。实际上,CPU的hart接不接收中断信号,是可以通过几个CSR寄存器配置的,在默认情况下(即中断事件没有委托给内核模式或者用户模式时),就是通过mie、mip和mstatus来决定。首先来看一下mie的结构(在32位和64位下,它有不同的长度,所以最高bit位用MXLEN-1来表示,MXLEN的值位32或64),如图16所示:
图16
这个寄存器是通过使能位来设置是否接受相对应的中断的。一共分为3个部分,软件中断(Local Software)、定时器中断(Local Timer)和外部中断(External from PLIC)。在软件中断中,它一共有3个使能位:
- USIE:User-Mode Software Interrupt Enable。用户模式软件中断使能位。
- SSIE:Supervisor-Mode Software Interrupt Enable。监管模式软件中断使能位。
- MSIE:Machine-Mode Software Interrupt Enable。机器模式软件中断使能位。
RISC-V架构的软件中断指的是,从其他hart发送过来的中断事件。接下来看一下定时中断相关的使能位:
- UTIE:User-Mode Timer Interrupt Enable。用户模式定时中断使能位。
- STIE:Supervisor-Mode Timer Interrupt Enalbe。监管模式定时中断使能位。
- MTIE:Machine-Mode Timer Interrupt Enable。机器模式定时中断使能位。
最后来看一下外部中断的使能位:
- UEIE:User-Mode External Interrupt Enable。用户模式外部中断使能位。
- SEIE:Supervisor-Mode External Interrupt Enable。监管模式外部中断使能位。
- MEIE:Machine-Mode External Interrupt Enable。机器模式外部中断使能位。
这些使能位的含义非常简单,当这些比特为设置为1时,表示中断被允许,否则不允许。已知当运行在RISC-V架构CPU上的程序,未将中断事件委托给任何特权等级更低的模式时,所有的异常和中断均是在机器模式下进行处理。mie寄存器设置了,允许CPU接收哪些中断事件,mie之所以有监管模式和用户模式的使能位,是因为这两种模式的中断可以先从机器模式来响应,然后由机器模式转向其他模式。
此外,RISC-V架构的CPU还提供了一个全局中断的使能位,回顾一下图8,这个使能位就是mstatus寄存器的MIE域。当外部中断、定时器中断或者软件中断希望被响应时,除了mie寄存器相对应的位要设置为1以外,mstatus.MIE也要被设置为1,中断信号才能被送到CPU中,当中断信号被送达CPU时,mip寄存器相对应的位也会被设置为1[17]。mip寄存器的结构如图17所示:
图17
观察图16和图17,读者们应该可以发现,mie和mip的布局是一一对应的。现在来看一个例子,如果一个外部中断触发时,mie.MEIE为1,mstatus.MIE为1,那么此时意味着外部中断可以被CPU响应。此时mip.MEIP会被设置为1,代表此时有一个外部中断要处理。如果CPU的中断响应被委托到监管模式,那么决定是否响应中断事件的寄存器,和标志是否有中断事件到达的寄存器则是sie和sip,它们的布局如图18所示。
图18
大家可以看到,sie、sip和mie、mip寄存器的布局基本类似,sie和sip除了没有机器模式相关的域以外,其他两种模式都有。与sstatus寄存器一样,sie和sip寄存器,是当CPU处于监管模式的时候,通过与运算来判定是否接收中断请求,如果允许(如外部中断sie.SEIE & sstatus.SIE == 1),则在sip相关的位上置1(sip.SEIP = 1)。
前文反复提到了异常和中断委托,而机器启动时,CPU默认处于机器模式中,并且异常或中断处理默认是在机器模式下进行。那么如果dummy-xv6系统希望在监管模式中响应异常或中断事件,而不希望通过机器模式再向其他模式转换的时候,就需要通过设置两个寄存器来实现,其中一个是medeleg,这个寄存器通过设置指定比特位来委托异常处理。图19展示了medeleg的结构,图中第0到第15位比特位任意一位被设置为1时,代表其对应的异常委托到监管模式处理。图19的比特位索引和图7中的异常值一一对应,这些异常值就是mcause寄存器中存储的值。
图19
另一个设置中断委托的寄存器是mideleg寄存器。该寄存器和mip寄存器的布局一一对应,图20展示了mideleg寄存器的布局。当mideleg寄存器中的某个有效比特位被设置为1时,该类型中断就被委托到监管模式响应了,比如mideleg.STIP设置为1,那么监管模式下的时间中断处理就委托给了监管模式。
图20
读到这里,读者们可能会疑惑,为什么mie、mip寄存器会包含用户模式和监管模式相关的中断使能位呢?监管模式下不是有sie和sip来做同样的事情吗?这个问题问的非常好,以下是笔者的一些理解,供大家参考:
- mie、mip寄存器之所以包含监管模式和用户模式的使能位,是因为默认情况下,所有的中断都是在机器模式进行处理的,然后可以通过mret指令重定向到低特权等级的模式中。如果在重定向之前,中断或异常使能位就被设置为0,那么也没有重定向的必要了。
- 不同的特权等级x下,需要使用其对应的xie、xip、xstatus等寄存器来决定中断是否能被响应。比如SSIE、STIE和SEIE使能位,在mie和sie中都有,但是监管模式下的CPU不能操纵更高特权等级的mie、mip和mstatus寄存器,监管模式下操纵sie、sip和sstatus可以直接决定自己是否可以进行异常或中断响应,而不用提升特权等级。
- 统一的寄存器视图,让软件层能够用更加统一的代码来实现功能逻辑,更易于维护。
2.5 系统调用与ecall指令
经过本章,读者们应该都已经了解到在用户模式中的CPU是无法执行特权指令的,而系统调用则需要转入到内核执行对应的功能。那么在用户模式中调用的系统接口是怎样的呢?现在以fork函数作为举例,fork函数是unix系统下创建子进程的系统调用,在dummy-xv6中,它的用户模式接口如下所示:
.global fork
fork:
li a7, SYS_fork
ecall
retSYS_fork的值是一个整型数值,用户模式fork函数首先将SYS_fork的值读到寄存器a7中,然后调用ecall触发environment-call-from-U-mode异常。此时CPU进入监管模式(或内核模式),内核会将a7寄存器内的值读出,并且到一个函数映射表中,查找对应的系统函数并执行,如下所示:
// 内核中执行如下代码
1 void
2 syscall(void)
3 {
4 int num;
5 struct proc *p = myproc();
6
7 num = p->trapframe->a7;
8 if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
9 // Use num to lookup the system call function for num, call it,
10 // and store its return value in p->trapframe->a0
11 p->trapframe->a0 = syscalls[num]();
12 } else {
13 printf("%d %s: unknown sys call %d\n",
14 p->pid, p->name, num);
15 p->trapframe->a0 = -1;
16 }
17 }完成系统调用后,CPU会从监管模式返回到用户模式,并继续执行用户模式fork函数中ecall后面的逻辑。
2.6 外部中断流程示例
本章介绍了大量异常和中断相关的寄存器,并且描述了大量的细节,为了尽可能降低读者对本章内容的困惑程度,这里增加一节来阐述两个外部中断发生的例子。
第一个例子是,RISC-V CPU没有将中断事件委托给任何特权等级更低的模式,所有的中断响应都将在机器模式下进行。此时PLIC接收到外部IO设备的中断信号,由于响应中断的是机器模式,如图21所示,中断流程随箭头不断完成逻辑流程:
- 首先CPU要判断mie寄存器的MEIE域是否被设置(即mie.MEIE==1),当它被设置时代表允许机器模式下响应中断,流程继续。否则流程中断。
- CPU判断mstatus的MIE域是否被设置(mstatus.MIE==1),这是一个控制是否在机器模式中响应全局中断的比特位,如果是,流程继续,否则流程中断。
- 走到这一步,说明中断允许被响应,此时令mip.MEIP=1。
- 接下来的操作是一步到位的,此时要进行进入中断处理函数前的准备工作:
- mepc=PC+4。
- PC=mtvec。
- mstatus.MPP=U,保存触发中断时,进入机器模式前的程序在运行的模式,在本例中是用户模式U。
- mstatus.MPIE=mstatus.MIE,保存当前的中断使能位,为后面恢复用。
- mstatus.MIE=0,马上要处理中断事件了,暂时禁止新的中断信号被派送过来。
- 执行中断处理函数。
- 退出中断处理函数前,要执行退出前的准备工作:
- PC=mepc,继续中断触发前的运行程序,接着中断的地方继续执行。
- mstatus.MPP=M,前一个模式时机器模式,CPU恢复到用户模式。
- mstatus.MIE=mstatus.MPIE,CPU恢复可接受中断信号的状态。
- mip.MEIP=0,中断处理完成,清楚标记位,mip寄存器一般是由PLIC操作。
图21
现在综合展示了机器模式下的RISC-V架构CPU处理流程,但是中间还有很多保存上下文的工作,需要操作系统来做,这些内容将在后续章节呈现。接下来看另一个例子,这个例子中,中断处理委托给了监管模式(内核模式),因此处理流程如图22所示。读者们可以观察一下图22的流程,大体和前一个例子类似,只是所有参与流程的寄存器变成了监管模式才能使用的寄存器:sxx。这个流程读者可以自行推断一下,这里不作赘述。
图22
2.7 结束语
本章花费了大量的篇幅来讨论RISC-V架构的CPU的特权架构,以及异常和中断机制的一些细节。理解本章的难度是比较高的,不过希望读者能够仔细,甚至多读几遍,总会厘清其内部的逻辑,本章是实现后续dummy-xv6内核的基础,所以彻底理解他是很有必要的。
Reference
[1] 1.2 操作系统结构
[2] 一文搞懂操作系统的用户态与内核态
[3] 3.3 操作系统防御性(Defensive)
[4] What is the difference between the operating system and the kernel?
[5] 《RISC-V 手册》(翻译:勾凌睿、黄成、刘志刚)第十章 RV32/64特权架构 10.1 导言
[6] RISC-V Reference
[7] riscv-spec-v2.2
[8] 2 Control and Status Registers (CSRs)
[9] 《riscv-privileged-20211203》 P30 3.1.8 Machine Trap Delegation Registers (medeleg and mideleg)
[10] 《riscv-privileged-20211203》 P38 3.1.15 Machine Cause Register (mcause)
[11] 《riscv-privileged-20211203》 P38 3.1.14 Machine Exception Program Counter (mepc)
[12] RISC-V: Why does the mstatus include SIE, SPIE and SPP?
[13] 18-447 Lecture 11:Interrupt and Exception
[14] RISC-V Fast Interrupts
[15] 《riscv-privileged-v1.10》 Chapter 7 Platform-Level Interrupt Controller (PLIC)
[16] 6.S081: Interrupts. What if several interrupts arrive?
[17] 《riscv-privileged-v1.10》 3.1.14 Machine Interrupt Registers (mip and mie)