1. 程式人生 > 其它 >xv6 陷入與系統呼叫

xv6 陷入與系統呼叫

Trap機制

Trap機制就是使用者空間有核心空間的切換,目的是為了安全性隔離,併為了兼顧效率,由於系統呼叫與lazy allocation等導致的page falut的頻繁發生,Trap要設計的儘可能簡單

有三種情況會發生trap:

  • 程式執行系統呼叫
  • 程式出現了類似page fault、運算時除以0的錯誤,就是異常
  • 一個裝置觸發了中斷使得當前程式執行需要響應核心裝置驅動,就是中斷

初始時,shell程式(也就是shell指令碼的直譯器)執行在使用者態,如果要執行系統呼叫,比如write,就會從擁有user許可權並且位於使用者空間切換到擁有supervisor許可權的核心。

切換到核心需要修改一個程式狀態,其中最重要的就是32個使用者暫存器,使用暫存器的指令效能最好

注意PC,MODE,SATP,STVEC,SEPC,SSCRATCH這些暫存器不屬於32個暫存器之中。Trap過程中暫存器的變化:

  • 由於核心程式也需要使用這些暫存器,並且為了安全性考慮,核心程式碼不應該去使用使用者態下的暫存器中的資料,因為其中可能儲存著惡意資料,所以為了安全性與透明性,在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

其他問題

  • 為什麼沒有把函式引數放到暫存器的指令,
    函式呼叫的呼叫規範保證了放到暫存器了
  • 移位了是什麼意思