MIT-6.S081 XV6 Chapter4 Traps and System Calls

381 阅读8分钟

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 assembly常用指令

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

image-20230509135812287.png 栈从高地址向低地址增长,每个大的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要被取代为stvecsret是从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将执行以下步骤:

  1. 如果trap是一个设备产生的中断,而SIE又被清除的情况下,不做下方的任何动作
  2. 清除SIE来disable一切中断
  3. pc复制到sepc
  4. 把当前的模式(user / supervisor)保存到SPP
  5. 设置scause寄存器来指示产生trap的原因
  6. 将当前的模式设置为supervisor
  7. stvec的值复制到pc
  8. 开始执行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,uservecstvec指向的trap vector instruction。uservec要切换satp到kernel页表,同时kernel页表中也要有和user页表中对uservec相同的映射。RISC-V将uservec保存在trampoline页中,并将TRAMPOLINE放在kernel页表和user页表的相同位置处(MAXVA)。

​ 当uservec开始时所有的32个寄存器都是trap前代码的值,但是uservec需要对某些寄存器进行修改来设置satp,可以用sscratcha0的值进行交换,交换之前的sscratch中是指向user process的trapframe的地址,trapframe中预留了保存所有32个寄存器的空间。p->trapframe保存了每个进程的TRAPFRAME的物理空间从而让kernel页表也可以访问该进程的trapframe。

​ 当交换完a0sscratch之后,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。userretuserrapret调用返回时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,并执行uservecusertrap,然后执行syscall

​ kernel trap code将把寄存器的值保存在当前进程的trapframe中。syscall将把trapframe中的a7寄存器保存的值提取出来,索引到syscalls这个函数数列中查找对应的syscall种类,并进行调用,然后把返回值放置在p->trapframe->a0中,如果执行失败,就返回-1。

​ syscall的argument可以用argintargaddrargfd等函数从内存中取出

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运行。

最后返回到kernelveckernelvec将保存的寄存器值从堆栈中弹出,执行sretsepc复制到pc来执行之前被打断的kernel code