頁表是操做系統中很是重要的一部分,用於將虛擬地址轉化爲物理地址。虛擬內存是操做系統實現進程隔離的關鍵技術。
在 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 位的物理地址。
app
而實際上,RISC-V 使用的三級頁表,1 級頁表爲 1 頁(4KB),包含 512 個 PTE。27 位頁號中的高 9 位爲一級頁表,中間 9 位爲 2 級頁表,末 9 位爲三級頁表。函數
在 PTE 中,低 8 位爲標誌位,其中 PTE_V 表明地址是否有效,當訪問無效頁面時會觸發page fault;PTE_U表明地址可否在用戶模式被訪問,若是未設置則頁面只能在 supervisor mode 中訪問。
ui
爲了使分頁機構可以正常運行,操做系統必須設置satp
寄存器爲1級頁表的物理地址。操作系統
XV6 每一個進程擁有一個獨立頁表,同時內核也擁有一個頁表用於描述內核地址空間。內核會將自身地址空間直接映射到物理地址上,來方便訪問物理內存和硬件資源。內核地址空間定義在memlayout.h
中,以下圖所示:
在QEMU中,0~0x80000000用於映射設備接口,而0x80000000(KERNBASE) ~ 0x86400000(PHYSTOP)爲RAM。指針
有一小部份內核地址空間不是直接映射的,Trampoline 頁面在地址空間最高的位置,隨後是每一個進程對應的內核棧,每一個棧之間都有一個 Guard page ,該頁的 PTE_V 設置爲 0,用於避免緩衝區溢出。code
若是內核棧使用直接映射的方法,那麼 Guard page 相對應的物理內存中將會產生不少空洞,致使內存管理變得困難。對象
與地址空間有關的代碼主要在vm.c
文件中。pagetable_t
表明一級頁表,實際數據類型是一個指針,指向頁表的物理地址。blog
walk
函數是最核心的函數,該函數經過頁表pagetable
將虛擬地址va
轉換爲PTE,若是alloc
爲1
就會分配一個新頁面。接口
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 地址轉換,以下圖所示。
在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 來防止緩衝區溢出。
系統調用char* sbrk(int)
用於增長或減小物理內存,當參數爲正數時增長,負數時減小。sbrk
實際經過growproc
進行,growproc
調用uvmalloc
或uvmdealloc
完成工做。
進程地址空間是從 0 開始連續向上增加的,所以經過
proc.sz
獲取已分配字節數,就能夠計算獲得當前已分配空間的頂部地址,以後就能夠獲得對應的頁面地址。
uvmalloc
函數先計算須要申請的頁面數,以後在進程地址空間頂部再申請所需的連續的頁面。函數經過kalloc
申請物理頁面,以後使用mappages
函數映射到進程頁表中。
uvmdealloc
函數先計算須要減小的頁面數,以後經過uvmunmap
刪除頁面。在uvmunmap
函數內部經過walk
獲取對應 PTE,將PTE_V設置爲0,最後經過kfree
函數將該物理頁面添加到空閒鏈表中。
系統調用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 訪問。第二頁設置爲進程的棧。而後將argc
、argv
和返回地址壓棧,完成棧的準備工做。
最後,exec
函數更新進程結構體,將舊頁表釋放。
在 Program Header 的vaddr
中,程序能夠指定被加載到的虛擬地址,而這多是危險的,所以在exec
中會檢查if(ph.vaddr + ph.memsz < ph.vaddr)
,避免發生加法溢出。
在 XV6 中,內核直接加載到 0x80000000 的位置上,而在實際操做系統中,通常會使用 kaslr 技術,即內核地址隨機化,使攻擊者不能直接經過反彙編獲取內核變量和函數的地址。
在 RISC-V 中支持 super pages,即大小爲4MB的頁面,用於下降大內存機器上的頁表開銷。
XV6 也缺乏相似於 malloc 的機制來減小使用sbrk
大量分配小對象的開銷。