Pgtbl
- Print a page table
- A kernel page table per process
- Simplify copyin/copyinstr
本Lab简单优化了系统的页表功能,使得程序在内核态时可以直接解析用户态的指针。
笔者用时约8h
Print a page table
第一部分是为系统添加一个打印给定页表的函数vmprint
,该函数接收一个参数pagetable
(根页表的物理地址),递归遍历整张页表,打印有效的表项。
参考freewalk
函数(定义在kernel/vm.c:331),每次遍历512个表项,若表项有效,则打印相关信息(第几级、第几项、pte内容和pte内容对应的物理地址),且若为一二级页表则继续递归,直到第三级页表返回。参考代码如下:
void
vmprint_helper(pagetable_t pgtbl, int level)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pgtbl[i];
if(pte & PTE_V){
for (int j = 0; j < level; j ++ ) {
if (j) printf(" ");
printf("..");
}
printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
if ((pte & (PTE_R|PTE_W|PTE_X)) == 0) {
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
vmprint_helper((pagetable_t)child, level + 1);
}
}
}
}
void
vmprint(pagetable_t pgtbl)
{
printf("page table %p\n", pgtbl);
vmprint_helper(pgtbl, 1);
}
A kernel page table per process
如标题,第二部分的内容是为每一个进程添加一个单独的内核页表副本,为下一节直接解引用用户态指针做铺垫。
首先需要在进程的结构体(定义在kernel/proc.h中)struct proc
中添加一个字段维护内核页表副本,如下图所示
然后,由于我们需要在分配进程时需要为每一个进程初始化一个内核页表的副本,于是需要参考kvminit
函数(定义在kernel/vm.c:66),编写一个初始化进程中内核页表副本的函数proc_kvminit
,代码如下所示。该函数内容与kvminit
函数基本一致。其中的uvmmap
与kvmmap
函数(定义在kernel/vm.c:171)类似,映射给定的虚拟地址和物理地址范围,唯一不同点是前者修改的是传入的指定页表而不仅仅是全局的内核页表。
void
uvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(pagetable, va, sz, pa, perm) != 0)
panic("uvmmap");
}
/*
* create a direct-map page table for the given process.
*/
pagetable_t
proc_kvminit()
{
pagetable_t pgtbl = (pagetable_t) kalloc();
if (pgtbl == 0) return 0;
memset(pgtbl, 0, PGSIZE);
// uart registers
uvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
uvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// CLINT
uvmmap(pgtbl, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
uvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
uvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
uvmmap(pgtbl, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
uvmmap(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return pgtbl;
}
接着在allocproc
函数(定义在kernel/proc.c:95)中调用上述定义的proc_kvminit
函数,实现在进程分配时初始化进程中的内核页表副本。同时,还需要将procinit中对进程内核栈对应的页表项初始化代码段移动到allocproc
函数中,如下所示。这里需要注意的是,原始代码中对进程context
字段的修改一定要放在最下面。(暂时不知道为啥,等知道了再补一下原因)
...
// initialize the process kernel page table
p->kernel_pagetable = proc_kvminit();
if(p->kernel_pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// initialize the process kernel stack in kernel process kernel page table
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
uvmmap(p->kernel_pagetable, 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
函数(定义在kernel/proc.c:489)中,当调度进程执行时,将进程对应的内核页表加载到satp寄存器中,且调用sfence_vma
进行刷新。在进程执行完,调用将页表切换回全局的内核页表,代码段如下所示。
// load process's kernel page table and flush the TLB
w_satp(MAKE_SATP(p->kernel_pagetable));
sfence_vma();
swtch(&c->context, &p->context);
// load kernel page table when process done
kvminithart();
最后,还需要在freeproc
函数(定义在kernel/proc.c:155)中释放进程所维护的内核页表副本。需要将进程中内核页表维护的内核栈物理空间释放掉,调用uvmunmap
函数(定义在kernel/vm.c:230)即可。同时还需要将维护的内核页表副本销毁掉,由于freewalk
函数只销毁第一级和第二级页表表项,需要自己写一个类似的函数来销毁第三级页表的表项,如下所示。
// Recursively free process's kernel page-table pages.
void
proc_freewalk(pagetable_t pagetable)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
proc_freewalk((pagetable_t)child);
pagetable[i] = 0;
} else if(pte & PTE_V){
pagetable[i] = 0;
}
}
kfree((void*)pagetable);
}
在freeproc
函数中的相关代码段如下
// free process's kernel page table
uvmunmap(p->kernel_pagetable, p->kstack, 1, 1);
p->kstack = 0;
proc_freewalk(p->kernel_pagetable);
p->kernel_pagetable = 0;
Simplify copyin/copyinstr
这一部分需要实现的是将每一个进程的用户空间映射添加到进程维护的内核页表副本中(上一节创建的),由于用户空间的虚拟地址从0开始,且内核的虚拟地址从较高的地址开始(文档里说是PLIC,但是xv6book里面的图3.3是从CLINT开始的,暂时不知道为啥),所以给用户空间的映射留下了一些虚拟空间进行映射(0~PLIC-1)。
我们需要在fork
函数、exec
函数、growproc
函数与userinit
函数中,为进程维护的内核页表添加上用户空间的映射,因为这些函数都更改了用户映射。
首先,我仿照uvmcopy
函数(定义在kernel/vm.c:384),定义了一个函数uvm2ukvm
,它接收两个页表,一个是用户进程页表,一个是用户进程中维护的内核页表,并接收需要映射的起始虚拟地址和末尾虚拟地址,将这个范围内的用户空间虚拟地址复制到进程维护的内核页表中。注意需要将PTE_U标志位置为0,否则内核无法访问。
void uvm2ukvm(pagetable_t upgtbl, pagetable_t ukpgtbl, uint64 st, uint64 ed)
{
pte_t *pte_u, *pte_uk;
uint64 pa, i;
uint flags;
for (i = st; i < ed; i += PGSIZE) {
if((pte_u = walk(upgtbl, i, 0)) == 0)
panic("uvm2ukvm: pte_u should exist");
if((*pte_u & PTE_V) == 0)
panic("uvm2ukvm: page not present");
pa = PTE2PA(*pte_u);
flags = PTE_FLAGS(*pte_u);
flags &= (~PTE_U);
if((pte_uk = walk(ukpgtbl, i, 1)) == 0)
panic("uvm2ukvm: pte_uk should exist");
*pte_uk = PA2PTE(pa) | flags;
}
}
在fork
函数中(定义在kernel/proc.c:289),调用以上函数,添加一行代码即可。
...
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
uvm2ukvm(np->pagetable, np->kernel_pagetable, 0, np->sz);
...
在exec
函数中(定义在kernel/exec.c:13),也是一样的添加上一行代码即可。
...
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
uvm2ukvm(p->pagetable, p->kernel_pagetable, 0, sz);
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);
...
在growproc
函数中,当申请增长内存时,需要判断增长后的虚拟地址上界是否超过PLIC的起始地址,如果超过则返回-1,否则也是调用上述函数将增长的地址范围复制一份到进程维护的内核页表中即可。
...
if (PGROUNDUP(sz + n) > PLIC) {
return -1;
}
if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
uvm2ukvm(p->pagetable, p->kernel_pagetable, sz - n, sz);
...
在userinit
函数中第一次初始化进程页表时,也要进行复制。
...
// allocate one user page and copy init's instructions
// and data into it.
uvminit(p->pagetable, initcode, sizeof(initcode));
uvm2ukvm(p->pagetable, p->kernel_pagetable, 0, PGSIZE);
...
最后,把copyin
函数和copyinstr
函数体中的内容改成调用copyin_new
和copyinstr_new
函数即可。