内核中断机制
中断代理
1 | // delegate all interrupts and exceptions to supervisor mode. |
首先,我们在bootloader阶段进行中断代理,此时,我们仍处在M特权级,而我们的内核需要运行在S特权级。默认情况下,所有的陷阱中断都会在M态进行处理,因此我们需要将其代理到S态以便我们进行处理。
这里首先简要介绍一下medeleg
(machine exception delegation register),mideleg
(machine interrupt delegation register)以及sie
(supervisor interrupt register)寄存器的作用:
medeleg
和mideleg
表明表明当前的中断或者异常应当处理在更低的特权级。在系统中我们拥有3个特权级(M/S/U),通过设置medeleg
和mideleg
可以将发生在系统的中断代理到S态进行处理。如果支持U态的话,我们也可以通过设置sedeleg
和sideleg
寄存器来将中断代理到U态进行处理。
而sie
则是中断使能寄存器的意思,0-15位被分配为标准中断原因,16位以上则为特定的平台或者客户端所设置。
在这里我们需要设置sie
寄存器为可写的并且设置其SEIE
、STIE
以及SSIE
位。
其中,SEIE
设置表明开启S态外部中断;STIE
表示开启S态时钟中断;SSIE
设置表明开启S态软件中断。
开启中断
首先我们在trapinit
函数里面将kernelvec
作为地址写入stvec
寄存器中,stvec
寄存器为Supervisor Trap Vector Base Address Register
,包括向量基地址以及向量模式。如此一来,当我们的操作系统内核检测到发生中断后,就去stvec
去查看处理陷阱函数的地址,随后进入其中进行陷阱处理。
其中,我们的陷阱处理函数由一段汇编表示:
1 | .section .text |
可以看到,我们将必要的寄存器值保存进栈里,然后调用kerneltrap
这个函数进行内核陷阱的处理,当我们执行完kerneltrap
函数返回后,我们将栈内容恢复,并将保存的上下文内容恢复,随后执行sret
指令返回发生陷阱的指令后继续进行运行。
开启PLIC
PLIC
为the riscv Platform Level Interrupt Controller
,其作用如下图所示:
可以看到PLIC
接收到中断后将其转发给不同的Hart
来使其处理。
PLIC
结构如上图所示, 当PLIC
接收到中断信号后,在内部进行逻辑的信号处理,最后输出信号给对应的CPU进行处理。
而在QEMU
平台下,我们不需要去接入具体的外部设备,只需要向QEMU
提供的测试地址内写入一些值,即可开启外部中断的检测。
例如在xv6-rust
中的plicinit
函数:
1 | pub unsafe fn plicinit(){ |
我们只需要将对应的外设的值按规定写入对应的物理地址中,即可开启外设的外部中断机制。
内核中断处理
当我们进入内核中断处理函数(即kerneltrap
)后,我们需要通过scause
寄存器的值来获取发生中断或异常的原因,从而根据不同的原因进行处理。而与此同时,我们也需要获取去读取sstatus
和sepc
寄存器来进行中断间的上下文切换。
其中,sepc
寄存器记录了中断或者异常发生的虚拟地址,当我们的中断结束之后,RISC-V会将sepc
寄存器的值写入程序计数器以便继续执行,因此我们需要将sepc
的值进行修改并重新写入sepc
以保存上下文。
同样,sstatus
寄存器保存的是当前发生中断或者异常所在特权级,当中断或异常发生在U态的时候,sstatus
的SPP
位被置为0,然后sret
指令后将返回U态;当sstatus
的SPP
位被置为1时,sret
指令将返回S态,这时SPP
位仍然被置为0。
1 | // interrupts and exceptions from kernel code go here via kernelvec, |
其中,devintr
函数则检测其是否为外部中断,时钟中断以及其它中断或异常。