MIT 6.s081 lab5.3–lazytests
No.1 写在前面的话
5.1和5.2带我们初识页表错误,并了解如何对页表错误进行处理,5.3的tests则是帮助我们增加代码的健壮性,以适应多种状况的发生。当然,因为我对中断机制理解的还是不够深刻(alarm打算重做还没写博客的原因),中间稍稍借助了答案【脏了一点】,不过对于sbrk也算是更加了解了吧。
No.2 对于健壮性的考虑
情况一:sbrk参数为负
通过lecture可知,sbrk使用户程序扩大自己的heap,其系统调用参数为整数,代表了想要申请的page数量。查看原先的growproc(),它所充当的便是调整用户heap的工作,代码如下:
// Grow or shrink user memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;//heap最底端,stack最顶端
if(n > 0){
if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}
不难看出,需要扩展内存利用uvmalloc,而缩减则使用uvmdealloc,后者符合要求,那么将其补充到5.2中修改的sbrk里:
uint64
sys_sbrk(void)
{
uint64 addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
/*
if(growproc(n) < 0)
return -1;
*/
myproc()->sz += n;
if(n < 0){
uvmdealloc(myproc()->pagetable, addr, myproc()->sz);
}
return addr;
}
情况二:高于分配地址
一开始我以为高于sbrk分配的任何虚拟地址是指需要用PGROUNDDOWN判断是否高出页顶部【我的确到目前为止对于vm还是很模糊】,一头雾水的补充,然后报错,实则并不是这样。
sbrk的机制为记录想要申请的page数量,然后将p->sz增加(或减少)n个。在这里p->sz便是sbrk要申请分配的内存,如果高于此则无法进行分配;但因为要引起懒分配,必然是先记录sz引起中断再进行页分配,所以这段代码需要在trap里进行:
if(va >= p->sz){
p->killed = 1;//出现错误终止进程
}
情况三: 父到子拷贝
根据提示,看一看fork中哪一部分是内存拷贝:
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
//*****
}
追溯一下可以发现几个panic:
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
panic("uvmcopy: page not present");
根据之前处理错误的情况,将panic注释并添加continue就好,不过在这里还是想补充一下为何这样做可行的原因(frans解释):
A: 我们现在是lazy allocation,我们只会为需要的内存分配物理内存page。如果我们不需要这部分内存,那么就不会存在map关系,这非常的合理。相应的,我们对于这部分内存也不能释放,因为没有实际的物理内存可以释放,所以这里最好的处理方式就是continue,跳过并处理下一个page。
Q: 之前的panic存在是有理由的,是否应该判断一下,然后对于特定的场景还是panic?
A: 为什么之前的panic会存在?对于未修改的XV6,永远也不会出现用户内存未map的情况,所以一旦出现这种情况需要panic。但是现在我们更改了XV6,所以我们需要去掉这里的panic,因为之前的不可能变成了可能。
情况五:内存不足
没有先处理情况四的原因是我还有些茫然,先处理看着简单的。
执行kalloc失败则终止进程,那么要如何判断呢?搜索kalloc()查看注释:
// Returns 0 if the memory cannot be allocated.
ok,非常简单易懂,那么在trap中对分配内存的部分进行判断:
if(mem == 0){
p->killed = 1;
}else{
//分配内存
情况六:用户栈下的无效页面
说实话,因为对于汇编和各种寄存器指针并不熟悉,第一眼我并没有思路,不懂何为用户栈,只得看一看之前关于栈的分布图:
唯一与栈有关系的就是sp了,这时候我忽然想起,这不是栈指针吗?既然它在底部,如果低于它那么一定会错,这不就符合条件了吗?
兴高采烈一个p->sp才发现不对,又停了下来,究竟该如何获得sp呢?(怪我没理解trap那里Morris的演示)
再次梳理trap课程,此时我们已经在trap.c的位置对中断进行处理,而中断处理的寄存器在trampoline中,不能直接调用,所以应该这样写:
if(va < p->trapframe->sp){
p->killed = 1;//有错就杀死!
}
至此,只剩下最后一种情况。
情况四:传递地址但未分配内存
提示中sbrk向系统调用传递地址,但我用sbrk中的growproc追溯了一遍也不知道它是怎么给write或read传递的【好废】,没办法,只得换一种思路,直接看看这两系统调用,或许有线索。
直接追溯sys_read,可知它的路线为:fileread->readi->either_copyout->copyout->walkaddr,终于走到终点才知道我们真正需要修改的是哪个函数,既然该内存地址尚未分配,那将其分配就好,可以利用我们已经写好的集大成代码块;而其又提示道有效地址,我们很容易就能知道在哪里修改:
if(pte == 0 || (*pte & PTE_V) == 0){//有效的页表条目和有效位,合并起来一起判断
对于此前上面的各种情况进行整理,trap中的代码整理重写如下:
else if(r_scause() == 13 || r_scause() == 15){
uint64 va = r_stval();
if(va >= p->sz){
p->killed = 1;
}
va = PGROUNDDOWN(va);
if(va < p->trapframe->sp){
p->killed = 1;
}
//对于vm的判断到此结束,现在为分配内存的判断
uint64* mem = (uint64*)kalloc();
if(mem == 0){
p->killed = 1;
}else{
memset(mem, 0, PGSIZE);
if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
p->killed = 1;
}
}
}
将其全部复制过来,粘贴到walkaddr,并进行修改:
struct proc *p = myproc();//照常添加
if(va >= MAXVA)
return 0;
pte = walk(pagetable, va, 0);
if(pte == 0 || (*pte & PTE_V) == 0){
if(va >= p->sz) return 0;
if(va < p->trapframe->sp) return 0;
uint64* mem = (uint64*)kalloc();
if(mem == 0) return 0;
memset(mem, 0, PGSIZE);
//uint64 a = PGROUNDUP(va);
//一些文章在这里利用dup,虽然用down也能通过测试,但我不知道用dup的原因是什么
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
return 0;
}
}
if((*pte & PTE_U) == 0)
return 0;
(寻找修改部分我参看了答案)。
【用时一天半】