主要记录下我在写这个lab的时候遇到的问题,具体的代码网上已经有很多了 。
BUG1
理论上来讲,vm.c文件中freewalk的panic是不需要注释掉的,当时写的时候老是报错
panic freewalk: leaf
后来发现问题出在trap函数中:
else if((r_scause() == 13 || r_scause() == 15) && r_stval() >= p->heap_start){
uint64 vmaddr = r_stval();
if(vmaddr < p->sz){ // disaligned ?
uint64 rel = uvmalloc(p->pagetable, PGROUNDDOWN(vmaddr), vmaddr + PGSIZE); // bug!
if(rel == 0){
printf("no more memory\n");
p->killed = 1;
}
}
在传uvmalloc第三个参数的时候,忘了加上PGROUNDDOWN宏。导致本来是分出一页的,却分出了两页,第二页可能超出p->sz,因此uvmunmap没有把第二页对应的页表项清除,因此在freewalk中报错了。
BUG2
这个bug我应该把锅扔给xv6的开发者(或者人家是有意来坑一下?),首先看一下原来sbrk的样子:
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz; // 64位无符号数截断为32位有符号数!
if(growproc(n) < 0)
return -1;
return addr; //32位有符号数拓展回64位无符号数!
}
因此原本的sbrk是有隐患的,但是由于用户的内存不可能到4G,因此这个隐患没有暴露出来。但是在这个lab里面,因为延迟分配,所以用户内存是可能到达4G以上的,这里就出问题。因此修改sbrk的第一要义是,把addr的类型改为uint64。
许多人给出的解法基本都是这样的
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) p->sz = uvmdealloc(p->pagetable, p->sz, p->sz + n);
else p->sz += n;
// if(growproc(n) < 0)
// return -1;
return addr;
}
典型的缺页处理是这样:
else if(cause == 13 || cause == 15) { // 页表映射异常
uint64 va = r_stval();
// if(lazy_uvmalloc(p->pagetable, va) != 0) {
// p->killed = 1;
// }
if (va >= p->sz || va < p->trapframe->sp){
printf("va : %p, \np->sz: %p", va, p->sz);
p->killed = 1; // 如果访问地址不在用户空间内或者栈以上则杀死进程
}
else { // 延迟分配
if(lazy_uvmalloc(p->pagetable, va) != 0) {
p->killed = 1; //分配失败也杀死进程
}
}
}
当然,这样写最后会通过测试,我们来分析一下这样写为什么能过test。
查看test3的测试逻辑,可知测试进程会不断使用malloc分配内存并消耗内存,如果malloc返回0时该进程还没有被操作系统杀死,那么测试失败。如果该进程因为耗尽内存而被系统杀死(异常中止),则测试成功。
上面的代码在p->sz第一次大于 2 31 2^{31} 231时,调用sbrk,此时赋值给addr,addr符号位为1,在返回addr时,它会先进行符号拓展。因此sbrk返回的地址是一个无比巨大的uint64!
然后用户进程对返回地址进行赋值,触发页错误,进入内核,这个时候va就是一个无比巨大的uint64,而且远大于p->sz,因此上面的trap逻辑会判断用户进程越界,从而杀死进程。因此test3成功通过了。但这里用户进程被杀死并不是因为耗尽内存!
最后测试验证一下:
上边是一个计数器,计数test3中调用了多少次malloc,可以看到,在调用了128次malloc后,返回的va编变成了一个巨大的uint64,因此被内核杀死了(参考上面写的trap中的printf语句)。