从上课视频来看,估计当年上这门课的同学们被Page Table Lab整破防了,于是教授在这里想方设法地降难度给提示来补偿大家在Page Table Lab受的罪。
本次实验要实现Lazy Allocation。应用程序通常不会事先知道自己要用多少内存,因此往往会有很多内存预先申请了但是没有使用,造成了很大的浪费。如果实现Lazy-Allocation,就可以提高内存的利用率。具体而言,我们给应用程序分配虚拟地址,但是不急着分配对应的物理空间,而是直到进程需要访问物理空间的时候才分配就像老师宣布要ddl了你才开始赶作业一样。
1. 修改sbrk
第一步是修改sbrk
或者说就是修改growproc
,很简单,直接修改进程大小字段即可,不分配物理页,等到访问(不存在的)物理页发生Page Fault了再分配。所以这一步做完以后用shell执行命令必然会出错。
1 2 3 4 5 6
| int growproc(int n) { myproc()->sz += n; return 0; }
|
2. Lazy Allocation
其实到这里为止,教授都在课堂上写过代码。。。在usertrap
中判断中断原因(检查scause
寄存器的值)是否为Page Fault,然后从stval
寄存器中取出发生Page Fault的虚拟地址,再照抄参考借鉴uvmalloc
中的代码片段就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| if(r_scause() == 13 || r_scause() == 15) { uint64 va = r_stval(); printf("usertrap(): scause %p: page fault. pid=%d\n", r_scause(), p->pid); printf(" sepc=%p stval=%p. Doing lazy allocation.\n", r_sepc(), r_stval()); uint64* mem = (uint64*)kalloc(); if(mem == 0) p->killed = 1; memset(mem, 0, PGSIZE); uint64 a = PGROUNDDOWN(va); if(mappages(p->pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_U) != 0) { kfree(mem); p->killed = 1; } if(p->killed) printf("Oops, lazy allocation failed. Killed process.\n"); else printf("Lazy allocation done: %p -> %p\n", a, (uint64)mem); } 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
中,如果我们遇到了一个没分配物理地址的虚拟地址(一般来说就是分配了但是还没等到用就被释放了),uvmunmap
会panic,把相关的判断去掉即可。此外,如果设置了释放物理页的选项,那么用kfree
释放一个未分配物理页的虚拟页也会panic,因此在kfree
之前先检查物理页是否存在,如果它不存在(还没分配)那就不用释放了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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_FLAGS(*pte) == PTE_V) panic("uvmunmap: not a leaf"); if(do_free){ uint64 pa = PTE2PA(*pte); if(*pte & PTE_V) kfree((void*)pa); } *pte = 0; } }
|
3. 各种奇葩的特殊情况
虽然这个难度标的是moderate,但是我花的时间差不多相当于一个hard太菜了,感觉大概是moderate里比较麻烦的那种顶尖力B肯定比不过普通力A嘛。不过它当然比不上Page Table Lab那两个魔鬼hard任务。
尽管如此,还是有一些坑点。提示里面已经给出了一些要完善的地方,比如判断虚拟地址的范围不能爆栈,比如注意缩小用户进程内存空间的情况(个人认为应该及时uvmdemalloc
,不这么做的话你不能指望后面会在这里发生Page Fault让你释放物理页)。。。这些都还是比较简单的,很多做个判断差不多就行。下面列出几个比较坑的地方。
首先是不建议在usertrap
里给错误信息输出提示语句,因为usertests里有几个邪门测试是要疯狂分配内存直到穷尽地址空间为止,教授就是希望你能判断出这些情况不合法然后不处理或者杀掉进程。但是这些邪门的测试把这种事情干了很多很多很多遍,所以输出了一大堆错误提示信息让我误以为出问题了,然后对空debug搞半天。。。实际上就是正常的。
然后是sbrkbug
测试的一个坑点,这个测试会调用sbrk
把用户进程内存空间缩小为0。。。然后肯定会触发Page Fault,但是这个Page Fault的错误代码既不是13也不是15而是12,在usertrap
里要注意这种情况,不然会一直处理这个Page Fault。
最后是sbrkarg
测试,这个测试可以检查你是否做对了copyin
。copyin
接受用户空间的指针的时候会用walkaddr
去寻找对应的物理地址,如果没找到的话会直接判错并返回。但是现在我们采用了Lazy Allocation策略,有可能访问到合法但是未分配的物理地址,这时我们当然不能草率地返回一个错误码。因此在walkaddr
里面也要进行Lazy Allocation,如果我们访问不到物理地址,可以试试看是不是需要Lazy Allocation,如果Lazy Allocation成功了就再访问一次(我知道可以优化一小下,懒得改了),这次我们就得到合法的物理地址了。
代码大致长这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| uint64 walkaddr(pagetable_t pagetable, uint64 va) { pte_t *pte; uint64 pa;
if(va >= MAXVA) return 0; pte = walk(pagetable, va, 0); if(!pte || ((*pte) & PTE_V) == 0) { if(!lazy_alloc(va)) return 0; pte = walk(pagetable, va, 0); } if((*pte & PTE_U) == 0) return 0; pa = PTE2PA(*pte); return pa; }
|
其中lazy_alloc
是我把第二个任务写在usertrap
里的代码提取出来的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| int lazy_alloc(uint64 va) { struct proc *p = myproc(); if(va >= MAXVA) return 0; if(va >= p->sz || va < p->trapframe->sp) return 0; uint64* mem = (uint64*)kalloc(); if(mem == 0) { return 0; } memset(mem, 0, PGSIZE); uint64 a = PGROUNDDOWN(va); if(mappages(p->pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_U) != 0) { printf("map %p to %p, failed\n", a, (uint64)mem); kfree(mem); return 0; } return 1; }
|
好像就没啥了诶。。。一些细节的改动可以看看我的github,虽然它们有些感觉不是必须的。。。