一、Print a page table
编写一个打印页表内容的函数。
xv6采用三级页表,将三级页表展开可以得到一个页表树,因此可以通过深度优先遍历这个页表树来打印出整个页表结构。
// 递归函数处理页表树,level表示层数
void vmprintrun(pagetable_t pagetable, int level)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
// 只处理有效PTE
if(pte & PTE_V){
// 打印前置..
for(int j = 0; j <= level - 1; ++j)
printf(".. ");
printf("..");
printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
// 遍历下一级页表
if(level < 2){
uint64 child = PTE2PA(pte);
vmprintrun((pagetable_t)child, level + 1);
}
}
}
}
void vmprint(pagetable_t pagetable)
{
// 显示pagetable的参数
printf("page table %p\n", pagetable);
vmprintrun(pagetable, 0);
}
在exec.c中的return argc之前插入if(p->pid==1) vmprint(p->pagetable),以打印第一个进程的页表。
现在编译运行xv6会打印如下输出来描述第一个进程的页表:
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
二、A kernel page table per process
xv6为每个进程维护一个页表,用来描述进程的用户地址空间,同时整个xv6操作系统维护一个内核页表。当CPU进入内核态时,使用内核页表完成虚拟地址到物理地址的映射。如果内核需要使用在系统调用中传递的用户指针(例如,传递给write()
的缓冲区指针)时,内核必须首先将指针指向的虚拟地址转换为物理地址。
本节和下一节的任务就是解决上述问题,使得内核可以直接解引用用户指针。
本节任务是修改内核,给每一个进程生成一个自己的内核页表,让每一个进程在内核中执行时使用它自己的内核页表的副本,每个进程的内核页表都应当与现有的的全局内核页表完全一致。(本节中的内核页表内容是暂时的,到下一节还会添加用户空间的映射到内核页表中,使内核可以解引用用户指针)
1、在struct proc(kernel/proc.h)中新增内核页表字段。
// Per-process state
struct proc {
// ..
pagetable_t kernel_pagetable;// proc's kernel pagetable
};
2、仿造kvminit函数(kernel/vm.c)初始化内核页表
调用mappages函数(kernel/vm.c)添加跟全局内核页表一样的va->pa映射。
pagetable_t
proc_kvminit(struct proc *p)
{
// 分配一页内存来存储进程p的内核页表
pagetable_t p_kernel_pagetable = (pagetable_t) kalloc();
if(p_kernel_pagetable == 0)
return 0;
memset(p_kernel_pagetable, 0, PGSIZE);
// uart registers
if(mappages(p_kernel_pagetable, UART0, PGSIZE, UART0, PTE_R | PTE_W) != 0)
panic("proc_kvminit");
// virtio mmio disk interface
if(mappages(p_kernel_pagetable, VIRTIO0, PGSIZE, VIRTIO0, PTE_R | PTE_W) != 0)
panic("proc_kvminit");
// CLINT
if(mappages(p_kernel_pagetable, CLINT, 0x10000, CLINT, PTE_R | PTE_W) != 0)
panic("proc_kvminit");
// PLIC
if(mappages(p_kernel_pagetable, PLIC, 0x400000, PLIC, PTE_R | PTE_W) != 0)
panic("proc_kvminit");
// map kernel text executable and read-only.
if(mappages(p_kernel_pagetable, KERNBASE, (uint64)etext-KERNBASE, KERNBASE, PTE_R | PTE_X) != 0)
panic("proc_kvminit");
// map kernel data and the physical RAM we'll make use of.
if(mappages(p_kernel_pagetable, (uint64)etext, PHYSTOP-(uint64)etext, (uint64)etext, PTE_R | PTE_W) != 0)
panic("proc_kvminit");
// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
if(mappages(p_kernel_pagetable, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) != 0)
panic("proc_kvminit");
return p_kernel_pagetable;
}
3、使每一个进程的内核页表中都一个该进程的内核栈映射。
将procinit函数中映射内核栈的代码迁移到allocproc函数(kernel/proc.h)中
在allocproc函数中生成进程的内核页表,然后添加关于该进程对应的内核栈映射。
found:
p->pid = allocpid();
// 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;
}
p->kernel_pagetable = proc_kvminit(p);
if(p->kernel_pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// 设置该进程对应的内核栈,此处代码从procinit中迁移过来
// 将procinit中这些代码注释
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
mappages(p->kernel_pagetable, va, PGSIZE, (uint64)pa, 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;
4、修改scheduler函数(kernel/proc.c)加载进程的内核页表到核心的satp
寄存器。
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();
int found = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == RUNNABLE) {
// Switch to chosen process. It is the process's job
// to release its lock and then reacquire it
// before jumping back to us.
p->state = RUNNING;
c->proc = p;
// 切换进程的同时切换到该进程的内核页表
w_satp(MAKE_SATP(p->kernel_pagetable));
sfence_vma();
swtch(&c->context, &p->context);
// 进程运行完后切换到全局kernel_pagetable
kvminithart();
// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
found = 1;
}
release(&p->lock);
}
#if !defined (LAB_FS)
if(found == 0) {
// 没有进程运行时使用全局kernel_pagetable
kvminithart();
intr_on();
asm volatile("wfi");
}
kvmpa函数(kernel/vm.c)完成内核虚拟地址到物理地址的转换,因此需要更改其中用到的内核页表。
// vm.c中加上2行头文件
#include "spinlock.h"
#include "proc.h"
uint64
kvmpa(uint64 va)
{
uint64 off = va % PGSIZE;
pte_t *pte;
uint64 pa;
// 内核页表设置为当前进程的内核页表
pte = walk(myproc()->kernel_pagetable, va, 0);
if(pte == 0)
panic("kvmpa");
if((*pte & PTE_V) == 0)
panic("kvmpa");
pa = PTE2PA(*pte);
return pa+off;
}
5、释放进程的内核页表。
freeproc函数(kernel/proc.c)中添加如下代码:
if(p->kernel_pagetable)
proc_free_kernel_pagetable(p->kernel_pagetable, p->kstack);
p -> kernel_pagetable = 0;
proc_free_kernel_pagetable函数(kernel/proc.c)调用uvmunmap函数(kernel/vm.c)解除映射关系,调用uvmfree函数(kernel/vm.c)释放内核页表所占物理内存。
void
proc_free_kernel_pagetable(pagetable_t pagetable, uint64 kstack)
{
uvmunmap(pagetable, UART0, 1, 0);
uvmunmap(pagetable, VIRTIO0, 1, 0);
uvmunmap(pagetable, CLINT, 0x10000 / PGSIZE, 0);
uvmunmap(pagetable, PLIC, 0x400000 / PGSIZE, 0);
uvmunmap(pagetable, KERNBASE, ((uint64)etext-KERNBASE) / PGSIZE, 0);
uvmunmap(pagetable, (uint64)etext, (PHYSTOP-(uint64)etext) / PGSIZE, 0);
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
// 该进程私有的内核栈释放物理内存
uvmunmap(pagetable, kstack, 1, 1);
// 释放页表所占物理内存
uvmfree(pagetable, 0);
}
三、Simplify copyin/copyinstr
本节实验内容是继续上一节的工作,将用户空间的映射添加到每个进程的内核页表中,使得内核可以完成对用户虚拟地址的映射,允许copyin/copyinstr函数可以直接对用户指针解引用。
1、将copyin/copyinstr函数主体替换成对copyin_new和copyinstr_new的调用
copyin_new和copyinstr_new函数已经给出,位于kernel/vmcopyin.c中,将2个函数的声明添加到kernel/defs.h中,以便后面调用。
//vmcopyin.c
int copyin_new(pagetable_t, char *, uint64, uint64);
int copyinstr_new(pagetable_t, char *, uint64, uint64);
2、实现一个函数,将进程的用户页表项复制到内核页表中
调用walk函数(kernel/vm.c)找到所有用户页表项,再次调用walk函数在内核页表上分配相同的pte。(注意需要去除PTE_U标志,因为在内核模式下,无法访问设置了PTE_U的页面)
// 将每个进程的用户地址映射添加到内核页表中
void
uptbl_to_kptbl(pagetable_t kernel_pagetable, pagetable_t user_pagetable, uint64 srcva, uint64 sz)
{
srcva = PGROUNDUP(srcva);
for(uint64 i = srcva; i < srcva + sz; i += PGSIZE)
{
pte_t* user_pte = walk(user_pagetable, i, 0); // 找到i对应的pte
pte_t* kernel_pte = walk(kernel_pagetable, i, 1); // 为i分配pte
if(!user_pte || !kernel_pte)
panic("uptbl_to_kptbl");
*kernel_pte = (*user_pte) & (~PTE_U); // 去除PTE_U标志
}
}
userinit函数(kernel/proc.c)中调用uptbl_to_kptbl函数,为第一个进程的内核页表添加用户空间的映射。
p->sz = PGSIZE;
uptbl_to_kptbl(p->kernel_pagetable, p->pagetable, 0, p->sz);
// prepare for the very first "return" from kernel to user.
p->trapframe->epc = 0; // user program counter
p->trapframe->sp = PGSIZE; // user stack pointer
3、修改内核页表初始化和释放函数
在xv6中用户进程的最大大小限制为小于内核的最低虚拟地址。内核启动后,在XV6中该地址是0x0C000000,即PLIC寄存器的地址。由于CLINT的地址低于0x0C000000,因此初始化内核页表时不能添加CLINT的映射。将proc_kvminit函数(kernel/vm.c)这部分代码注释。
// virtio mmio disk interface
if(mappages(p_kernel_pagetable, VIRTIO0, PGSIZE, VIRTIO0, PTE_R | PTE_W) != 0)
panic("proc_kvminit");
// 用户进程的最大大小限制为小于内核的最低虚拟地址。内核启动后,在XV6中该地址是0xC000000,即PLIC寄存器的地址
// CLINT
// if(mappages(p_kernel_pagetable, CLINT, 0x10000, CLINT, PTE_R | PTE_W) != 0)
// panic("proc_kvminit");
// PLIC
if(mappages(p_kernel_pagetable, PLIC, 0x400000, PLIC, PTE_R | PTE_W) != 0)
panic("proc_kvminit");
同时proc_free_kernel_pagetable函数(kernel/proc.c)中也要做相应修改,由于内核页表中加入了用户空间的映射,因此释放内核页表时也要解除这一部分映射。
void
proc_free_kernel_pagetable(pagetable_t pagetable, uint64 kstack, uint64 sz)
{
uvmunmap(pagetable, UART0, 1, 0);
uvmunmap(pagetable, VIRTIO0, 1, 0);
// uvmunmap(pagetable, CLINT, 0x10000 / PGSIZE, 0);
uvmunmap(pagetable, PLIC, 0x400000 / PGSIZE, 0);
uvmunmap(pagetable, KERNBASE, ((uint64)etext-KERNBASE) / PGSIZE, 0);
uvmunmap(pagetable, (uint64)etext, (PHYSTOP-(uint64)etext) / PGSIZE, 0);
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
// 该进程私有的内核栈释放物理内存
uvmunmap(pagetable, kstack, 1, 1);
// 去除内核页表上所有关于用户空间的映射
uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 0);
// 释放页表所占物理内存
uvmfree(pagetable, 0);
}
4、修改fork()、exec()和sbrk()
fork函数(kernel/proc.c):
np->sz = p->sz;
// 调用uptbl_to_kptbl函数,复制用户页表pte
uptbl_to_kptbl(np->kernel_pagetable, np->pagetable, 0, np->sz);
np->parent = p;
exec函数(kernel/exec.c):
if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
goto bad;
// 释放旧的内核页表pte
uvmunmap(p->kernel_pagetable,0,PGROUNDUP(oldsz)/PGSIZE,0);
uptbl_to_kptbl(p->kernel_pagetable, pagetable, 0, sz);
// arguments to user main(argc, argv)
// argc is returned via the system call return
// value, which goes in a0.
p->trapframe->a1 = sp;
sbrk函数(kernel/sysproc.c):
由于sbrk设计到用户虚拟地址空间的扩大和缩小,因此就存在pte的新增和释放。空间扩大时,应该添加对应的pte;空间缩小时,应该释放对应的pte。同时空间扩大时注意用户进程不能超过PLIC这一限制。
uint64
sys_sbrk(void)
{
int addr;
int n;
struct proc* p = myproc();
if(argint(0, &n) < 0)
return -1;
// 用户进程虚拟地址不能超过PLIC
if(PGROUNDUP(p->sz + n) >= PLIC)
return -1;
addr = p->sz;
if(growproc(n) < 0)
return -1;
// n大于0时需要添加映射
if(n > 0){
uptbl_to_kptbl(p->kernel_pagetable, p->pagetable, addr, n);
}
else{
// 释放对应pte
// [addr + n,addr)
for(uint64 i = PGROUNDUP(addr + n); i < PGROUNDUP(addr); i += PGSIZE){
uvmunmap(p->kernel_pagetable, i, 1, 0);
}
}
return addr;
}
代码写完后,运行make clean,然后make grade。(注意answers-pgtbl.txt需要自己添加,不然通不过,虽然我也不知道answers-pgtbl.txt内容应该是什么。。)