這一個實驗主要是學習XV6的頁表(分頁機制),關於分頁機制的相關內容已經寫在XV6學習 (3)裏面了。
代碼放在Github上。git
這一個就是要實現一個vmprint()
函數來遍歷頁表並打印,能夠仿照freewalk()
函數來寫。github
void printwalk(pagetable_t pagetable, uint level) { char* prefix; if (level == 2) prefix = ".."; else if (level == 1) prefix = ".. .."; else prefix = ".. .. .."; for(int i = 0; i < 512; i++){ pte_t pte = pagetable[i]; if(pte & PTE_V){ uint64 pa = PTE2PA(pte); printf("%s%d: pte %p pa %p\n", prefix, i, pte, pa); if((pte & (PTE_R|PTE_W|PTE_X)) == 0){ printwalk((pagetable_t)pa, level - 1); } } } } void vmprint(pagetable_t pagetable) { printf("page table %p\n", pagetable); printwalk(pagetable, 2); }
在這裏是經過pte & (PTE_R|PTE_W|PTE_X)
來判斷當前PTE是否是指向下一級頁表。app
這一題是要爲每一個進程分配一個獨立的內核頁表,而不是使用全局的內核頁表。這一題主要是爲了下一題作準備。函數
所以,首先就是要創建一個函數來建立內核頁表。這個函數內部只要仿照kvminit
函數,給對應的頁面建立映射就好了。post
pagetable_t proc_kpagetable() { pagetable_t kpagetable; kpagetable = uvmcreate(); if(kpagetable == 0) return 0; ukvmmap(kpagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W); ukvmmap(kpagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W); ukvmmap(kpagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W); ukvmmap(kpagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W); ukvmmap(kpagetable, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X); ukvmmap(kpagetable, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W); ukvmmap(kpagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X); return kpagetable; } void ukvmmap(pagetable_t pagetable ,uint64 va, uint64 pa, uint64 sz, int perm) { if(mappages(pagetable, va, sz, pa, perm) != 0) panic("ukvmmap"); }
以後,將procinit
函數中的內核棧的映射移動到allocproc
函數中。在allocproc
函數中先建立一個內核頁表,以後將內核棧映射到對應位置上就能夠了。學習
static struct proc* allocproc(void) { ... // Allocate a trapframe page. if((p->trapframe = (struct trapframe *)kalloc()) == 0){ release(&p->lock); return 0; } // An empty user page table. p->pagetable = proc_pagetable(p); if(p->pagetable == 0){ freeproc(p); release(&p->lock); return 0; } // create the kernel page table. p->kpagetable = proc_kpagetable(p); if(p->kpagetable == 0){ freeproc(p); release(&p->lock); return 0; } // init the kernel stack. char *pa = kalloc(); if(pa == 0) panic("kalloc"); uint64 va = KSTACK((int) (p - proc)); ukvmmap(p->kpagetable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W); p->kstack = va; // Set up new context to start executing at forkret, // which returns to user space. memset(&p->context, 0, sizeof(p->context)); p->context.ra = (uint64)forkret; p->context.sp = p->kstack + PGSIZE; return p; }
在scheduler
函數中在進程切換以後對內核頁表也進行切換,記得用sfence_vma
刷新TLB。ui
// switch the kernel pagetable. w_satp(MAKE_SATP(p->kpagetable)); sfence_vma();
最後一步就是在freeproc
的時候對內核頁表和內核棧也進行釋放。spa
static void freeproc(struct proc *p) { if(p->trapframe) kfree((void*)p->trapframe); p->trapframe = 0; // free kstack pte_t *pte = walk(p->kpagetable, p->kstack, 0); if(pte == 0) panic("freeproc: free kstack"); kfree((void*)PTE2PA(*pte)); p->kstack = 0; if(p->pagetable) proc_freepagetable(p->pagetable, p->sz); if(p->kpagetable) proc_freekpagetable(p->kpagetable); ... } void proc_freekpagetable(pagetable_t kpagetable) { for (int i = 0; i < 512; i++) { pte_t pte = kpagetable[i]; if (pte & PTE_V) { if ((pte & (PTE_R|PTE_W|PTE_X)) == 0) { uint64 child = PTE2PA(pte); proc_freekpagetable((pagetable_t)child); } } } kfree((void*)kpagetable); }
這一個就是利用上一步的進程內核頁表,將進程的地址空間映射到內核頁表中,來簡化copy_in
操做,使得copy_in
不須要去查找進程的頁表來進行地址轉換。code
之因此能進行這個映射就是由於進程的地址空間是從 0 開始增加的,而內核須要的地址空間是從PLIC
開始增加的(CLINT
僅在內核初始化的時候使用,以後就不須要了)。所以,進程的地址空間是能夠從 0 增加到PLIC
的,而這裏就須要在growproc
中對進程的地址空間進行限制,避免其超出PLIC
。進程
if (PGROUNDUP(sz + n) >= PLIC) return -1;
在XV6中,會涉及到進程頁表改變的只有三個地方:fork
exec
sbrk
,所以要在對進程頁表改變後,將其同步到內核頁表中。
// copy page table void ukvmcopy(pagetable_t pagetable, pagetable_t kpagetable, uint64 oldsz, uint64 newsz) { pte_t *src, *dest; uint64 cur; if (newsz < oldsz) return; oldsz = PGROUNDUP(oldsz); for(cur = oldsz; cur < newsz; cur += PGSIZE){ if ((src = walk(pagetable, cur, 0)) == 0) panic("ukvmcopy: pte not exist"); if ((dest = walk(kpagetable, cur, 1)) == 0) panic("ukvmcopy: pte alloc failed"); uint64 pa = PTE2PA(*src); *dest = PA2PTE(pa) | (PTE_FLAGS(*src) & (~PTE_U)); } }
頁表的同步就經過上面的ukvmcopy
函數來實現,在上述三個函數對頁表進行改變後,就須要調用這個函數進行同步。
這裏有一個問題就是在newsz < oldsz
的時候,即釋放內存的時候,沒有對頁表項進行刪除,後面須要完善。