xv6中的用户程序使用sbrk
系统调用向内核请求增长堆内存或者缩减堆内存。对于增长内存的情况,用户往往会申请多于实际需要的内存以便使用。于是,对于暂未使用的内存,如果在用户申请时就将其分配给用户,挺浪费的。Lazy page allocation基于这样一种思想,当用户申请增长内存时,暂时不分配物理内存,而是仅增长用户进程的空间大小。这时如果用户实际使用到了新分配的内存,则会引发page fault(因为进程页表暂未维护该内存的虚拟地址与物理地址的映射)。page fault其实是trap的一种,在原始xv6代码中并为做特殊处理,而是仅仅杀死引发该page fault的进程。而要实现lazy page allocation,我们需要对page fault进行处理,其实就是对找不到映射的虚拟地址分配物理地址并在页表中添加映射。具体实现就是这个实验做的事情。
这一部分就是删除sbrk
对应的系统调用函数sys_sbrk
(定义在kernel/sysproc.c:42)中的页面分配代码,因为初始代码是一申请就分配的,现在我们想要延迟分配,只需要在sbrk
被调用时,如果参数大于0,则增加进程的内存大小即可;如果参数小于0,参考growproc
的处理方式即可,如下所示。
uint64
sys_sbrk(void)
{
int addr;
int n;
struct proc* p = myproc();
if(argint(0, &n) < 0)
return -1;
addr = p->sz;
if (n > 0) {
p->sz += n;
} else if (n < 0) {
p->sz = uvmdealloc(p->pagetable, p->sz, p->sz + n);
}
// if(growproc(n) < 0)
// return -1;
return addr;
}
这一部分需要我们处理page fault,为暂未分配物理内存但是在合理范围内的虚拟地址分配内存并在进程页表中维护映射。
这里的合理范围指的是,地址大小小于当前进程的内存大小,大于当前进程的栈底地址。
具体实现上,需要在usertrap
函数(定义在kernel/trap.c:37)中添加处理page fault的代码。
首先,当page fault
产生时(其实是一种trap类型),scause寄存器的值会被设置为13或者15,于是可以通过读取scause寄存器的值来判断当前的trap是否是page fault。
然后,需要读取stval寄存器以获取造成page fault的虚拟地址,并判断该虚拟地址是否在合理的地址范围内。
如果不再合理范围内则把该进程杀死,否则开始为该虚拟地址分配一页物理内存空间,如果分配失败也是杀死进程,否则初始化该物理页为空,并调用mappages
函数进行地址映射。
else if (r_scause() == 13 || r_scause() == 15) {
uint64 va = r_stval();
if (va < p->sz && va >= PGROUNDUP(p->trapframe->sp)) {
uint64 pa = (uint64)kalloc();
if (pa == 0) {
p->killed = 1;
} else {
memset((void *) pa, 0, PGSIZE);
va = PGROUNDDOWN(va);
if (mappages(p->pagetable, va, PGSIZE, pa, PTE_U | PTE_R | PTE_W) != 0) {
kfree((void*)pa);
p->killed = 1;
}
}
} else {
p->killed = 1;
}
}
在添加page fault的处理代码段之后,echo hi
命令仍然不能正常运行,会出现以下报错。
panic: uvmunmap: not mapped
这个报错发生在uvmunmap
函数中,原因是,对于一些新申请的空间,我们暂未使用它们,于是内核不会为我们实际分配物理内存,于是在进程执行结束将销毁进程页表时,会引发此panic。
这里其实分为两种情况,第一种是某个虚拟地址对应的pte不存在,那么这一句判断会成立,由于此时条件成立不应该引发panic,故将panic删除并修改为continue;
即可。
if((pte = walk(pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
第二种情况是,某个虚拟地址的pte存在,一开始一直在疑惑为什么某个虚拟地址不存在映射但是pte却存在,后来发现自己是傻的,每次新建二三级页表都会开辟512个pte啊,所以之后的地址pte可能包括在里面,此时需要判断该pte是否是valid的,即判断标志位PTE_V即可。这里原始代码也是panic的,也是换成continue;
就好。
if((*pte & PTE_V) == 0)
panic("uvmunmap: not mapped");
该部分处理剩下的一些问题,根据实验文档,第一个问题是处理sbrk
参数为负的情况,这个我在第一部分就已经处理好了。
第二个问题是,如果某个进程在高于sbrk
分配的任何虚拟地址内存上出现page fault,或者在用户栈下面出现page fault,则终止该进程。这一个问题我在第一部分也处理了,就是对于那个合理地址范围的讨论。
第三个问题是在fork
函数中正确处理父进程到子进程拷贝。观察fork
函数可以发现,它调用了uvmcopy
将父进程的进程页表映射拷贝到子进程中,并为子进程分配空间,如下所示。
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
查看uvmcopy
函数可以发现,它也有像uvmunmap
函数的问题,就是没有考虑存在暂未分配的虚拟地址,同样将两个panic变为continue;
即可。
if((pte = walk(old, i, 0)) == 0)
continue;
if((*pte & PTE_V) == 0)
continue;
最后一个问题,也是隐藏最深的一个问题,就是当使用sbrk
系统调用申请完空间后,直接将暂未分配内存但有效的地址传递给其他系统调用进行使用(如read或者write),此时系统调用获取用户指针的方式是通过argaddr
函数,我们只需要在这里去判断该地址是否可以解析为物理地址(使用walkaddr
函数),如果无法解析,说明该虚拟地址还未分配物理空间,类似usertrap
中判断并分配即可。
// Retrieve an argument as a pointer.
// Doesn't check for legality, since
// copyin/copyout will do that.
int
argaddr(int n, uint64 *ip)
{
*ip = argraw(n);
struct proc* p = myproc();
if (walkaddr(p->pagetable, *ip) == 0) {
uint64 va = *ip;
if (va < p->sz && va >= PGROUNDUP(p->trapframe->sp)) {
uint64 pa = (uint64)kalloc();
if (pa == 0) {
return -1;
} else {
memset((void *) pa, 0, PGSIZE);
va = PGROUNDDOWN(va);
if (mappages(p->pagetable, va, PGSIZE, pa, PTE_U | PTE_R | PTE_W) != 0) {
kfree((void*)pa);
return -1;
}
}
} else {
return -1;
}
}
return 0;
}