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