4.5: 从内核空间陷入(Traps from kernel space)
Xv6 从内核代码处理陷阱的方式与从用户代码处理陷阱的方式不同。当进入内核时,usertrap
将 stvec
指向汇编代码 kernelvec
(文件位置:kernel/kernelvec.S:12
)。由于 kernelvec
仅在 Xv6 已经处于内核状态时执行,因此 kernelvec
可以依赖于 satp
已设置为内核页表,并且栈指针指向有效的内核栈。
.\xv6-labs-2024\kernel\kernelvec.S
#
# interrupts and exceptions while in supervisor
# mode come here.
#
# the current stack is a kernel stack.
# push registers, call kerneltrap().
# when kerneltrap() returns, restore registers, return.
#
.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
# make room to save registers.
addi sp, sp, -256
# save caller-saved registers.
sd ra, 0(sp)
sd sp, 8(sp)
sd gp, 16(sp)
sd tp, 24(sp)
sd t0, 32(sp)
sd t1, 40(sp)
sd t2, 48(sp)
sd a0, 72(sp)
sd a1, 80(sp)
sd a2, 88(sp)
sd a3, 96(sp)
sd a4, 104(sp)
sd a5, 112(sp)
sd a6, 120(sp)
sd a7, 128(sp)
sd t3, 216(sp)
sd t4, 224(sp)
sd t5, 232(sp)
sd t6, 240(sp)
# call the C trap handler in trap.c
call kerneltrap
# restore registers.
ld ra, 0(sp)
ld sp, 8(sp)
ld gp, 16(sp)
# not tp (contains hartid), in case we moved CPUs
ld t0, 32(sp)
ld t1, 40(sp)
ld t2, 48(sp)
ld a0, 72(sp)
ld a1, 80(sp)
ld a2, 88(sp)
ld a3, 96(sp)
ld a4, 104(sp)
ld a5, 112(sp)
ld a6, 120(sp)
ld a7, 128(sp)
ld t3, 216(sp)
ld t4, 224(sp)
ld t5, 232(sp)
ld t6, 240(sp)
addi sp, sp, 256
# return to whatever we were doing in the kernel.
sret
kernelvec
将所有 32 个寄存器压入栈中,稍后它会从栈中恢复这些寄存器,使被中断的内核代码能够不受干扰地继续运行。kernelvec
将寄存器保存在被中断的内核线程的栈上,这是合理的,因为这些寄存器的值属于该线程。如果陷阱导致切换到另一个线程运行,这一点尤为重要——在这种情况下,陷阱实际上会从新线程的栈返回,而被中断线程的寄存器则安全地保存在它自己的栈上。
在保存寄存器后,kernelvec
跳转到 kerneltrap
(文件位置:kernel/trap.c:135
)。kerneltrap
处理两种类型的陷阱:设备中断和异常(系统调用一般从用户态触发)。它调用 devintr
(文件位置:kernel/trap.c:185
)来检查和处理设备中断。如果陷阱不是设备中断,那么它必然是异常,而在 Xv6 内核中发生异常总是致命错误;内核会调用 panic
并停止执行。
如果 kerneltrap
是由于计时器中断被调用的,并且正在运行的是某个进程的内核线程(而不是调度器线程),kerneltrap
会调用 yield
,让其他线程有机会运行。在某个时刻,这些线程之一会调用 yield
,使我们的线程及其 kerneltrap
能够再次恢复。第七章详细解释了 yield
的工作原理。
当 kerneltrap
的工作完成后,它需要返回到被陷阱中断的代码。由于 yield
可能会扰乱 sepc
和 sstatus
中的先前模式,因此 kerneltrap
在开始时会保存它们。然后,它会恢复这些控制寄存器,并返回到 kernelvec
(文件位置:kernel/kernelvec.S:38
)。kernelvec
从栈中弹出保存的寄存器,并执行 sret
指令,将 sepc
的值复制到 pc
,从而恢复被中断的内核代码。
思考一下如果 kerneltrap
由于计时器中断而调用 yield
的情况下,陷阱返回是如何发生的,这会很有帮助。
当 CPU 从用户空间进入内核时,Xv6 将该 CPU 的 stvec
设置为 kernelvec
;这一过程可以在 usertrap
中看到(文件位置:kernel/trap.c:29
)。在内核开始执行但 stvec
仍然设置为 uservec
的时间窗口内,确保不会发生设备中断是至关重要的。幸运的是,RISC-V 在开始处理陷阱时总会禁用中断,而 usertrap
在设置 stvec
之前不会重新启用中断。