在操做系统中,有三种状况会致使CPU的控制流发生转移:用户态中经过ecall
指令进入内核态;异常发生,如除零、访问非法地址;设备中断,如硬盘完成读写请求。上面这些状况能够统称为陷阱(trap)。数组
陷阱在通常状况下应该是透明的,即当执行完处理程序后可以恢复以前程序的状态。这就要求在陷入内核态时,内核要保存以前的寄存器等状态信息,当执行完处理程序以后再进行恢复。app
在XV6中处理陷阱有如下四步:CPU进行硬件操做,汇编向量被设置,C陷阱处理程序决定如何处理,系统调用或设备驱动处理该陷阱。内核中一般分三种状况来分别处理这些陷阱:用户态陷阱、内核态陷阱、时钟中断。函数
RISC-V CPU有一系列控制寄存器来决定如何处理陷阱,这些寄存器是由内核来设置的。操作系统
stvec
:陷阱处理程序入口,CPU会跳转到此处来处理陷阱sepc
:保存陷阱发生时的pc
,使用sret
指令会将pc
恢复scause
:陷阱缘由sscratch
:内核保存特定的值,见下文sstatus
:sstatus
中的SIE
位控制中断是否容许;SPP
位表示陷阱来自用户模式仍是监管模式。当发生陷阱时,硬件会进行如下操做:设计
SIE
是清空的,就不响应SIE
以关闭中断pc
到sepc
SPP
scause
stvec
到pc
硬件不会自动切换内核页表和内核栈,也不会保存除pc
之外的寄存器,处理程序必须完成上述工做。这样设计能够给软件更好的灵活性。而设置pc
的工做必须由硬件完成,由于当切换到内核态时,用户指令可能会破坏隔离性。指针
XV6的用户态陷阱处理流程以下:uservec
-> usertrap
-> usertrapret
-> userret
。code
因为CPU不会进行页表切换,所以用户页表必须包含uservec
函数(stvec
所指向的函数)的映射。该函数要将satp
切换为内核页表,为了切换后的指令能继续执行,该函数必须在用户页表和内核页表中有相同的地址。为了知足上述要求,XV6将一个叫trampoline
的页映射到相同的虚拟地址TRAMPOLINE
,其中包含了trampoline.S
的指令,并设置stvec
为uservec
。进程
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等技术也使用了缺页异常。