在操做系統中,有三種狀況會致使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等技術也使用了缺頁異常。