一、trap简介
三类 trap
- 系统调用:用户程序执行
ecall
指令要求内核为其提供服务 - 异常:(用户或内核)指令做了一些非法的事情,例如除以零或使用无效的虚拟地址;
- 设备中断,一个设备,例如当磁盘硬件完成读或写请求时,向系统表明需要处理。
trap执行流程
1. 强制将控制权转移到内核
2. 内核保存寄存器和其他状态,以便之后恢复执行。
3. 内核根据trap类型进行对应的处理(例如,系统调用接口或设备驱动程序)
4. 内核恢复状态,并从trap返回到用户态
5. 机器从之前停止的地方恢复执行
xv6 trap处理流程
RISC-V CPU进行硬件操作
为内核C代码执行而准备的汇编程序集“向量”(vector)
决定如何处理trap的C trap处理程序
系统调用或设备驱动程序服务例程
- 三种trap类型,用同一种处理路径进行处理
- 但对于三种不同的情况:来自用户空间的trap、来自内核空间的trap和定时器中断,则分别使用不同的程序进行处理
- 处理trap的内核代码(由汇编或C语言写成)被称为
handler
- 第一个
handler
程序由汇编语言编写,被称为向量(vector
)
二、RISC-V trap机制
-
每个CPU核都有一组控制寄存器,
- 内核通过向这些寄存器写入内容告诉CPU如何处理trap
- 内核也通过读取这些寄存器来明确发生的trap类型
-
一些重要的寄存器:
这些寄存器只能在管理模式下使用,用户模式下不能读写。
机器模式下也有一组类似的控制寄存器,xv6只在计时器中断使用。
-
stvec
:存储trap处理程序的地址,中断时,CPU会跳转到stvec
中的地址。 -
sepc
:发生trap时,RISC-V会在这里保存当前的pc
值,用于之后恢复。从trap返回时,使用
sret
指令,其会将sepc
复制到pc
。所以内核可通过sepc
来控制sret
的去向。 -
scause
: trap时,RISC-V会在这里放置一个数字,说明trap的原因 -
sscratch
: trap handler代码使用sscratch
来避免在保存寄存器之前覆盖它们。类似于数字交换时的中转站
比如trap处理的代码
csrw sscratch, a0li a0, TRAPFRAMEsd ra, 40(a0) sd gp, 56(a0)
需要使用a0存储TRAPFRAME作为基地址,保存寄存器的值
但是a0本身也需要保存,所以先将a0保存到sscratch,然后使用a0,最后将sscratch存储到TRAPFRAME中,这样a0就没有丢失。
-
sstatus
:SIE位
:设备中断是否启用。如果清空SIE,设备中断将停止,直到重新设置。SPP位
:trap是来自用户还是管理模式,并控制sret
返回的模式。
-
RISC-V硬件处理trap的流程
- 如果是设备中断,且SIE位被清空,则不执行任何操作
- 清除SIE以禁用中断
- 将
pc
复制到sepc
- 将当前模式(用户或管理)保存在状态的SPP位中
- 设置
scause
以反映产生trap
的原因 - 将模式设置为管理模式
- 将
stvec
复制到pc
- 在新的
pc
上开始执行
三、第一种trap:来自用户空间
trampoline page
触发trap后,不会自动切换页表,意味着中断开始处理时,仍然使用用户页表。因此用户页表中必须有中断处理程序的映射
此外,在切换到内核页表后,还要继续处理中断,所以内核页表也要有中断处理程序的映射
Xv6使用trampoline page
来解决这些问题。trampoline page
映射在每个页表的trampoline
地址上,同时没有PTE_U
标志,所以trap的管理员模式下可以访问,用户模式不能访问
内核页表:
用户页表:
处理流程
-
xv6启动时,在trap.c/usertrapret()函数中,有一行w_stvec(trampoline_uservec);,会将uservec的地址写入stvec寄存器,即设置中断处理程序为uservec,uservec位于trampoline.S
-
当发生中断时,处理器会自动跳转到uservec进行处理。
-
uservec的作用
uservec:# 将a0保存到sscratch,用于之后恢复csrw sscratch, a0# 将TRAPFRAME基地址导入a0li a0, TRAPFRAME# 使用a0+offset保存其他30个寄存器到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)# 读出sscratch中保存的a0值,然后保存到TRAPFRAMEcsrr t0, sscratchsd t0, 112(a0)# initialize kernel stack pointer, from p->trapframe->kernel_spld sp, 8(a0)# make tp hold the current hartid, from p->trapframe->kernel_hartidld tp, 32(a0)# load the address of usertrap(), from p->trapframe->kernel_trapld 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 returnjr t0
因为X0寄存器永远是0,所以无需保存
-
usertrap
的工作是确定trap的原因,处理它,然后返回void usertrap(void) {int which_dev = 0;if((r_sstatus() & SSTATUS_SPP) != 0)panic("usertrap: not from user mode");// 修改stvec,内核中的陷阱由kernelvec()处理w_stvec((uint64)kernelvec);struct proc *p = myproc();// sepc寄存器介绍在下方p->trapframe->epc = r_sepc();if(r_scause() == 8){// trap类型是系统调用if(killed(p))exit(-1);// sepc points to the ecall instruction,// but we want to return to the next instruction.p->trapframe->epc += 4;// 启动设备中断intr_on();// 调用syscall进行处理syscall();} else if((which_dev = devintr()) != 0){// ok} else {printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());setkilled(p);}if(killed(p))exit(-1);// give up the CPU if this is a timer interrupt.if(which_dev == 2)yield();usertrapret(); }
-
SEPC:"Supervisor Exception Program Counter","监管者异常程序计数器"。是一个特殊寄存器,存储trap发生时的PC值。
比如程序执行到0x1000处的指令,然后发生了异常。在这种情况下,SEPC寄存器将会存储0x1004,所以sepc存储的就是返回用户态时,应该继续执行的位置
之所以要保存SEPC,是因为可能会切换到另一个进程,而该进程可能会修改sepc
-
关于
intr_on
当xv6进入trap时,默认关闭设备中断响应,因为此时已经进入内核,在内核中执行的中断处理程序是不同的,所以需要先关闭,等到设置好stvec之后,再开启中断响应。
-
-
最后通过usertrapret返回到用户态
void usertrapret(void) {struct proc *p = myproc();// we're about to switch the destination of traps from// kerneltrap() to usertrap(), so turn off interrupts until// we're back in user space, where usertrap() is correct.intr_off();// 设置stvec指向uservecuint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);w_stvec(trampoline_uservec);// 保存内核运行状态,以便下次运行p->trapframe->kernel_satp = r_satp(); // kernel page tablep->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stackp->trapframe->kernel_trap = (uint64)usertrap;p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()// 设置寄存器的值,在使用sret返回到用户态时,需要使用// 设置sstatus的值unsigned long x = r_sstatus();x &= ~SSTATUS_SPP; // clear SPP to 0 for user modex |= SSTATUS_SPIE; // enable interrupts in user modew_sstatus(x);// set S Exception Program Counter to the saved user pc.w_sepc(p->trapframe->epc);// 获取用户页表的地址uint64 satp = MAKE_SATP(p->pagetable);// 使用trampoline.S\userret,跳转回用户态uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);((void (*)(uint64))trampoline_userret)(satp); }
四、系统调用流程
- 参数会被放置在
a0
~a5
的寄存器中 - 系统调用的编号放置在
a7
中 - 在执行ecall后,最终执行到
syscall
函数,执行对应的系统调用 - 返回值放置在
a0
中,如果系统调用无效,返回-1
五、系统调用参数传递
执行系统调用需要得到用户传来的参数
一般是放在寄存器中,通过argint
、argaddr
和argfd
等函数从trapframe
中检索,并以整数、指针或文件描述符的形式保存。
但是有些参数是指针,内核必须使用指针指向的数据,指针带来了两个挑战:
- 用户程序可能有缺陷或恶意,会传递给内核一个无效的指针,或一个旨在欺骗内核访问内核内存而非用户内存的指针。
- xv6内核页表映射与用户页表映射不同,因此内核不能使用普通指令从用户提供的地址加载或存储。
内核实现了一些函数,安全地传输数据。fetchstr
从用户空间复制字符串到内核空间
copyinstr
从用户空间复制数据到内核空间。
六、从内核空间陷入
当xv6执行在内核空间时,stvec
指向kernelvec(kernel/kernelvec.S:10)
发生中断时,直接将寄存器保存在线程栈中,这样,被中断线程保存的寄存器值将安全地留在其堆栈上。
保存寄存器后,跳转到kerneltrap
。其为两种trap做好了准备:设备中断和异常。调用devintr
来处理前者。如果是异常,内核中异常将是致命错误;内核调用panic
停止执行。
如果是计时器中断,并且一个进程的内核线程正在运行(而不是调度程序线程),kerneltrap
会调用yield
,给其他线程一个运行的机会。第7章解释yield
中发生的事情。
附:相关寄存器介绍
这些寄存器只能在管理模式下使用,用户模式下不能读写。
机器模式下也有一组类似的控制寄存器,xv6只在计时器中断使用。
1.stvec
存储trap处理程序的地址,中断时,CPU会跳转到stvec
中的地址。
2. sepc
发生trap时,RISC-V会在这里保存当前的pc
值,用于之后恢复。
从trap返回时,使用sret
指令,其会将sepc
复制到pc
。所以内核可通过sepc
来控制sret
的去向。
3. scause
trap时,RISC-V会在这里放置一个数字,说明trap的原因
4. sscratch
trap handler代码使用sscratch
来避免在保存寄存器之前覆盖它们。
类似于数字交换时的中转站
比如trap处理的代码
csrw sscratch, a0li a0, TRAPFRAMEsd ra, 40(a0)
sd gp, 56(a0)
需要使用a0存储TRAPFRAME作为基地址,保存寄存器的值
但是a0本身也需要保存,所以先将a0保存到sscratch,然后使用a0,最后将sscratch存储到TRAPFRAME中,这样a0就没有丢失。
5.sstatus
SIE位
:设备中断是否启用。如果清空SIE,设备中断将停止,直到重新设置。
SPP位
:指明trap来自用户还是管理模式,并控制sret
返回的模式。比如SPP为0,代表中断来自于用户模式,在之后使用sret
返回时,也会切换回用户模式