這一個實驗主要是學習XV6的頁表(分頁機制),關於分頁機制的相關內容已經寫在XV6學習 (3)里面了。
代碼放在Github上。
Print a page table (easy)
這一個就是要實現一個vmprint()
函數來遍歷頁表並打印,可以仿照freewalk()
函數來寫。
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是不是指向下一級頁表。
A kernel page table per process (hard)
這一題是要為每個進程分配一個獨立的內核頁表,而不是使用全局的內核頁表。這一題主要是為了下一題做准備。
因此,首先就是要建立一個函數來創建內核頁表。這個函數內部只要仿照kvminit
函數,給對應的頁面創建映射就行了。
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。
// switch the kernel pagetable.
w_satp(MAKE_SATP(p->kpagetable));
sfence_vma();
最后一步就是在freeproc
的時候對內核頁表和內核棧也進行釋放。
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);
}
Simplify copyin/copyinstr (hard)
這一個就是利用上一步的進程內核頁表,將進程的地址空間映射到內核頁表中,來簡化copy_in
操作,使得copy_in
不需要去查找進程的頁表來進行地址轉換。
之所以能進行這個映射就是因為進程的地址空間是從 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
的時候,即釋放內存的時候,沒有對頁表項進行刪除,后面需要完善。