xv6 陷入與系統呼叫
Trap機制
Trap機制就是使用者空間有核心空間的切換,目的是為了安全性隔離,併為了兼顧效率,由於系統呼叫與lazy allocation等導致的page falut的頻繁發生,Trap要設計的儘可能簡單
有三種情況會發生trap:
- 程式執行系統呼叫
- 程式出現了類似page fault、運算時除以0的錯誤,就是異常
- 一個裝置觸發了中斷使得當前程式執行需要響應核心裝置驅動,就是中斷
初始時,shell程式(也就是shell指令碼的直譯器)執行在使用者態,如果要執行系統呼叫,比如write,就會從擁有user許可權並且位於使用者空間切換到擁有supervisor許可權的核心。
切換到核心需要修改一個程式狀態,其中最重要的就是32個使用者暫存器,使用暫存器的指令效能最好
- 由於核心程式也需要使用這些暫存器,並且為了安全性考慮,核心程式碼不應該去使用使用者態下的暫存器中的資料,因為其中可能儲存著惡意資料,所以為了安全性與透明性,在Trap之前,以及回到使用者態之後,這些暫存器的值不能夠改變
- pc暫存器也需要儲存,這樣返回核心的時候才知道去執行哪一條使用者態指令
- 我們需要將mode改成supervisor mode,因為我們想要使用核心中的各種各樣的特權指令
- SATP暫存器現在正指向user page table,而user page table只包含了使用者程式所需要的記憶體對映和一兩個其他的對映,它並沒有包含整個核心資料的記憶體對映。所以在執行核心程式碼之前,我們需要將SATP指向kernel page table。
- Trap過程也需要去將堆疊暫存器(堆疊暫存器屬於32個使用者暫存器)指向位於核心的一個地址,因為我們需要一個堆疊來呼叫核心的C函式。
supervisor mode的特權
- 可以讀寫控制暫存器了。比如說,當你在supervisor mode時,你可以:讀寫SATP暫存器,也就是page table的指標;STVEC,也就是處理trap的核心指令地址;SEPC,儲存當發生trap時的程式計數器;SSCRATCH,儲存了
trapframe page
的使用者頁表虛擬地址,等等。在supervisor mode你可以讀寫這些暫存器,而使用者程式碼不能做這樣的操作。 - 它可以使用PTE_U標誌位為0的PTE。當PTE_U標誌位為1的時候,表明使用者程式碼可以使用這個頁表;如果這個標誌位為0,則只有supervisor mode可以使用這個頁表
supervisor mode也存在限制
- supervisor mode中的程式碼並不能讀寫任意實體地址。在supervisor mode中,就像普通的使用者程式碼一樣,也需要通過page table來訪問記憶體。如果一個虛擬地址並不在當前由SATP指向的page table中,又或者SATP指向的page table中PTE_U=1(也就是使用者態才能夠讀的頁表項),那麼supervisor mode不能使用那個地址。所以,即使我們在supervisor mode,我們還是受限於當前page table設定的虛擬地址。
Trap的執行流程
以write系統呼叫舉例:
- 在使用者態把系統呼叫號放到a7暫存器
- ecall指令(ecall是一個硬體指令)
- trampoline中的uservec()
- trap.c中的usertrap()
- syscall()
- sys_write()
- 回到trap.c中usertrap()
- trap.c中的usertrapret()
- trampoline中的userret()
- 回到使用者態
ECALL之前
- wirte函式關聯到了一個庫函式,這個庫函式在user/usys.S中
- 在ecall之前的
使用者頁表
的最後兩項表示trampoline與trampframe頁
由於標誌位u未置位,那麼只有在supervisor mode才能訪問這兩個PTE,在ecall之後,可以訪問每一個程序虛擬地址空間中的trampoline與trapframe頁
ECALL之後
- ECALL(ecall是一個硬體指令指令)會做三件事:
- 將user mode改為supervisor mode
- ecall將程式計數器的值儲存在了SEPC暫存器。
-
STVEC是一個核心暫存器,其中存有
trampoline page
的最開始的地址,但是核心暫存器只有在supervisor mode下才能讀寫,由於ecall將程式碼從user mode改為了supervisr mode,ecall便可以使pc指向trampoline page
的最開始,trampoline page
中的
- 由於ECALL指令將將user mode改為supervisor mode,(這個時候頁表還是使用者頁表)
那麼這個時候就可以可以訪問頁表項的最後一項 -
trampoline page
的第一條指令是csrrw a0, sscratch, a0
,這條指令將a0的資料儲存在了sscratch中,同時又將sscratch內的資料儲存在a0中。之後核心就可以任意的使用a0暫存器了。 -
trampoline page
包含了trap處理程式碼,因為ecall並不會切換page table,我們需要在user page table中的某個地方來執行最初的核心程式碼。 - 為了保持ecall指令的靈活性,ecall指令不會不會儲存使用者暫存器,或者切換page table指標來指向kernel page table,或者自動的設定Stack Pointer指向kernel stack,或者直接跳轉到kernel的C程式碼,,ecall靈活性可以帶來如下好處:
uservec
uservec是trampoline頁的最開始的函式
-
每個程序被建立的時候會被分配一個trapframe,並做好在
user page table中做好對映
,其在程序的虛擬地址空間中trampoline
的正下方 -
使用
csrrw
指令,交換a0和sscratch
兩個暫存器的內容,sscratch
中存有的是trapframe page
的虛擬地址 -
之後是儲存使用者的32個暫存器到
trapframe
-
每個程序的
trapframe
還保留了5個核心資料其中kernel_sp
就是程序的核心棧,暫存器sp
的值會被設定為它 -
儲存CPU核的編號到
tp
暫存器,在核心中好幾個地方都會使用了這個值,例如,核心可以通過這個值確定某個CPU核上運行了哪些程序。 -
將之後要執行的
usertrap
函式的指標放入t0
暫存器 -
將使用者頁表轉換為核心頁表
-
進入
usertrap
函式
usertrap函式
-
首先將
kernelvec()
的地址放入stvec
暫存器中,使用者態的時候放的是trampoline
的地址,在核心態,處理trap的函式是kernelvec()
-
之後是儲存
sepc
暫存器,其中儲存的是當發生trap時的程式計數器,因為可能發生這種情況:當程式還在核心中執行時,我們可能切換到另一個程序,並進入到那個程式的使用者空間,然後那個程序可能再呼叫一個系統呼叫進而導致SEPC暫存器的內容被覆蓋。所以,我們需要儲存當前程序的SEPC暫存器到一個與該程序關聯的記憶體中,這樣這個資料才不會被覆蓋。這裡我們使用trapframe來儲存這個程式計數器。 -
根據SCAUSE暫存器中的數字判斷trap的型別做出相應的處理,
- 對於系統呼叫,我們需要
p->trapframe->epc += 4;
,因為我們希望返回到使用者態時,去執行ecall下面的一條指令由於有些系統呼叫需要大量時間處理,所以當儲存好了當前程序的暫存器等相關狀態後,可以開啟中斷,之後去呼叫syscall();
可以看到核心中的實現系統呼叫的函式就是通過程序的trapframe獲取引數 - 如果是裝置中斷,就交給devintr
- 如果是異常,那麼就終止該程序的執行。
- 對於系統呼叫,我們需要
-
處理完成系統呼叫等問題後,回到
usertrap函式
,執行usertrapret(void)
函式
usertrapret
其他問題
-
為什麼沒有把函式引數放到暫存器的指令,
函式呼叫的呼叫規範保證了放到暫存器了
- 移位了是什麼意思