该实验只需要在kernel文件夹下进行修改,主要是proc.c,vm.c和defs.h这几个文件,需要先阅读参考资料,并配合其中讲解代码的几节理解相关代码。
总体来说,跟着实验的提示可以按部就班的完成,只是这个实验调试起来有点麻烦。
Print a page table
这个实验很简单,根据提示仿照vm.c里的freewalk()函数来编写即可。但需要注意,在第三级页表不需要判断(pte2 & (PTE_R|PTE_W|PTE_X)) == 0。
需要注意的是PTE2PA()返回值一定是uint64,不可以用pagetable_t。
代码
void vmprint(pagetable_t p)
{
printf("page table %p\n", p);
for(int i = 0; i < 512; i++){
pte_t pte = p[i];
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
uint64 pa = PTE2PA(pte);
printf("..%d: pte %p pa %p\n", i, pte, pa);
pagetable_t p1 = (pagetable_t)pa;
for(int j = 0; j < 512; j++){
pte_t pte1 = p1[j];
if((pte1 & PTE_V) && (pte1 & (PTE_R|PTE_W|PTE_X)) == 0){
uint64 pa1 = PTE2PA(pte1);
printf(".. ..%d: pte %p pa %p\n", j, pte1, pa1);
pagetable_t p2 = (pagetable_t)pa1;
for(int k = 0; k < 512; k++){
pte_t pte2 = p2[k];
if(pte2 & PTE_V){
uint64 pa2 = PTE2PA(pte2);
printf(".. .. ..%d: pte %p pa %p\n", k, pte2, pa2);
}
}
}
}
}
}
}
运行结果
A kernel page table per process
实验要求每个进程需要维护一个内核页表,所以先根据提示先在proc.h的struct proc增加一个变量pagetable_t。
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
pagetable_t kernel_pagetable;// Kernel page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};
然后,我们需要实现每个进程的内核页表的初始化,进程状态为RUNNABLE时切换为该进程的内核页表,以及进程结束后相应的清除。要实现这些要求,需要修改vm.c和proc.c。
修改vm.c
首先,根据提示,我们要实现一个类似kvminit()的函数,为每个进程创建一个内核页表。不同于kvminit(),因为需要给myproc()->kernel_pagetable赋值,所以这个函数需要返回pagetable_t。
注意,需要在defs.h里添加prockvminit()的声明,以便于调用。
pagetable_t
prockvminit()
{
pagetable_t p = (pagetable_t) kalloc();
if(p == 0) return 0;
memset(p, 0, PGSIZE);
// uart registers
prockvmmap(p, UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
prockvmmap(p, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// CLINT
prockvmmap(p, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
prockvmmap(p, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
prockvmmap(p, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
prockvmmap(p, (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.
prockvmmap(p, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return p;
}
kvminit()里需要调用kvmmap(),而kvmmap()默认使用kernel_pagetable,所以我们还需要实现一个prockvmmap()函数,把进程的kernel_pagetable传进去后,再做相关映射。
void
prockvmmap(pagetable_t p, uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(p, va, sz, pa, perm) != 0)
panic("prockvmmap");
}
此外,当切换进程时,需要将进程的kernel_pagetable放入satp寄存器,应当仿照kvminithart()编写prockvminithart()。
void
prockvminithart(pagetable_t pagetable)
{
w_satp(MAKE_SATP(pagetable));
sfence_vma();
}
修改proc.c
首先,需要调用prockvminit()为进程创建内核页表,根据提示应修改allocproc(),在创建好进程的用户页表后调用。
内核页表需要有进程内核栈的映射,将procinit()里的相关代码复制过来修改,接在内核页表创建之后即可。
需要注意的是,procinit()里kSTACK的物理地址是刚分配的,这里不需要进行分配,直接调用kvmpa()函数计算。
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// An proc kernel page table.
p->kernel_pagetable = prockvminit();
if(p->kernel_pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// Map kstack to kernel page table
uint64 va = KSTACK((int) (p - proc));
uint64 pa = kvmpa(va);
prockvmmap(p->kernel_pagetable, va, pa, PGSIZE, PTE_R | PTE_W);
接下来,需要在scheduler()里把RUNNABLE的进程的内核页表放入satp寄存器,这里调用之前写好的prockvminithart()。
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
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;
prockvminithart(p->kernel_pagetable);
swtch(&c->context, &p->context);
// Process is done running for now.
// It should have changed its p->state before coming back.
kvminithart();
c->proc = 0;
found = 1;
}
release(&p->lock);
}
#if !defined (LAB_FS)
if(found == 0) {
intr_on();
asm volatile("wfi");
}
#else
;
#endif
}
}
最后需要修改freeproc(),因为不要求清除物理地址的页,所以参照vm.c下的freewalk()去掉panic,使程序可以正常运行。
// free a proc kernel page table
// no memory page
void
free_proc_kernel_pagetable(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);
free_proc_kernel_pagetable((pagetable_t)child);
pagetable[i] = 0;
}
}
kfree((void*)pagetable);
}
Simplify copyin/copyinstr
根据题目要求,我们需要把vm.c里的copyin()和copyinstr()函数改为调用vmprint.c里的copyin_new()和copyinstr_new(),然后再修改fork,exec,sbrk几个功能让程序能正常运行。
copyin()和copyinstr()直接改成调用copyin_new()和copyinstr_new()就行,但要注意defs.h里没有声明copyin_new()和copyinstr_new(),要自行添加。
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
return copyin_new(pagetable, dst, srcva, len);
}
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
return copyinstr_new(pagetable, dst, srcva, max);
}
而fork,exec,sbrk修改的地方则是当进程的用户页表有修改时,进程的内核页表映射需要相对应的修改。因此,要先实现用户页表->内核页表的复制。
用户页表->内核页表
vm.c中的uvmcopy()为子进程创建了新的物理页,再将父进程用户页表映射的物理页复制过去,最后建立新的物理页与子进程用户页表之间的映射。
参考uvmcopy(),进程用户页表->内核页表不需要创建新的物理页,只需要获取每一个页表项的flags,然后建立物理页与内核页表的映射。
需要注意的是,uvmcopy()是对整个页的复制,所以只需要一个参数传入页表中有效页表项的大小,而考虑到实现sbrk功能时,只需要增加新物理页的映射,所以做了相应的修改。
根据提示,内核页表的flags中PTE_U不可为真,否则会无法在内核模式中读取,需要相应处理。
此外,在建立物理页和页表项映射时,使用mappages()会弹出panic:"remap",所以使用在mappages()基础上,删去了panic条件的kmappages()函数。
int
kvmcopy(pagetable_t old, pagetable_t new, uint64 oldsz, uint64 newsz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
oldsz = PGROUNDUP(oldsz);
if(newsz <= oldsz) return 0;
for(i = oldsz; i < newsz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
panic("kvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
panic("kvmcopy: page not present");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
flags &= (~PTE_U);
if(kmappages(new, i, PGSIZE, pa, flags) != 0){
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 0);
return -1;
}
int
kmappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + size - 1);
for(;;){
if((pte = walk(pagetable, a, 1)) == 0)
return -1;
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
fork
fork里的功能很好实现,在子进程复制了父进程的物理页,建立了自己的用户页表后,调用kvmcopy()即可,详细代码可参考fork()里对uvmcopy()的调用。
int
fork(void)
{
...
// 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;
np->parent = p;
// Copy child map from user to kernel
if(kvmcopy(np->pagetable, np->kernel_pagetable, 0, np->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
...
}
exec
同样,exec()里找到p->pagetable被修改的地方,在后面调用kvmcopy()即可。
int
exec(char *path, char **argv)
{
...
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
// Copy to the proc kernel pagetable
if(kvmcopy(pagetable, p->kernel_pagetable, 0, sz) == -1)
goto bad;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);
if(p->pid == 1) vmprint(p->pagetable);
return argc; // this ends up in a0, the first argument to main(argc, argv)
bad:
if(pagetable)
proc_freepagetable(pagetable, sz);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}
sbrk
sbrk稍微复制一点,在sysproc.c中可以知道sys_sbrk()调用的是growproc()函数,在defs.h下查找到其是在proc.c下。
因为sbrk的功能是动态分配内存,不够时增加,空闲了则减少,所以需要分情况讨论。
当n>0,也就是增加内存时,所以需要防止其超过PLIC限制,再调用kvmcopy()建立新分配内存与内核页表间的映射。
当n<0,减少内存时,我本来是考虑直接调用uvmdealloc(),这样会比较方便,但是这样运行usertests会出错。
经过对uvmdealloc()的仔细研究,我才明白,这个函数调用uvmunmap()时,do_free参数一直是1,也就是会删除物理页,而这步是用户页表已经做过的事情,不要内核页表这里再做一次,所以会出错。
最后,我只能仿照uvmdealloc()里的写法,直接调用uvmunmap(),完成了实验。
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
if(sz + n >= PLIC || (sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
if(kvmcopy(p->pagetable, p->kernel_pagetable, p->sz, sz) == -1)
return -1;
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
uvmunmap(p->kernel_pagetable, PGROUNDUP(sz), (PGROUNDUP(p->sz) - PGROUNDUP(sz)) / PGSIZE,0);
}
p->sz = sz;
return 0;
}
userinit()
最后,根据提示,userinit()刚创建的用户页表,也需要同步内核页表。
// Set up first user process.
void
userinit(void)
{
struct proc *p;
p = allocproc();
initproc = p;
// allocate one user page and copy init's instructions
// and data into it.
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;
// user page to kernel page
kvmcopy(p->pagetable, p->kernel_pagetable, 0, PGSIZE);
// prepare for the very first "return" from kernel to user.
p->trapframe->epc = 0; // user program counter
p->trapframe->sp = PGSIZE; // user stack pointer
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
p->state = RUNNABLE;
release(&p->lock);
}