这一个实验主要是学习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
的时候,即释放内存的时候,没有对页表项进行删除,后面须要完善。