XV6學習(3)Page tables


頁表是操作系統中非常重要的一部分,用於將虛擬地址轉化為物理地址。虛擬內存是操作系統實現進程隔離的關鍵技術。
在 XV6 中通過 RISC-V 的頁表機構完成了虛擬地址向物理地址的轉換。

分頁硬件機構

XV6 運行於 Sv39 RISC-V 上,64 位地址中的低 39 位被使用。RISC-V 的頁表邏輯上是 page table entries (PTEs) 的數組,長度為 2^27。PTE 包含 44 位物理地址號(PPN)。頁的大小為 4KB,因此,分頁硬件使用 39 位中的高 27 位查找 PTE,之后轉化為 56 位的物理地址。
地址轉換

而實際上,RISC-V 使用的三級頁表,1 級頁表為 1 頁(4KB),包含 512 個 PTE。27 位頁號中的高 9 位為一級頁表,中間 9 位為 2 級頁表,末 9 位為三級頁表。

在 PTE 中,低 8 位為標志位,其中 PTE_V 代表地址是否有效,當訪問無效頁面時會觸發page fault;PTE_U代表地址能否在用戶模式被訪問,如果未設置則頁面只能在 supervisor mode 中訪問。

為了使分頁機構能夠正常運行,操作系統必須設置satp寄存器為1級頁表的物理地址。

內核地址空間

XV6 每個進程擁有一個獨立頁表,同時內核也擁有一個頁表用於描述內核地址空間。內核會將自身地址空間直接映射到物理地址上,來方便訪問物理內存和硬件資源。內核地址空間定義在memlayout.h中,如下圖所示:

在QEMU中,0~0x80000000用於映射設備接口,而0x80000000(KERNBASE) ~ 0x86400000(PHYSTOP)為RAM。

有一小部分內核地址空間不是直接映射的,Trampoline 頁面在地址空間最高的位置,隨后是每個進程對應的內核棧,每個棧之間都有一個 Guard page ,該頁的 PTE_V 設置為 0,用於避免緩沖區溢出。

如果內核棧使用直接映射的方法,那么 Guard page 相對應的物理內存中將會產生很多空洞,導致內存管理變得困難。

Code: 創建地址空間

與地址空間有關的代碼主要在vm.c文件中。pagetable_t代表一級頁表,實際數據類型是一個指針,指向頁表的物理地址。

walk函數是最核心的函數,該函數通過頁表pagetable將虛擬地址va轉換為PTE,如果alloc1就會分配一個新頁面。

kvminit分析

kvminit函數用於初始化內核頁表,該函數在內核啟動開啟分頁機制前被調用,因此是直接對物理地址進行操作的。函數首先通過kalloc申請了一個頁面用於保存一級頁表。kalloc函數就簡單地從kmem.freelist中取出一個空閑頁面。

kmem結構體的初始化在kinit中進行,該函數在kvminit之前被調用。該函數首先初始化鎖,之后使用freerange函數將內核之后的全部空閑 RAM 以 4KB 為一頁加入該鏈表。

void kinit()
{
  initlock(&kmem.lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}

回到kvminit函數,在申請到頁表后,通過調用kvmmap函數,將物理地址中的UART0 CLINT等映射到內核頁表中,完成了內核頁表的初始化。

kvminit函數完成后,main函數緊接着就會調用kvminithart函數。在該函數中,使用MAKE_SATP產生SATP的值,將該值寫入satp寄存器中,之后使用sfence_vma刷新 TLB,完成了虛擬地址轉換的開啟,之后代碼中的地址就全部會通過地址轉換機構進行轉換。

#define SATP_SV39 (8L << 60)
#define MAKE_SATP(pagetable) (SATP_SV39 | (((uint64)pagetable) >> 12))

而在MAKE_SATP中使用SATP_SV39設置 MODE 域為 8,即開啟 SV39 地址轉換,如下圖所示。
satp

kvminithart函數執行完成后,就會調用procinit函數,初始化所有進程結構體,對每個進程結構體申請兩個頁面作為內核棧,之后將該頁面映射到內核地址空間的高位上。最后再次調用kvminithart函數,刷新 TLB,使硬件知道新 PTEs 的加入,防止使用舊的 TLB 項。

sfence.vma

sfence.vma rs1, rs2指令是一條特權指令,用於通知處理器頁表的修改。rs1指示了頁表哪個虛址對應的轉換被修改了;rs2給出了被修改頁表的進程的地址空間標識符(ASID)。如果兩者都是x0,便會刷新整個 TLB。

sfence.vma 僅影響執行當前指令的 hart 的地址轉換硬件。當 hart 更改了另一個 hart 正在使用的頁表時,前一個 hart 必須用處理器間中斷來通知后一個 hart,他應該執行 sfence.vma 指令。這個過程通常被稱為 TLB 擊落。

在 XV6 中,兩個地方使用了sfence.vma指令,一個是上文提到的kvminithart函數,另一個就是trampoline.S中,當陷入內核以及返回用戶態時會調用。

進程地址空間

每個進程擁有獨立的地址空間,當進程切換時同時會對頁表進行切換。XV6 進程地址空間從 0 開始到 MAXVA,即 256GB。

當進程申請內存時,內核就會先調用kalloc函數申請物理頁面,之后構造PTE加入進程對應的頁表項中。
進程地址空間
在進程地址空間的最高位置為 trampoline,所有進程的該頁面映射到同一個物理頁面上。同樣地,在用戶棧的下方也設置了一個 guard page 來防止緩沖區溢出。

Code: sbrk

系統調用char* sbrk(int)用於增加或減少物理內存,當參數為正數時增加,負數時減少。sbrk實際通過growproc進行,growproc調用uvmallocuvmdealloc完成工作。

進程地址空間是從 0 開始連續向上增長的,因此通過proc.sz獲取已分配字節數,就可以計算得到當前已分配空間的頂部地址,之后就可以得到對應的頁面地址。

uvmalloc函數先計算需要申請的頁面數,之后在進程地址空間頂部再申請所需的連續的頁面。函數通過kalloc申請物理頁面,之后使用mappages函數映射到進程頁表中。

uvmdealloc函數先計算需要減少的頁面數,之后通過uvmunmap刪除頁面。在uvmunmap函數內部通過walk獲取對應 PTE,將PTE_V設置為0,最后通過kfree函數將該物理頁面添加到空閑鏈表中。

Code: exec

系統調用exec用於創建進程地址空間。函數首先使用namei獲取可執行文件,讀取 ELF 頭,檢查 ELF 中的 magic。之后使用proc_pagetable創建進程頁表。

proc_pagetable函數中,先使用uvmcreate函數申請一個頁面,之后將 trampoline 和 trapframe 映射到高位地址空間中。

exec之后使用uvmalloc申請內存空間,再使用loadseg函數將程序加載到對應頁面中。在 Program Header 中描述了各段的 filesz,memsz等信息,當 filesz 小於 memsz 時,中間的空隙用 0 填充(如C語言中的全局變量)。

程序加載完成后,再申請兩塊頁面,第一塊為 guard page ,使用uvmclear函數將該頁面PTE_U設置為0,即不允許 user mode 訪問。第二頁設置為進程的棧。然后將argcargv和返回地址壓棧,完成棧的准備工作。

最后,exec函數更新進程結構體,將舊頁表釋放。

在 Program Header 的vaddr中,程序可以指定被加載到的虛擬地址,而這可能是危險的,因此在exec中會檢查if(ph.vaddr + ph.memsz < ph.vaddr),避免發生加法溢出。

實際操作系統

在 XV6 中,內核直接加載到 0x80000000 的位置上,而在實際操作系統中,一般會使用 kaslr 技術,即內核地址隨機化,使攻擊者不能直接通過反匯編獲取內核變量和函數的地址。

在 RISC-V 中支持 super pages,即大小為4MB的頁面,用於降低大內存機器上的頁表開銷。

XV6 也缺少類似於 malloc 的機制來減少使用sbrk大量分配小對象的開銷。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM