XV6学习(5)陷阱和系统调用

在操作系统中,有三种情况会导致CPU的控制流发生转移:用户态中通过ecall指令进入内核态;异常发生,如除零、访问非法地址;设备中断,如硬盘完成读写请求。上面这些情况可以统称为陷阱(trap)。

陷阱在一般情况下应该是透明的,即当执行完处理程序后能够恢复之前程序的状态。这就要求在陷入内核态时,内核要保存之前的寄存器等状态信息,当执行完处理程序之后再进行恢复。

在XV6中处理陷阱有以下四步:CPU进行硬件操作,汇编向量被设置,C陷阱处理程序决定如何处理,系统调用或设备驱动处理该陷阱。内核中通常分三种情况来分别处理这些陷阱:用户态陷阱、内核态陷阱、时钟中断。

RISC-V CPU有一系列控制寄存器来决定如何处理陷阱,这些寄存器是由内核来设置的。

  • stvec:陷阱处理程序入口,CPU会跳转到此处来处理陷阱
  • sepc:保存陷阱发生时的pc,使用sret指令会将pc恢复
  • scause:陷阱原因
  • sscratch:内核保存特定的值,见下文
  • sstatussstatus中的SIE位控制中断是否允许;SPP位表示陷阱来自用户模式还是监管模式。

当发生陷阱时,硬件会进行以下操作:

  1. 如果是设备中断,并且SIE是清空的,就不响应
  2. 清空SIE以关闭中断
  3. 保存pcsepc
  4. 保存当前模式到SPP
  5. 设置scause
  6. 切换到监管模式
  7. 拷贝stvecpc
  8. 开始执行处理程序

硬件不会自动切换内核页表和内核栈,也不会保存除pc以外的寄存器,处理程序必须完成上述工作。这样设计可以给软件更好的灵活性。而设置pc的工作必须由硬件完成,因为当切换到内核态时,用户指令可能会破坏隔离性。

用户态陷阱

XV6的用户态陷阱处理流程如下:uservec -> usertrap -> usertrapret -> userret

由于CPU不会进行页表切换,因此用户页表必须包含uservec函数(stvec所指向的函数)的映射。该函数要将satp切换为内核页表,为了切换后的指令能继续执行,该函数必须在用户页表和内核页表中有相同的地址。为了满足上述要求,XV6将一个叫trampoline的页映射到相同的虚拟地址TRAMPOLINE,其中包含了trampoline.S的指令,并设置stvecuservec

uservec

在进入uservec函数时,所有的32个寄存器都是被中断代码所享有的,而uservec需要使用寄存器来执行指令,因此,RISC-V提供了sscratch寄存器,通过csrrw a0, sscratch, a0指令,保存a0,之后就可以使用a0寄存器了。

之后,函数就需要保存所有用户寄存器到trapframe结构体中,该结构体的地址在进入用户模式之前,被保存在sscratch寄存器中,因此经过之前的csrrw操作后,就被保存在a0中。当创建进程时,内核会申请一个页面保存trapframe,该页面就位于TRAMPOLINE下方,进程的p->trapframe也指向该页面。

最后,函数从trapframe中取出内核栈地址、hartid、usertrap的地址、内核页表地址,切换页表,跳转到usertrap函数。

usertrap

usertrap的工作即判断陷阱类型并处理,最后返回。函数首先将stvec设置为kernelvec的地址,使内核态发生的中断由kernelvec函数来处理。之后保存sepc寄存器,防止其被覆盖。然后判断陷阱类型,如果是系统调用,就将pc指向ecall的下一条指令,然后交给syscall函数处理;如果是设备中断,就交给devintr;否则就是异常,那么就终止该进程的运行。在最后会判断进程是否已经被杀死或者当发生时钟中断时,让出处理器。

usertrapret

该函数首先将stvec设置为uservec的地址,之后设置trapframe(这些内容在uservec中会使用到),然后恢复sepc寄存器。最后,调用userret函数。

最后,在userret函数中进行与uservec相反的步骤,将页表和寄存器进行恢复。

系统调用

initcode.S中的系统调用为例,将两个参数分别放在a0 a1寄存器中,将系统调用号放在a7寄存器中,然后执行ecall指令。

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall

而在syscall函数中,会取出a7的值,然后查找syscalls数组,找到相应的处理函数即sys_exec,交由该函数进行处理,最后将返回值放在trapframe->a0中。

内核态陷阱

内核态陷阱的处理路径为:kernelvec -> kerneltrap -> kernelvec

kernelvec

由于陷阱发生在内核态,因此,不需要对satp和栈指针进行处理,只需要保存所有通用寄存器即可。之后跳转到kerneltrap进行处理,当该函数返回后,再恢复所保存的寄存器。

kerneltrap

kerneltrap只需要处理两种陷阱:设备中断和异常。通过调用devintr判断是否为设备中断,如果不是设备中断,那么就是异常,且该异常发生在内核态,内核调用panic函数终止执行。如果是时钟中断,那么就让出处理器。由于yield函数会导致sepc sstatus寄存器被修改,因此在kerneltrap中要对其进行保存和恢复。

缺页异常

在XV6中,并没有对异常进行处理,仅仅是简单地kill或panic。而在真实操作系统中,会对异常进行具体的处理。例如使用缺页异常来实现COW(copy on write)fork。

在RISC-V中,有三种不同的缺页异常:load page faults(当load指令转换虚拟地址时发生),store page faults(当store指令转换虚拟地址时发生),instruction page faults(当指令的地址转化时发生)。在scause寄存器中保存了异常原因,stval中保存了转换失败的地址。

COW fork使子进程与父进程享有相同的物理页面,但是设置为只读的。当子进程或父进程执行store指令时,就会触发异常,此时再对页面进行拷贝,然后以读写的模式映射到父子进程的地址空间。

另一种技术是lazy allocation,当应用调用sbrk时,增长地址空间,但在页表中标记新地址为无效的。当在新地址上发生缺页异常后,才真正地分配物理页面给进程。

paging from disk即虚拟内存,操作系统选择一部分保存到磁盘上并标记页表项为无效,当读写该页面时再从磁盘中取回内存。除此之外,还有如automatically extending stacks 和 memory-mapped files等技术也使用了缺页异常。

posted @ 2021-01-04 10:35  星見遥  阅读(1428)  评论(0编辑  收藏  举报