Chapter4 Traps and System Calls
三种导致CPU中断当前指令的执行,转而去处理当前事件的情况,这三种情况统称为trap。
- system call
- exception
- device interrupt
trap的大致处理思路:
- trap迫使控制权转移到内核;
- 内核保存寄存器和其他状态,以便可以恢复;
- 内核执行适当的处理程序代码;
- 内核恢复保存的状态并从trap中返回;
- 原有的代码恢复到它离开的地方。
xv6在kernel中处理所有traps。
Xv6的trap处理程序四阶段:
- Hardware actions taken by the RISC-V CPU;
- 一些汇编指令,为内核C代码做准备;
- 一个决定如何处理trap的C函数;
- 系统调用或设备驱动服务例程或直接杀死该进程。
通常对于user space的trap、kernel space的trap和计时器中断会有不同的trap handler。
RISC-V 汇编代码示例
ld/lb/lw rd, 8(rs) # 将*(rs+8)的值写入到rd寄存器,lb=load byte, ld=load double word, lw=load word
sd/sb/sw rs, 8(rd) # 将*(rd)的值写入到rs+8地址上
add rd, rs1, rs2 # *(rd) <- *(rs1) + *(rs2)
addi rd, rs1, int # *(rd) <- *(rs1) + int
mv t0, a0 # t0 <- a0
li a0, 0 # a0 <- 0
栈从高地址向低地址增长,每个大的box叫一个stack frame(栈帧),栈帧由函数调用来分配,每个栈帧大小不一定一样,但是栈帧的最高处一定是return address。
sp是stack pointer,用于指向栈顶(低地址),保存在寄存器中。
fp是frame pointer,用于指向当前帧底部(高地址),保存在寄存器中,同时每个函数栈帧中保存了调用当前函数的函数(父函数)的fp(保存在to prev frame那一栏中)。
RISC-V trap machinery
stvec:trap handler的地址,由kernel写入
sepc:保存trap发生时的现场program counter,因为接下来pc要被取代为stvec。sret是从trap回到现场的指令,将sepc写回到pc
scause:一个trap产生的原因代码,由CPU写入
sscratch:放在trap handler的最开始处
sstatus:控制设备中断是否被开启,如果sstatus中的SIE位被清除,则RISC-V将推迟设备中断。SPP位指示这个trap是在user space中产生的还是在kernel space产生的,并将控制sret回到什么模式
以上寄存器只在supervisor模式下发生的trap被使用
SIE(Supervisor Interrupt Enable)寄存器:这个寄存器中有一个bit(SEIE)专门针对例如UART的外部设备的中断;有一个bit(SSIE)专门针对软件中断,软件中断可能由一个CPU核触发给另一个CPU核;还有一个bit(STIE)专门针对定时器中断。我们这节课只关注外部设备的中断。
SIP(Supervisor Interrupt Pending)寄存器。当发生中断时,处理器可以通过查看这个寄存器知道当前是什么类型的中断。
satp: (监督员地址转换和保护)寄存器持有页表根的(物理)地址。
stval: (Supervisor Trap Value)寄存器持有页面故障(虚拟)地址
上述寄存器与在监督员模式下处理的陷阱有关,它们不能在用户模式下读或写。有一组类似的控制寄存器用于在机器模式下处理的陷阱;xv6只在定时器中断的特殊情况下使用它们。
当发生除了计时器中断以外的其他类型的trap时,RISC-V将执行以下步骤:
- 如果trap是一个设备产生的中断,而SIE又被清除的情况下,不做下方的任何动作
- 清除SIE来disable一切中断
- 把
pc复制到sepc - 把当前的模式(user / supervisor)保存到SPP
- 设置
scause寄存器来指示产生trap的原因 - 将当前的模式设置为supervisor
- 将
stvec的值复制到pc - 开始执行
pc指向的trap handler的代码
注意CPU并没有切换到kernel页表,也没有切换到kernel栈
Traps from user space
什么时候触发?
- system call (ecall instruction);
- does something illegal;
- or if a device interrupts.
当user space中发生trap时,会将stvec的值复制到pc,而此时stvec的值是trampoline.S中的uservec,因此跳转到uservec,先保存一些现场的寄存器,恢复kernel栈指针、kernel page table到satp寄存器,再跳转到usertrap(kernel/trap.c)trap handler,然后返回usertrapret(kernel/trap.c),跳回到kernel/trampoline.S,最后用userret(kernel/trampoline.S)通过sret跳回到user space。
uservec
- 交换a0和sscratch中的值。交换后a0中保存的是trapframe的地址,而sscratch中保存a0的值。
- 保存32个user寄存器的值。以a0中trapframe的地址为基础,做一定的偏移,然后将所有的32个registers的值都保存到trapframe中,这里需要注意的是,在进程创建的时候,xv6都会为每一个进程的trapframe分配一页,并建立映射关系。
- 最后将trapframe中的其他参数加载到部分register中,包括加载kernel stack pointer的地址,usertrap()函数的地址,kernel pagetable的地址,最后跳转到usertrap()函数。
usertrap主要作用是确定陷阱的原因,对其进行处理,然后返回。
- 首先检查该trap是否来自于user mode。
- 改变stvec的值指向kernelvec而不是userevec,从而将后续过程中发生的trap类型为interrupts和exceptions交给kerneltrap()来处理,因为现在已经在kernel中了。
- 保存spec中的user porgram counter。因为usertrap可能会调用yield()函数,改变到另一个process’s kernel thread,而另一个process可能会返回user space,从而改变sepc的值。
- 判断trap的类型。如果trap是一个syscall,就调用syscall()处理;设备中断就调用devintr()处理,否则就是异常,kernel杀死该进程。
- 在syscall()处理之前,需要做一些准备工作。比如p→trapframe→epc需要+4。
- 最后在usertrap()函数结束之前,会检查该process是否被杀死,或者如果是timer interrupt的话,需要出让CPU。
usertrapret主要用于返回user space。
- 准备工作:关闭中断;
- 设置stvec指向uservec;
- 为process将来再一次的trap设置好trapframe。比如kernel pagetable,进程的kernel stack,hartid等等;
- 将SPP置为0,恢复中断功能;
- p→trapframe→epc设置为user pc;
- 最后调用userret汇编函数;
userret主要用于完成从kernel到user态的完整切换。
- 传入的参数中,a1存放指向process’s 用户态页表的指针,首先交换a1和satp,调用汇编指令切换到user pagetable;
- a0存放TRAPFRAME,将a0的值存在t0,然后存到sscratch中;
- 从trapframe中恢复除了a0之外的所有寄存器的值;
- 交换sscratch和a0的值,恢复a0,并将trapframe保存到了sscratch,为下一次trap做好了准备;
- 最后调用
sret来返回到用户空间;sret与ecall相反
- 启用中断。
- 将sepc复制到PC上。
- 将模式设置为用户模式。
- 在新电脑上开始执行。
RISC-V在trap中不会改变页表,因此user page table必须有对uservec的mapping,uservec是stvec指向的trap vector instruction。uservec要切换satp到kernel页表,同时kernel页表中也要有和user页表中对uservec相同的映射。RISC-V将uservec保存在trampoline页中,并将TRAMPOLINE放在kernel页表和user页表的相同位置处(MAXVA)。
当uservec开始时所有的32个寄存器都是trap前代码的值,但是uservec需要对某些寄存器进行修改来设置satp,可以用sscratch和a0的值进行交换,交换之前的sscratch中是指向user process的trapframe的地址,trapframe中预留了保存所有32个寄存器的空间。p->trapframe保存了每个进程的TRAPFRAME的物理空间从而让kernel页表也可以访问该进程的trapframe。
当交换完a0和sscratch之后,uservec可以通过a0把所有当前寄存器的值保存到trapframe中。由于当前进程的trapframe已经保存了当前进程的kernel stack、当前CPU的hartid、usertrap的地址、kernel page table的地址等,uservec需要获取这些值,然后切换到kernel pagetable,调用usertrap。
usertrap主要是判断trap产生的原因并进行处理,然后返回。因为当前已经在kernel里了,所以这时候如果再发生trap,应该交给kernelvec处理,因此要把stvec切换为kernelvec。如果trap是一个system call,那么syscall将被调用,如果是设备中断,调用devintr,否则就是一个exception,kernel将杀死这个出现错误的进程。
回到user space的第一步是调用usertrapret(),这个函数将把stvec指向uservec,从而当回到user space再出现trap的时候可以跳转到uservec,同时设置p->trapframe的一些值为下一次trap作准备,比如设置p->trapframe->kernel_sp = p->kstack + PGSIZE。**清除SPP为从而使得调用sret后能够回到user mode。**设置回到user space后的program counter为p->trapframe->epc,最后调用跳转到TRAMPOLINE页上的userret回到trampoline.S,加载user page table。userret被userrapret调用返回时a0寄存器中保存了TRAPFRAME,因此可以通过这个TRAPFRAME地址来恢复之前所有寄存器的值(包括a0),最后把TRAPFRAME保存在sscratch中,用sret回到user space。
trampoline page是什么,能发挥什么作用?
Xv6使用trampoline页面来满足这些要求。trampoline page包含uservec,即stvec所指向的xv6陷阱处理代码。 trampoline page被映射到每个进程的页面表中,地址为TRAMPOLINE,它位于虚拟地址空间的末端,因此它将位于程序自己使用的内存之上。 trampoline page也被映射在内核页表的地址TRAMPOLINE处。注意:kernel page table 中只有trampoline page,而每个process中除了trampoline page之外,还有trapframe这一页.
trampoline page如何保证在从user pgtbl切换到kernel pgtbl之后,原有的中断程序能够继续执行?
因为trampoline page被映射在用户页表中,通过PTE_U标志,trap可以在监督员模式下开始执行。
因为trampoline page映射在内核地址空间的同一地址上,trap处理程序在切换到内核页表后可以继续执行。
Calling system calls
user调用exec执行system call的过程:用的参数放入a0、a1;(SYS_exec)代码放入a7,ecall指令将陷入内核中(通过usys.pl中的entry)。
ecall的效果有三个:
- 将CPU从user mode切换到supervisor mode;
- 将
pc保存到epc以供后续恢复; - 将
uservec设置为stvec,并执行uservec、usertrap,然后执行syscall。
kernel trap code将把寄存器的值保存在当前进程的trapframe中。syscall将把trapframe中的a7寄存器保存的值提取出来,索引到syscalls这个函数数列中查找对应的syscall种类,并进行调用,然后把返回值放置在p->trapframe->a0中,如果执行失败,就返回-1。
syscall的argument可以用argint、argaddr、argfd等函数从内存中取出
Traps from kernel space
什么情况会触发traps from kernel space?
在监督员模式下,会发生以下情况:interrupts,exceptions。
当执行kernel code发生CPU trap的时候,stvec是指向kernelvec的汇编代码的。kernelvec将寄存器的值保存在被中断的kernel thread的栈里而不是trapframe里,这样当trap需要切换kernel thread时,再切回来之后还可以从原先的thread栈里找到之前的寄存器值。
保存完寄存器之后,跳转到kerneltrap这个trap handler。kerneltrap可以对设备中断和exception这两种trap进行处理。如果是设备中断,调用devintr进行处理,如果是exception就panic,如果是因为计时器中断,就调用yield让其他kernel thread运行。
最后返回到kernelvec中,kernelvec将保存的寄存器值从堆栈中弹出,执行sret,将sepc复制到pc来执行之前被打断的kernel code。