網(wǎng)站是先備案 還是先做網(wǎng)站網(wǎng)站策劃書模板范文
在本實驗室中,您將探索頁表并對其進行修改,以簡化將數(shù)據(jù)從用戶空間復制到內(nèi)核空間的函數(shù)。
一、實驗準備
開始編碼之前,請閱讀xv6手冊的第3章和相關(guān)文件:
- kernel/memlayout.h,它捕獲了內(nèi)存的布局。
- kernel/vm.c,其中包含大多數(shù)虛擬內(nèi)存(VM)代碼。
- kernel/kalloc.c,它包含分配和釋放物理內(nèi)存的代碼。
可看這一篇博客來增加理解。
211.xv6——3(page tables)-CSDN博客
要啟動實驗,請切換到pgtbl分支:
$ git fetch
$ git checkout pgtbl
$ make clean
二、Print a page table (easy)
1.實驗要求
? ? ? ? 為了幫助您了解RISC-V頁表,也許為了幫助將來的調(diào)試,您的第一個任務(wù)是編寫一個打印頁表內(nèi)容的函數(shù)。
- 定義一個名為
vmprint()
的函數(shù)。- 它應(yīng)當接收一個
pagetable_t
作為參數(shù),并以下面描述的格式打印該頁表。- 在
exec.c
中的return argc
之前插入if(p->pid==1) vmprint(p->pagetable)
,以打印第一個進程的頁表。- 如果你通過了
pte printout
測試的make grade
,你將獲得此作業(yè)的滿分。現(xiàn)在,當您啟動xv6時,它應(yīng)該像這樣打印輸出來描述第一個進程剛剛完成
exec()
inginit
時的頁表:page table 0x0000000087f6e000 ..0: pte 0x0000000021fda801 pa 0x0000000087f6a000 .. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000 .. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000 .. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000 .. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000 ..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000 .. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000 .. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000 .. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
- 第一行顯示
vmprint
的參數(shù)。之后的每行對應(yīng)一個PTE,包含樹中指向頁表頁的PTE。- 每個PTE行都有一些“
..
”的縮進表明它在樹中的深度。- 每個PTE行顯示其在頁表頁中的PTE索引、PTE比特位以及從PTE提取的物理地址。
- 不要打印無效的PTE。在上面的示例中,頂級頁表頁具有條目0和255的映射。
- 條目0的下一級只映射了索引0,該索引0的下一級映射了條目0、1和2。
您的代碼可能會發(fā)出與上面顯示的不同的物理地址。條目數(shù)和虛擬地址應(yīng)相同。
2.提示
- 你可以將
vmprint()
放在kernel/vm.c中- 使用定義在kernel/riscv.h末尾處的宏
- 函數(shù)
freewalk
可能會對你有所啟發(fā)- 將
vmprint
的原型定義在kernel/defs.h中,這樣你就可以在exec.c
中調(diào)用它了- 在你的
printf
調(diào)用中使用%p
來打印像上面示例中的完成的64比特的十六進制PTE和地址3.實現(xiàn)
(1)首先在kernel/vm.c中添加vmprint()函數(shù)
// 遞歸打印頁表的函數(shù)。 // pagetable是頁表,level表示當前遞歸的深度。 void _vmprint(pagetable_t pagetable, int level) {// 遍歷頁表中的每一個PTE(頁表項)。for (int i = 0; i < 512; i++){pte_t pte = pagetable[i]; // 獲取當前的頁表項。if(pte & PTE_V) // 如果頁表項有效(存在)。{// 打印縮進,根據(jù)當前遞歸的深度level來決定。for (int j = 0; j < level; j++){if(j)printf(" ");printf("..");}uint64 child = PTE2PA(pte); // 獲取頁表項指向的物理地址。printf("%d: pte %p pa %p\n", i, pte, child); // 打印頁表項信息。// 如果不是葉子節(jié)點(沒有R/W/X權(quán)限),繼續(xù)遞歸打印下一級頁表。if((pte & (PTE_W | PTE_R | PTE_X)) == 0){_vmprint((pagetable_t)child, level + 1);}}} }// 打印頁表的入口函數(shù)。 // pagetable是頁表的根。 void vmprint(pagetable_t pagetable) {printf("page table %p\n", pagetable); // 打印頁表的根地址。_vmprint(pagetable, 1); // 從根頁表開始遞歸打印,初始深度為1。 }
(2)在kernel/defs.h中添加定義
(3)在kernel/exec.c中添加
4.測試結(jié)果
三、A kernel page table per process (hard)
? ? ? ? Xv6有一個單獨的用于在內(nèi)核中執(zhí)行程序時的內(nèi)核頁表。內(nèi)核頁表直接映射(恒等映射)到物理地址,也就是說內(nèi)核虛擬地址
x
映射到物理地址仍然是x
。Xv6還為每個進程的用戶地址空間提供了一個單獨的頁表,只包含該進程用戶內(nèi)存的映射,從虛擬地址0開始。因為內(nèi)核頁表不包含這些映射,所以用戶地址在內(nèi)核中無效。因此,當內(nèi)核需要使用在系統(tǒng)調(diào)用中傳遞的用戶指針(例如,傳遞給write()
的緩沖區(qū)指針)時,內(nèi)核必須首先將指針轉(zhuǎn)換為物理地址。本節(jié)和下一節(jié)的目標是允許內(nèi)核直接解引用用戶指針。1.實驗要求
? ? ? ? 你的第一項工作是修改內(nèi)核來讓每一個進程在內(nèi)核中執(zhí)行時使用它自己的內(nèi)核頁表的副本。修改
struct proc
來為每一個進程維護一個內(nèi)核頁表,修改調(diào)度程序使得切換進程時也切換內(nèi)核頁表。對于這個步驟,每個進程的內(nèi)核頁表都應(yīng)當與現(xiàn)有的的全局內(nèi)核頁表完全一致。如果你的usertests
程序正確運行了,那么你就通過了這個實驗。? ? ? ? 閱讀本作業(yè)開頭提到的章節(jié)和代碼;了解虛擬內(nèi)存代碼的工作原理后,正確修改虛擬內(nèi)存代碼將更容易。頁表設(shè)置中的錯誤可能會由于缺少映射而導致陷阱,可能會導致加載和存儲影響到意料之外的物理頁存頁面,并且可能會導致執(zhí)行來自錯誤內(nèi)存頁的指令。
2.提示
- 在
struct proc
中為進程的內(nèi)核頁表增加一個字段- 為一個新進程生成一個內(nèi)核頁表的合理方案是實現(xiàn)一個修改版的
kvminit
,這個版本中應(yīng)當創(chuàng)造一個新的頁表而不是修改kernel_pagetable
。你將會考慮在allocproc
中調(diào)用這個函數(shù)。- 確保每一個進程的內(nèi)核頁表都關(guān)于該進程的內(nèi)核棧有一個映射。在未修改的XV6中,所有的內(nèi)核棧都在
procinit
中設(shè)置。你將要把這個功能部分或全部的遷移到allocproc
中- 修改
scheduler()
來加載進程的內(nèi)核頁表到核心的satp
寄存器(參閱kvminithart
來獲取啟發(fā))。不要忘記在調(diào)用完w_satp()
后調(diào)用sfence_vma()
- 沒有進程運行時
scheduler()
應(yīng)當使用kernel_pagetable
- 在
freeproc
中釋放一個進程的內(nèi)核頁表- 你需要一種方法來釋放頁表,而不必釋放葉子物理內(nèi)存頁面。
- 調(diào)式頁表時,也許
vmprint
能派上用場- 修改XV6本來的函數(shù)或新增函數(shù)都是允許的;你或許至少需要在kernel/vm.c和kernel/proc.c中這樣做(但不要修改kernel/vmcopyin.c,?kernel/stats.c,?user/usertests.c, 和user/stats.c)
- 頁表映射丟失很可能導致內(nèi)核遭遇頁面錯誤。這將導致打印一段包含
sepc=0x00000000XXXXXXXX
的錯誤提示。你可以在kernel/kernel.asm通過查詢XXXXXXXX
來定位錯誤。3.具體實現(xiàn)
本實驗主要是讓每個進程都有自己的內(nèi)核頁表,這樣在內(nèi)核中執(zhí)行時使用它自己的內(nèi)核頁表的副本。
(1)首先在kernel/proc.h里面的struct proc增加內(nèi)核頁表的字段,表示內(nèi)核態(tài)頁表。
(2)在
vm.c
中添加新的方法proc_kpt_init
,該方法用于在allocproc
?中初始化進程的內(nèi)核頁表。這個函數(shù)還需要一個輔助函數(shù)uvmmap
,該函數(shù)和kvmmap
方法幾乎一致,不同的是kvmmap
是對Xv6的內(nèi)核頁表進行映射,而uvmmap
將用于進程的內(nèi)核頁表進行映射。//用于映射虛擬地址到物理地址 void uvmmap(pagetable_t pagetable,uint64 va,uint64 pa,uint64 sz,int perm) {if(mappages(pagetable,va,sz,pa,perm)!=0){panic("uvmmap");} }//用于初始化內(nèi)核頁表 pagetable_t ukvminit() {pagetable_t kernelpt = uvmcreate();if(kernelpt==0)return 0;uvmmap(kernelpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);uvmmap(kernelpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);uvmmap(kernelpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);uvmmap(kernelpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);uvmmap(kernelpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);uvmmap(kernelpt, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);uvmmap(kernelpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);return kernelpt; }
(3)在?
kernel/proc.c
?中的?allocproc
?函數(shù)里添加調(diào)用函數(shù)的代碼:
記得在?
kernel/defs.h
?添加函數(shù)聲明:pagetable_t ukvminit(void);
(4)在內(nèi)核棧的初始化原來是在?
kernel/proc.c
?中的?procinit
?函數(shù)內(nèi),這部分要求將函數(shù)內(nèi)的代碼轉(zhuǎn)移到?allocproc
?函數(shù)內(nèi),因此在上一步初始化內(nèi)核態(tài)頁表的代碼下面接著添加初始化內(nèi)核棧的代碼:
kvminithart
是用于原先的內(nèi)核頁表,我們將進程的內(nèi)核頁表傳進去就可以。在vm.c里面添加一個新方法proc_inithart
。
然后在
scheduler()
內(nèi)調(diào)用即可,但在結(jié)束的時候,需要切換回原先的kernel_pagetable
。直接調(diào)用調(diào)用上面的kvminithart()
就能把Xv6的內(nèi)核頁表加載回去。
(6)?在
freeproc
中釋放一個進程的內(nèi)核頁表。首先釋放頁表內(nèi)的內(nèi)核棧,調(diào)用uvmunmap
可以解除映射,最后的一個參數(shù)(do_free
)為一的時候,會釋放實際內(nèi)存。// free the kernel stack in the RAM uvmunmap(p->kernelpt, p->kstack, 1, 1); p->kstack = 0;
然后釋放進程的內(nèi)核頁表,先在kernel/proc.c里面添加一個方法
proc_freekernelpt
。如下,歷遍整個內(nèi)核頁表,然后將所有有效的頁表項清空為零。如果這個頁表項不在最后一層的頁表上,需要繼續(xù)進行遞歸。void proc_freekernelpt(pagetable_t kernelpt) {// similar to the freewalk method// there are 2^9 = 512 PTEs in a page table.for (int i = 0; i < 512;i++){pte_t pte = kernelpt[i];if(pte&PTE_V){kernelpt[i] = 0;if((pte&(PTE_R|PTE_W|PTE_X))==0){uint64 child = PTE2PA(pte);proc_freekernelpt((pagetable_t)child);}}}kfree((void *)kernelpt); }
(6). 將需要的函數(shù)定義添加到?
kernel/defs.h
?中
(7). 修改
vm.c
中的kvmpa
,將原先的kernel_pagetable
改成myproc()->kernelpt
,使用進程的內(nèi)核頁表。最后,在 vm.c 中添加頭文件:
#include "spinlock.h" #include "proc.h"
最后修改kvmpa函數(shù)
uint64 kvmpa(uint64 va) {uint64 off = va % PGSIZE;pte_t *pte;uint64 pa;pte = walk(myproc()->kernelpt, va, 0); // 修改這里if(pte == 0)panic("kvmpa");if((*pte & PTE_V) == 0)panic("kvmpa");pa = PTE2PA(*pte);return pa+off; }
4.測試結(jié)果
四、Simplify copyin/copyinstr
? ? ? ? 內(nèi)核的
copyin
函數(shù)讀取用戶指針指向的內(nèi)存。它通過將用戶指針轉(zhuǎn)換為內(nèi)核可以直接解引用的物理地址來實現(xiàn)這一點。這個轉(zhuǎn)換是通過在軟件中遍歷進程頁表來執(zhí)行的。在本部分的實驗中,您的工作是將用戶空間的映射添加到每個進程的內(nèi)核頁表(上一節(jié)中創(chuàng)建),以允許copyin
(和相關(guān)的字符串函數(shù)copyinstr
)直接解引用用戶指針。?1.實驗要求
- 將定義在kernel/vm.c中的
copyin
的主題內(nèi)容替換為對copyin_new
的調(diào)用(在kernel/vmcopyin.c中定義);- 對
copyinstr
和copyinstr_new
執(zhí)行相同的操作。- 為每個進程的內(nèi)核頁表添加用戶地址映射,以便
copyin_new
和copyinstr_new
工作。- 如果
usertests
正確運行并且所有make grade
測試都通過,那么你就完成了此項作業(yè)。? ? ? ? 此方案依賴于用戶的虛擬地址范圍不與內(nèi)核用于自身指令和數(shù)據(jù)的虛擬地址范圍重疊。Xv6使用從零開始的虛擬地址作為用戶地址空間,幸運的是內(nèi)核的內(nèi)存從更高的地址開始。然而,這個方案將用戶進程的最大大小限制為小于內(nèi)核的最低虛擬地址。內(nèi)核啟動后,在XV6中該地址是
0xC000000
,即PLIC寄存器的地址;請參見kernel/vm.c中的kvminit()
、kernel/memlayout.h和文中的圖3-4。您需要修改xv6,以防止用戶進程增長到超過PLIC的地址。2.提示
- 先用對
copyin_new
的調(diào)用替換copyin()
,確保正常工作后再去修改copyinstr
- 在內(nèi)核更改進程的用戶映射的每一處,都以相同的方式更改進程的內(nèi)核頁表。包括
fork()
,?exec()
, 和sbrk()
.- 不要忘記在
userinit
的內(nèi)核頁表中包含第一個進程的用戶頁表- 用戶地址的PTE在進程的內(nèi)核頁表中需要什么權(quán)限?(在內(nèi)核模式下,無法訪問設(shè)置了
PTE_U
的頁面)- 別忘了上面提到的PLIC限制
? ? ? ? Linux使用的技術(shù)與您已經(jīng)實現(xiàn)的技術(shù)類似。直到幾年前,許多內(nèi)核在用戶和內(nèi)核空間中都為當前進程使用相同的自身進程頁表,并為用戶和內(nèi)核地址進行映射以避免在用戶和內(nèi)核空間之間切換時必須切換頁表。然而,這種設(shè)置允許邊信道攻擊,如Meltdown和Spectre。
3.實現(xiàn)
? ? ? ? 本實驗是實現(xiàn)將用戶空間的映射添加到每個進程的內(nèi)核頁表,將進程的頁表復制一份到進程的內(nèi)核頁表就好。
? ? ? ? 首先添加復制函數(shù)。需要注意的是,在內(nèi)核模式下,無法訪問設(shè)置了
PTE_U
的頁面,所以我們要將其移除。(1)復制頁表內(nèi)容
void u2kvmcopy(pagetable_t pagetable, pagetable_t kernelpt, uint64 oldsz, uint64 newsz) {pte_t *pte_from, *pte_to;// 將 oldsz 向上取整到最近的頁邊界oldsz = PGROUNDUP(oldsz);// 遍歷 [oldsz, newsz) 范圍內(nèi)的每一頁for (uint64 i = oldsz; i < newsz; i += PGSIZE) {// 從用戶頁表中獲取地址 i 對應(yīng)的 PTEif ((pte_from = walk(pagetable, i, 0)) == 0)panic("u2kvmcopy: src pte does not exist");// 確保用戶頁表中的頁表項是有效的(即包含 PTE_V 標志)if (!(*pte_from & PTE_V))panic("u2kvmcopy: src pte not valid");// 獲取或創(chuàng)建內(nèi)核頁表中地址 i 對應(yīng)的 PTEif ((pte_to = walk(kernelpt, i, 1)) == 0)panic("u2kvmcopy: pte walk failed");// 從用戶頁表項中獲取物理地址uint64 pa = PTE2PA(*pte_from);// 從用戶頁表項中獲取標志,并移除用戶權(quán)限標志 PTE_Uuint flags = (PTE_FLAGS(*pte_from)) & (~PTE_U);// 設(shè)置內(nèi)核頁表項*pte_to = PA2PTE(pa) | flags;} }
第二步,fork(),sbrk(),exec()
然后在內(nèi)核更改進程的用戶映射的每一處 (
fork()
,?exec()
, 和sbrk()
),都復制一份到進程的內(nèi)核頁表。fork()
exec()
sbrk(),?在kernel/sysproc.c里面找到
sys_sbrk(void)
,可以知道只有growproc
是負責將用戶內(nèi)存增加或縮小 n 個字節(jié)。以防止用戶進程增長到超過PLIC
的地址,我們需要給它加個限制。
然后替換掉原有的
copyin()
和copyinstr()
// Copy from user to kernel. // Copy len bytes to dst from virtual address srcva in a given page table. // Return 0 on success, -1 on error. int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len) {return copyin_new(pagetable, dst, srcva, len); }// Copy a null-terminated string from user to kernel. // Copy bytes to dst from virtual address srcva in a given page table, // until a '\0', or max. // Return 0 on success, -1 on error. int copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max) {return copyinstr_new(pagetable, dst, srcva, max); }
并且添加到?
kernel/defs.h
?中// vmcopyin.c int copyin_new(pagetable_t, char *, uint64, uint64); int copyinstr_new(pagetable_t, char *, uint64, uint64);