4.1: RISC-V陷阱机制(RISC-V trap machinery)
每个RISC-V的CPU都有一组控制寄存器,内核可以写入状态告诉CPU如何处理traps,这里列举一些最重要的寄存器,在kernel/riscv.h文件中定义
stvec(supervisor trap vector base address):内核将其陷阱处理句柄的地址写入此寄存器;RISC-V 跳转到 stvec 中的地址以处理陷阱.
sepc(supervisor exception program counter):当发生陷阱时,RISC-V 将程序计数器(pc)保存到此寄存器(因为 pc 将被 stvec 中的值覆盖). sret(从陷阱返回)指令将 寄存器sepc 的值复制到 pc,内核可以写入 sepc 来控制 sret 的去向。程序计数器是一个寄存器,存储着下一条将要执行的指令的地址。它是控制程序流和执行顺序的关键组件。
scause:RISC-V 在此寄存器中放置一个数字,描述陷阱的原因。
sscratch:陷阱处理程序代码使用 sscratch 寄存器来避免在保存用户寄存器之前其值被新的操作覆盖。在trampoline.S中的汇编代码里有将sscratch暂存a0寄存器代码
sstatus:sstatus 中的 SIE 标记位控制设备中断是否启用。如果内核清除了SIE标记位,RISC-V 将推迟设备中断直到内核设置了SIE标记位。SPP标记位指示陷阱是来自用户模式还是特权模式,并控制 sret 返回到哪个模式。
上述寄存器与在特权模式下处理的陷阱相关,无法在用户模式下读取或写入。在多核CPU上每一个核都有上述寄存器,在任何时候可能有不止一个CPU来处理这个trap
RISC-V硬件处理Trap的流程
1、如果trap是设备中断,且寄存器sstatus中的SIE标记位被清除,则不执行以下操作。
2、禁用中断 (清除 sstatus寄存器中的SIE标记位)
3、保存当前的 PC 到sepc寄存器
4、保存当前模式 (用户或特权) 到sstatus寄存器中的SPP标记位
5、设置 scause 描述陷阱产生的情形原因(什么引起了trap)
6、切换到特权模式(supervisor)
7、将
stvec
寄存器的内容复制到PC8、执行新地址(新的程序计数器)的代码指令
请注意,CPU在发生陷阱时不会切换到内核页表,不会切换到内核中的堆栈,也不会保存除程序计数器(pc)之外的任何寄存器。内核软件必须执行这些任务(如将stvec设置为uservec代码段开始位置)。CPU在陷阱期间执行最少工作的一个原因是为了提供软件灵活性;例如,一些操作系统在某些情况下省略了页表切换,以提高陷阱的性能。
值得考虑的是,是否可以省略上述步骤中的任何步骤,或许是为了寻求更快的陷阱处理。虽然在某些情况下,简化的序列可以正常工作,但许多步骤如果省略会在一般情况下变得危险。例如,假设CPU没有切换程序计数器,那么来自用户空间的陷阱可能会在仍然执行用户指令的情况下切换到特权模式。这些用户指令可能会破坏用户与内核的隔离,例如通过修改satp
寄存器,将其指向一个允许访问所有物理内存的页表。因此,重要的是,CPU必须切换到由内核指定的指令地址,即stvec
。
uservec:
#
# trap.c sets stvec to point here, so
# traps from user space start here,
# in supervisor mode, but with a
# user page table.
#
# save user a0 in sscratch so
# a0 can be used to get at TRAPFRAME.
csrw sscratch, a0
# each process has a separate p->trapframe memory area,
# but it's mapped to the same virtual address
# (TRAPFRAME) in every process's user page table.
li a0, TRAPFRAME
# save the user registers in TRAPFRAME
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
sd t0, 72(a0)
sd t1, 80(a0)
sd t2, 88(a0)
sd s0, 96(a0)
sd s1, 104(a0)
sd a1, 120(a0)
sd a2, 128(a0)
sd a3, 136(a0)
sd a4, 144(a0)
sd a5, 152(a0)
sd a6, 160(a0)
sd a7, 168(a0)
sd s2, 176(a0)
sd s3, 184(a0)
sd s4, 192(a0)
sd s5, 200(a0)
sd s6, 208(a0)
sd s7, 216(a0)
sd s8, 224(a0)
sd s9, 232(a0)
sd s10, 240(a0)
sd s11, 248(a0)
sd t3, 256(a0)
sd t4, 264(a0)
sd t5, 272(a0)
sd t6, 280(a0)
# save the user a0 in p->trapframe->a0
csrr t0, sscratch
sd t0, 112(a0)
# initialize kernel stack pointer, from p->trapframe->kernel_sp
ld sp, 8(a0)
# make tp hold the current hartid, from p->trapframe->kernel_hartid
ld tp, 32(a0)
# load the address of usertrap(), from p->trapframe->kernel_trap
ld t0, 16(a0)
# fetch the kernel page table address, from p->trapframe->kernel_satp.
ld t1, 0(a0)
# wait for any previous memory operations to complete, so that
# they use the user page table.
sfence.vma zero, zero
# install the kernel page table.
csrw satp, t1
# flush now-stale user entries from the TLB.
sfence.vma zero, zero
# jump to usertrap(), which does not return
jr t0