traps源码阅读
这部分的文件主要是trampoline.S、kernelvec.S和trap.c。
trampoline.S
uservec
这是一个汇编代码文件,在xv6中负责处理用户态和内核态的切换。
.section trampsec // 声明代码位于trampsec段
.globl trampoline // 声明trampoline代码起始地址
.globl usertrap // 声明trap处理函数
trampoline: // trampoline代码起始标签
.align 4 // 对齐首先是一些声明。
.globl uservec // 声明用户态trap入口然后是uservec代码,它在用户态到内核态切换时保存寄存器状态并设置内核环境。trap.c会设置STVEC寄存器指向这里,因此用户空间中的陷阱触发后从这里开始,使用用户页表,但是在supervisor模式下。
csrw sscratch, a0 // 将用户a0寄存器值暂存到sscratch寄存器
li a0, TRAPFRAME // 加载TRAPFRAME虚拟地址到a0
// 下面这些里面只少了112,因为它是专门用来存原始a0值
sd ..., ...(a0) // 保存用户寄存器到trapframe
csrr t0, sscratch // 恢复用户原始a0值
sd t0, 112(a0) // 存入p->trapframe->a0
ld sp, 8(a0) // 加载内核栈指针,从p->trapframe->kernel_sp
ld tp, 32(a0) // 设置当前hartid,从p->trapframe->kernel_hartid
ld t0, 16(a0) // 加载usertrap函数地址,从trapframe->kernel_trap
ld t1, 0(a0) // 获取内核页表地址,从trapframe->kernel_satpa0前面的数字是trapframe结构体中各字段的内存偏移量,trapframe的定义在proc.h中。
然后sfence.vma zero, zero是内存屏障指令,会保证之前的内存操作完成。
这时sfence.vma zero zero会刷新TLB,清除旧页表缓存。
然后jr t0跳转到usertrap函数。
userret
由trap.c中的usertrapret调用,负责从内核态回到用户态。
先切换回用户页表。
这里的
a0是内核a0而非前面的用户a0,用户a0在先前被修改,但这里的内核a0是内核准备的参数值。
然后加载TRAPFRAME虚拟地址,保存寄存器等。
最后会把重新存回用户a0寄存器的值,并返回用户空间。
kernelvec.S
kernelvec是内核陷阱的入口,处理来自内核代码中的中断或异常。
它先分配256字节栈空间来保存寄存器的值。然后通过call kerneltrap调用C处理函数,处理完成后再恢复寄存器状态。
trap.c
trapinithart
trapinithart是内核陷阱处理的核心初始化函数。
它通过w_stvec设置陷阱向量基址寄存器,将内核陷阱处理程序入口地址设置为kernelvec。
kerneltrap
这是处理内核空间陷阱的核心函数。
首先保存关键寄存器的值,然后验证是否来自supervisor mode并检查中断是否已经禁用,再尝试用devintr处理中断。若是定时器中断且进程存在,则调用yield触发进程调度。最后恢复寄存器的值。
usertrap
这是处理用户空间陷阱的核心函数。
首先检查确认来自用户模式,然后设置入口地址,后续若触发中断将交由内核陷阱处理程序来处理。
然后保存用户程序计数器:
再根据scause寄存器的值来确定不同的处理方法:
当定时器中断则触发yield让出CPU:
最后调用usertrapret返回用户空间。
usertrapret
这个函数用于返回用户空间。
首先intr_off关闭中断,然后重新设置用户陷阱处理入口:
并重新配置trapframe和sstatus寄存器:
返回地址使用之前保存的:
然后切换页表,重新跳转到用户空间。
其他
devintr和clockintr涉及中断,后面再补充。