Eliminate allocation from sbrk()
这个相当简单,在sys_sbrk中对growproc的调用注释掉就行,然后把sz加上n。这样应该分配的页面没有分配,同时也没有映射到页表,所以使用这些分配的页面的时候会报错panic: uvmunmap: not mapped
。
sys_sbrk如下:
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
myproc()->sz += n;
// if(growproc(n) < 0)
// return -1;
return addr;
}
运行echo hi
结果如下
$ echo hi
usertrap(): unexpected scause 0x000000000000000f pid=3
sepc=0x00000000000012ac stval=0x0000000000004008
panic: uvmunmap: not mapped
Lazy allocation
这个实验也还是比较简单,尤其是课程上已经有一些展示了,按照提示所说,在usertrap
的最后一个else前加上处理页面错误的语句,分配一个新页面,置零,映射到页表上,然后重新执行指令,重新执行指令不需要做什么,因为此时p->trapframe->epc
存储的就是引发错误的指令。不过这个实验好像没有考虑sbrk减少堆空间的情况。
usertrap
新增如下
else if (r_scause() == 13 || r_scause() == 15) {
uint64 va = r_stval();
uint64 begin = PGROUNDDOWN(va);
pagetable_t pagetable = p->pagetable;
char *mem = kalloc();
if(mem == 0){
kfree(mem);
exit(-1);
}
memset(mem, 0, PGSIZE);
if(mappages(pagetable, begin, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
exit(-1);
}
vmprint(pagetable);
}
因为释放进程内存的时候会通过页表释放堆空间,而很多堆空间并没有实际分配,即有效位为0,所以在uvmunmap
中会panic,我们把相应的改为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)
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;
}
}
运行echo hi
,可以看到
$ echo hi
page table 0x0000000087f75000
..0: pte 0x0000000021fdc801 pa 0x0000000087f72000
.. ..0: pte 0x0000000021fd9401 pa 0x0000000087f65000
.. .. ..0: pte 0x0000000021fdc05f pa 0x0000000087f70000
.. .. ..1: pte 0x0000000021fd98df pa 0x0000000087f66000
.. .. ..2: pte 0x0000000021fdc40f pa 0x0000000087f71000
.. .. ..3: pte 0x0000000021fd68df pa 0x0000000087f5a000
.. .. ..4: pte 0x0000000021fd641f pa 0x0000000087f59000
..255: pte 0x0000000021fdd001 pa 0x0000000087f74000
.. ..511: pte 0x0000000021fdcc01 pa 0x0000000087f73000
.. .. ..510: pte 0x0000000021fd90c7 pa 0x0000000087f64000
.. .. ..511: pte 0x0000000020001c4b pa 0x0000000080007000
page table 0x0000000087f75000
..0: pte 0x0000000021fdc801 pa 0x0000000087f72000
.. ..0: pte 0x0000000021fd9401 pa 0x0000000087f65000
.. .. ..0: pte 0x0000000021fdc05f pa 0x0000000087f70000
.. .. ..1: pte 0x0000000021fd98df pa 0x0000000087f66000
.. .. ..2: pte 0x0000000021fdc40f pa 0x0000000087f71000
.. .. ..3: pte 0x0000000021fd68df pa 0x0000000087f5a000
.. .. ..4: pte 0x0000000021fd64df pa 0x0000000087f59000
.. .. ..19: pte 0x0000000021fd601f pa 0x0000000087f58000
..255: pte 0x0000000021fdd001 pa 0x0000000087f74000
.. ..511: pte 0x0000000021fdcc01 pa 0x0000000087f73000
.. .. ..510: pte 0x0000000021fd90c7 pa 0x0000000087f64000
.. .. ..511: pte 0x0000000020001c4b pa 0x0000000080007000
hi
有两次页面错误。
Lazytests and Usertests
这个我觉得有一定难度,如果没有提示的话很难考虑这么仔细。
按照提示一条条来
-
处理
sbrk()
参数为负的情况。这个好处理,为负的情况依旧调用growproc处理即可:uint64 sys_sbrk(void) { int addr; int n; struct proc * p = myproc(); if(argint(0, &n) < 0) return -1; addr = myproc()->sz; if(n > 0){ p->sz = p->sz + n; }else{ if (growproc(n) == -1) { return -1; } } return addr; }
-
如果某个进程在高于
sbrk()
分配的任何虚拟内存地址上出现页错误,则终止该进程:页面错误中断的时候在分配新页面前检查是否高于p->sz。 -
在
fork()
中正确处理父到子内存拷贝:这里的拷贝主要指的是uvmcopy
,很简单,把这个函数里面前两个panic注释掉或者改成continue即可,因为在父进程中这些页也是没有实际存在的,子进程只要p->sz保持好的父进程一致就可以了。 -
处理这种情形:进程从
sbrk()
向系统调用(如read
或write
)传递有效地址,但尚未分配该地址的内存。这个是最难的,我们可以发现read或write的系统调用最终用的是copyout或者copyin,而这两者在walkaddr中如果找的是还没有实际分配页面的地址,那么发挥的物理地址就是0,从而系统调用返回-1,系统调用失败,在这个过程中并没有产生页面错误直接失败了,所以不会进入陷阱机制处理页面错误。所以要处理这种情况的话我们就要在walkaddr里面直接把没有实际分配的页面分配了,就像在trap中处理页面错误一样。 -
正确处理内存不足:如果在页面错误处理程序中执行
kalloc()
失败,则终止当前进程。:如果kalloc返回的是0,则终止进程。 -
处理用户栈下面的无效页面上发生的错误。如果虚拟地址va小于了用户栈,说明访问了不该访问的内存,终止进程。
综上,我们可以在vm.c里面写一个函数来分配映射lazy allocation的页面,如下:
// 注意:如果要在vm.c中使用proc结构体及其字段,要先加上"spinlock.h",再加上"proc.h"
int lazyalloc(uint64 va) {
struct proc *p = myproc();
// 如果高于sbrk的地址或者低于用户栈地址,终止进程
if (va >= p->sz || va <= PGROUNDDOWN(p->trapframe->sp)) {
return -1;
}
// printf("page fault:%p\n", va);
uint64 begin = PGROUNDDOWN(va);
pagetable_t pagetable = p->pagetable;
char *mem = kalloc();
if(mem == 0){
return -1;
}
memset(mem, 0, PGSIZE);
if(mappages(pagetable, begin, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
return -1;
}
return 0;
}
// 需要在defs.h中加上这个函数的声明才能在其他地方使用这个函数
然后usertrap和walkaddr中调用这个函数
// usertrap
else if (r_scause() == 13 || r_scause() == 15) {
uint64 va = r_stval();
if (lazyalloc(va) == -1) {
p->killed = 1;
}
}
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
pte_t *pte;
uint64 pa;
if(va >= MAXVA)
return 0;
pte = walk(pagetable, va, 0);
if(pte == 0 || (*pte & PTE_V) == 0) {
if (lazyalloc(va) == -1) {
return 0;
} else {
pte = walk(pagetable, va, 0);
}
}
// return 0;
// if((*pte & PTE_V) == 0)
// return 0;
if((*pte & PTE_U) == 0)
return 0;
pa = PTE2PA(*pte);
return pa;
}
如果运行时出现了panic,把相应地方注释掉或者改为continue即可。
运行lazytests
$ lazytests
lazytests starting
running test lazy alloc
test lazy alloc: OK
running test lazy unmap
test lazy unmap: OK
running test out of memory
test out of memory: OK
ALL TESTS PASSED
运行usertests
,可以看到所有测试通过,如果walkaddr编写的有问题的话,sbrkarg不会通过。
总结
这个实验总的来说我觉得还是比lab3简单一点,最需要注意的就是最后一个中关于系统调用涉及到lazy allocation不会触发页面错误需要手动编写代码来分配页面。