xv6 traps
traps
引入
三种类型的事件会导致CPU暂时搁置普通指令的执行,并强制将控制转移给处理事件的特殊代码。
- 系统调用。用户程序执行调用指令要求内核为它做一些事情
- 异常。指令(用户或内核)做了一些非法的事情,例如除以零或使用无效的虚拟地址
- 设备中断。当设备发出需要注意的信号时,例如当磁盘硬件完成读或写请求时。
陷阱是这些情况的统称。在陷阱发生时正在执行的代码稍后将需要恢复,并且不需要知道发生了什么特殊情况。
通常的顺序是陷阱强制将控制转移到内核;内核保存寄存器和其他状态,以便可以恢复执行;内核执行适当的处理程序代码(例如,系统调用实现或设备驱动程序);内核恢复保存的状态并从陷阱中返回;
xv6陷阱处理分四个阶段进行:RISC-V CPU采取的硬件操作,为内核C代码做准备的一些汇编指令,决定如何处理陷阱的C函数,以及系统调用或设备驱动程序服务例程。虽然三种陷阱类型之间的通用性表明内核可以用单个代码路径处理所有的陷阱,但事实证明,为三种不同的情况使用单独的代码是很方便的:来自用户空间的陷阱、来自内核空间的陷阱和计时器中断。
xv6 traps机制
每个RISC-V CPU都有一组控制寄存器,来告诉CPU如何处理陷阱。
- stvec: 保存陷阱处理程序的地址
- sepc: 保存程序计数器即pc的地址
- scause: 保存陷阱原因,用一个数字描述
- sstatus: 保存CPU状态,包括中断使能位
- sscratch: 避免在保存寄存器前被重写,保存a0
- sstatus: 特权状态寄存器,包括使能中断位
RISC-V硬件对所有类型的陷阱(计时器中断除外)执行以下操作:
- 如果是设备中断,且SIE被清除,则什么也不做
- 清除SIE位来禁用中断
- 复制pc到sepc
- 将当前模式(用户或内核)记录到sstatus的SPP位
- 设置scase的值来指示陷阱类型
- 设置模式为内核模式
- 将stvec的值复制到pc
- 从pc处开始执行
CPU不会切换到内核页表,不会切换到内核中的堆栈,也不会保存除pc之外的任何寄存器。这些任务交给内核软件来做。
用户traps
xv6对内核陷阱和用户陷阱的处理方式不同。
来自用户空间的陷阱的高级路径是
- uservec (kernel/trampoline.S:21)
- usertrap (kernel/trap.c:37)
返回时
- usertrapret (kernel/trap.c:90)
- userret (kernel/trampoline.S:101)
xv6陷阱处理设计上的一个主要约束是RISC-V硬件在强制执行陷阱时不会切换页表。这意味着stvec中的陷阱处理程序地址必须在用户页表中有一个有效的映射,以便陷阱处理代码开始执行。此外,xv6的陷阱处理代码需要切换到内核页表;为了能够在切换后继续执行,内核页表还必须有一个映射到由stvec指向的处理程序的映射。
当uservec启动时,所有32个寄存器都包含被中断的用户代码所拥有的值。这32个值需要保存在内存的trapframe(proc.trapframe为物理地址),以便当trap返回到用户空间时可以恢复它们。trapframe包含当前进程的内核栈地址、当前CPU的hartid、usertrap函数地址和内核页表地址,加载这些项到寄存器之后启动usertrap函数。
usertrap的工作是确定产生trap的原因,处理它,然后返回(kernel/trap.c:37)。它首先更改stvec,以便内核中的陷阱将由kernelvec而不是uservec处理。它保存sepc寄存器(保存的用户程序计数器),因为usertrap可能调用yield切换到另一个进程的内核线程,而该进程可能返回到用户空间,在这个过程中它将修改sepc。如果陷阱是一个系统调用,usertrap调用sycall来处理它;如果设备中断,devintr;否则,它就是一个异常,内核将终止故障进程。系统调用路径为保存的用户程序计数器增加了4,因为在系统调用的情况下,RISC-V使程序指针指向调用指令,但用户代码需要在后续指令上继续执行。在退出时,usertrap检查进程是否已被终止或应该放弃CPU(如果此trap是计时器中断)。
usertrapret 和 userret 是对应过程的逆过程。
系统调用
initcode.S将exec的参数放在寄存器a0和a1中,并将系统调用号放在a7中。系统调用号匹配syscalls数组中的条目,一个函数指针表(kernel/sycall.c:107)。调用指令进入内核并导致uservec、usertrap和sycall执行。
sycall从trapframe中保存的a7中检索系统调用号,并使用它来索引系统调用。对于第一个系统调用,a7包含SYS_exec,导致对系统调用实现函数SYS_exec的调用。
当sys_exec返回时,syscall记录其返回值p->trapframe->a0。这将导致对exec()的原始用户空间调用返回该值,因为RISC-V上的C调用约定将返回值放在a0中。系统调用通常返回负数表示错误,返回零或正数表示成功。如果系统调用号无效,则输出错误并返回−1。
内核中的系统调用实现需要查找用户代码传递的参数。参数最初位于RISC-V C调用约定放置它们的地方:寄存器中,而内核陷阱代码将用户寄存器保存到当前进程的陷阱帧中,内核代码可以在trapframe中找到它们。内核函数argint、argaddr和argfd从trap帧中以整数、指针或文件描述符的形式检索第n个系统调用参数。它们都调用argraw来检索适当保存的用户寄存器。
一些系统调用传递指针作为参数,内核必须使用这些指针来读写用户内存。例如,exec系统调用向内核传递一个指针数组,该数组指向用户空间中的字符串参数。这些指标提出了两个挑战。首先,用户程序可能是错误的或恶意的,并且可能向内核传递一个无效的指针或一个旨在欺骗内核访问内核内存而不是用户内存的指针。其次,xv6内核页表映射与用户页表映射不同,因此内核不能使用普通指令从用户提供的地址加载或存储。
内核实现了在用户提供的地址之间安全地传输数据的函数,详见vm.c。
内核traps
当内核在CPU上执行时,内核将stvec指向位于kernelvec (kernel/kernelvec. s:12)的汇编代码。由于xv6已经在内核中,因此kernelvec可以依赖于设置到内核页表的satp,以及指向有效内核堆栈的堆栈指针。Kernelvec将所有32个寄存器压入栈,稍后将从中恢复它们,以便中断的内核代码可以不受干扰地恢复。
Kernelvec将寄存器保存在被中断的内核线程的栈上,这是有意义的,因为寄存器值属于该线程。如果陷阱导致切换到另一个线程,陷阱实际上会从新线程的栈返回,将被中断线程保存的寄存器安全地留在栈上。
保存寄存器后,Kernelvec跳转到kerneltrap。Kerneltrap是为两种类型的trap准备的:设备中断和异常。它调用devintr (kernel/trap.c:178)来检查和处理前者。如果陷阱不是设备中断,它一定是一个异常,如果它发生在xv6内核中,那总是一个致命错误;内核调用panic并停止执行。
如果由于计时器中断而调用kerneltrap,并且进程的内核线程正在运行(与调度程序线程相反),则调用kerneltrap以给其他线程一个运行的机会。在某个时刻,其中一个线程会退出,然后让我们的线程和它的内核再次恢复。
x6对异常的响应:如果在用户空间中发生异常,内核将终止故障进程。如果内核中发生异常,内核就会出现恐慌。
Traps Lab
主要讲一下alarm的流程
用户通过sigalarm设置internal和handler函数,使得每个进程在使用cpu达到一定internal周期后,会调用handler函数。
而handler函数调用中,先完成一些任务,如计数后,调用sigreturn函数。
时钟中断时,保存用户寄存器,转入usertrap,若果进程使用cpu时间到达internal,需要进入handler函数。采取的策略是,使用进程新的alarm_trap_frame保存用户寄存器,然后将epc设置为handler函数的地址。这样,中断返回后不会直接进入原用户空间而是进入handler函数。handler函数完成后,调用sigreturn函数,使用记录的alarm_trap_frame替换trapframe,然后返回到原用户空间。
要注意的是,TRAPFRAME就是用户用来保存寄存器的虚拟地址,在trampoline.S中,使用li a0, TRAPFRAME就将p->trapframe的虚拟地址放入a0中,然后恢复寄存器。