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

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

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

在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 -> userretcode

因为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也指向该页面。it

最后,函数从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等技术也使用了缺页异常。

相关文章
相关标签/搜索