本次实验是有关内存页懒分配的。所谓内存页懒分配,在本实验中,指的是在用户进程使用 sbrk()
系统调用来增加内存中堆的空间时,我们不直接在物理内存中分配相应的页,而是只是记录了分配到了哪些用户地址,在用户页面表中这些地址默认标记为无效。当进程首次尝试使用任何给定页面的懒惰分配内存时,CPU会生成一个 Page fault,内核通过分配物理内存、将其归零并将其映射来处理。
这次实验的难度不大,主要是写了个很无语的错误导致调了很久。
Eliminate allocation from sbrk() (easy)
Your first task is to delete page allocation from the sbrk(n) system call implementation, which is the function sys_sbrk() in sysproc.c. The sbrk(n) system call grows the process's memory size by n bytes, and then returns the start of the newly allocated region (i.e., the old size). Your new sbrk(n) should just increment the process's size (myproc()->sz) by n and return the old size. It should not allocate memory -- so you should delete the call to growproc() (but you still need to increase the process's size!).
这一步的要求只是将原本 sys_sbrk()
中调用 growproc()
的部分删去,使得增加堆空间的时候,只是简单地增加 myproc()->sz
的值,不进行页面分配。
这里我比任务要求多做了一步(这实际上会是下面的任务),判断了一下 n
的正负,如果为负则正常调用 growproc()
进行页面的删除,否则就只是记录 myproc()->sz
的值。
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
struct proc *p = myproc();
addr = p->sz;
if (n < 0) {
if(growproc(n) < 0)
return -1;
} else p->sz += n;
return addr;
}
正如期望的那样,这里出现了一个 SCAUSE
为 15
的写缺页异常。
Lazy allocation (moderate) & Lazytests and Usertests (moderate)
Modify the code in trap.c to respond to a page fault from user space by mapping a newly-allocated page of physical memory at the faulting address, and then returning back to user space to let the process continue executing. You should add your code just before the
printf
call that produced the "usertrap(): ..." message. Modify whatever other xv6 kernel code you need to in order to getecho hi
to work.
实现缺页处理
一个正常的缺页异常,可以通过 SCAUSE
寄存器的值来判断。SCAUSE
寄存器的值是 12, 13, 15
时就是缺页异常。在本实验中,我们只需要考虑 13
和 15
两种,分别是由读取和写入带来的缺页(12
是由指令执行带来的缺页,显然不应该出现在 sbrk
增加的堆空间里)
引发缺页的地址会被存储在 STVAL
寄存器中。
在实现缺页处理的时候,我们要首先判断缺页地址是不是由我们的 sbrk
扩展的堆空间中的。除了这个地址之外的所有地址,都不应该补充分配物理内存,而是直接 kill
掉。判断的方法由很多,我这里采用的是,这个地址应该小于 p->sz
,而不应该小于用户态 SP
寄存器的值(也就是 p->trapframe->sp
,这个条件主要是防止读写栈页前面的 guard page
)。
然后,我们可以申请一个物理页面,如果不能申请到,那么就是 OOM(Out Of Memory)
了,没有可用的物理地址,那么就把进程 kill
。
如果成功了,我们就将物理页面清零,然后将虚拟地址所在页面 map
上这个物理页面。在 map
的过程中也有可能会 OOM
(没有空间存放页表项了),那么我们也把这个进程 kill
。
因为在后面的任务中可能会重用这个过程的代码,我将缺页处理放在了一个函数中,方便直接调用。
int pfdeal(uint64 va) {
struct proc *p = myproc();
if (va >= p->sz || va < p->trapframe->sp)
return -1;
else {
va = PGROUNDDOWN(va);
uint64 pa = (uint64)kalloc();
if (pa == 0) {
return -1;
} else {
memset((void *)pa, 0, PGSIZE);
if (mappages(p->pagetable, va, PGSIZE, pa, PTE_W|PTE_R|PTE_U) != 0) {
kfree((void *)pa);
return -1;
}
}
}
return 0;
}
void
usertrap(void)
{
// ......
} else if((which_dev = devintr()) != 0){
// ok
} else if (r_scause() == 13 || r_scause() == 15) {
uint64 va = r_stval();
if (pfdeal(va) != 0)
p->killed = 1;
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
// ......
}
修改 uvmunmap
如果我们做完以上的修改,然后直接运行,那么我们可以发现,会出现 panic: uvmunmap: not mapped
的错误。
这是因为 xv6 原始的 uvmunmap
函数中,默认参数提供的虚拟地址范围都是已经分配好了的。这是理所当然的,只是在我们 lazy allocation 的版本中,有效的虚拟地址范围不一定都对应着物理页面。
所以我们可以将循环中前两个 if
判断原本的报错,全都修改为 continue
。这两个判断,一个是判断页表项是否存在,一个是判断是否有效。(显然,在一个虚拟地址没有分配物理页面的时候,其页表项所在的页目录可能存在,也可能不存在,这就分别对应着标志位无效和页表项不存在两种情况)
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
if((pte = walk(pagetable, a, 0)) == 0)
continue;
// panic("uvmunmap: walk");
if((*pte & PTE_V) == 0)
continue;
// panic("uvmunmap: not mapped");
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
修改 uvmcopy
这里和 uvmunmap
需要修改的原本是相似的,都是需要遍历一段虚拟页(只不过这里是将虚拟页对应的物理页拷贝到新页表中),修改内容也比较相似。
这个修改影响的往往是调用 fork
的时候,在父进程和子进程之间复制内存内容的时候。
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
char *mem;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
continue;
// panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
continue;
// panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
if((mem = kalloc()) == 0)
goto err;
memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
kfree(mem);
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
修改 copyin/copyout/copyinstr
修改到这里,应该 echo hi
这个命令已经可以正常执行了(其实应该只修改到 uvmunmap
就应该已经可以了)。
但是如果进行 usertests
,会发现还有很多点无法通过。
这是因为除了由用户进程读写一个地址引起缺页意外,在系统调用(比如 read
和 write
)中,用户也有可能传递一个用户空间地址过来,由内核对用户空间的这个地址进行读写。不会触发用户态的缺页异常,因此不能通过 usertrap
来处理。
对用户地址的读写往往是由 copyin/copyout/copyinstr
这几个函数完成的,因此我们只需要在这三个函数内部,判断读写的页是否是通过 sbrk
分配且还没有映射物理内存,然后进行物理页面的分配即可。这个过程已经被我们封装在 pfdeal
函数中了。
(为了能在 vm.c
中调用这个函数,记得修改 defs.h
)
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0) {
if (pfdeal(va0) != 0)
return -1;
pa0 = walkaddr(pagetable, va0);
}
// ...
}
return 0;
}
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(srcva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0) {
if (pfdeal(va0) != 0)
return -1;
pa0 = walkaddr(pagetable, va0);
}
// ...
}
return 0;
}
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
uint64 n, va0, pa0;
int got_null = 0;
while(got_null == 0 && max > 0){
va0 = PGROUNDDOWN(srcva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0) {
if (pfdeal(va0) != 0)
return -1;
pa0 = walkaddr(pagetable, va0);
}
// ...
}
// ...
}