本实验仍然是虚拟内存,所以暂且跳过了中间的内容(指lec和课本)
分析
copy on write fork :
在之前的code walk through(https://blog.csdn.net/RedemptionC/article/details/107709618)中提到了,fork的实现里,有一个uvmcopy的调用,作用是将父进程的内存复制到子进程中,这个复制包括申请物理内存,设置pte
但是在下列情况下:
- 父进程和子进程都不修改某些内存页
- fork之后子进程马上调用exec,而由exec的实现可知,子进程的内存马上会由可执行文件的内容代替
这个复制,就显得很浪费资源,而且这两种情况不在少数,因此我们需要copy on write
fork时不申请额外的内存,只申请一个页表,并且把子进程页表的pte指向父进程的物理内存(和父进程页表的pte一样),仅当父进程/子进程修改其内存时,才复制页
这里的做法和lazy allocation差不多,都是在异常处理时申请内存:在fork时,把父进程的pte的PTE_W clear,这样写的时候,就会trap
在异常处理处,申请内存,真正复制页之后,将分别的pte的PTE_W都set,然后就能顺利write了
此时我们在free的时候,如果没有真正复制页,也就是多个进程的pte指向同一个物理页,就需要等ref count为0,才能free掉
当然如果已经真正复制了,就可以直接free掉(由此可见我们需要将这两种页进行区分)
书里给出的建议方案:
- 修改uvmcopy,使其将父进程的物理页面映射到子进程的地址空间,而不是分配新页面,并且在两者中都clear PTE_W
- 修改usertrap,识别page fault,当cow page出现page fautl时,分配一个新页面,将旧页面分配到新的,并且设置两者的PTE_W为set
- 确保cow page只有在ref count为0时才会被free(可能可以在kalloc.c中实现ref count)
- 修改copyout ,即内核在修改用户地址空间的页面时,也要做与usertrap相同的工作
- 增加一个PTE flag标识页面是否时cow page
关于如何实现ref count,阅读kalloc.c可知,物理页是通过一个结构体的链表来管理空闲的物理页的,并且这个结构体本身就保存在空闲页中
问题是,ref count 需要保存在非空闲页中?直接保存信息不会被覆盖吗?该如何设计?
比如:在kalloc里保存一个数组,数组长度为物理页的个数(kinit中得出),然后根据地址知道自己应该是哪一个物理页,从而得到对应页的ref count?测试了一下 有32723个物理页,开辟一个char型数组内存压力不大?
update:
本来以为一上午就能做完的,没想到磨磨唧唧的用了两天才完成,但即使这样,这也是我做的最快最顺利(相对)的一个lab了!
前面60分照着上面的建议方案做,比较顺利,第一个fail是filetest,其实如果留心filetest的注释的话,就知道问题多半是出在copyout()
因为filetest里用到了pipe,所以涉及到内核内存空间和用户内存空间的交换,copyout正是从内核到用户
copyout本来的实现是:
copyout(pagetable,dstva,src,len)
其中dstva是用户内存空间的虚拟地址,src是内核内存空间的,因为是direct map,所以也是物理地址
首先将dstva PGROUNDDOWN,即找到对应内存页的起始地址,然后用walkaddr找到对应的物理地址,然后用memmove,在两个物理地址之间复制(其中有一些比较细节的问题,比如这一段,也不是很懂,不过可以不管
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
//memmove((void *)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
因为我们要实现从写时复制(copy on write),所以这里首先要判断这是不是一个cow page,如果不是,那么还是老做法
如果是,那么我们就要做和usertrap里类似的事,首先申请一个新页(kalloc),然后把老页的内容复制到新页,然后修改pte的flag:clear pte_cow, set pte_w,即标识这个页不是cow page了,是一个进程独享的,并且可以写
注意len可能大于PGSIZE,因此上述工作也要放在while循环里做,所以最后的代码是这样的:
while(len>0){
uint64 n, va0, pa0;
va0 = PGROUNDDOWN(dstva);
pte_t *pte=walk(pagetable,va0,0);
if(pte==0||((*pte)&PTE_V)==0||((*pte)&PTE_U)==0){
printf("copyout : pte not exist or not accessible to user\n");
return -1;
}
pa0=PTE2PA(*pte);
if((*pte)&PTE_COW){
char *mem=kalloc();
if(mem==0){
printf("copyout: out of memory\n");
return -1;
}
memmove(mem,(char *)pa0,PGSIZE);
// set PTE_W , unset PTW_COW
(*pte)|=PTE_W;
(*pte)&=~PTE_COW;
int perm=PTE_FLAGS((*pte));
(*pte)&=~PTE_V;
if(mappages(pagetable, va0, PGSIZE, (uint64)mem, perm) != 0){
kfree(mem);
return -1;
}
(*pte)|=PTE_V;
pa0=(uint64)mem; // NOT SURE
}
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
曾经遇到的一个bug是:filetest中,通过Pipe,应该要读到的是0,但是读到了5,后来发现5是kalloc申请内存时填充的所谓junk
也就是实际上我是没有把pipe中的内容写到buffer中的,因为我一开始把是否是cow page,作为copyout中的一个分支,但是实际上不管你是不是cow page,都需要保留原来的代码:把源的数据写到目的位置,而不是仅仅是复制页
另一个遇到的bug是:usertest中,pgbug,这里使用了一个大于MAXVA的地址,所以在copyout中是应该要返回-1(标识error)的,要加上这一句
还有一个是:usertrap中,处理异常时,我只处理了SSCAUSE为15的,把原来的这一段注释了:
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;
但是实际上很多不是15的异常,也需要被kill,但是没有,就导致不能通过一些测试
另外,最后还需要新建一个文件,保存你完成实验的时间,不然只能得99分=-=
顺利通过啦~
最后,一些debug的小技巧:
1.很多时候,如果按next,会进入不可知的??代码,所以在本实验中,是不能完全依赖next 单步调试的,应该要自己理清头绪,找到下一步运行到的地方(函数调用等),打断点,然后continue
2.这是一个hint里给出的提示:有时候遇到panic,我们可以直接在一开始 b panic,然后continue,最后利用bt(backtrace)查看函数调用‘栈’(?),不过有时候函数调用太深,也会遇到??,所以可能printf才是最好的debug方法吧qaq 但是我懒 所以尽量尝试gdb
写的比较简单 代码放这吧: