先导知识
我们之前在使用fork的时候,是直接复制的物理内存。然而这么做其实是非常浪费的,因为在实际中发现,fork完之后立马执行exec的概率很大,而exec是去加载特定的镜像,所以复制这个动作完全是浪费的。
除了fork这个系统调用以外,还有一些可以利用懒复制的应用。比如sbrk,它的作用是开辟内存空间,但是如果应用要了就开辟空间,而应用要是最后没去使用就很浪费;所以比较好的做法是在页表中给它新增映射关系,但是并不分配真正的物理内存;还有一个就是可以不全加载一个进程的所有内容到内存中,而是真正要被用到的时候才加载进来。
Eliminate allocation from sbrk()(简单)
这个就是把分配物理内存的代码移除,但是proc的size记得要加。还有一个比较容易忽视的点是,当缩小的时候,页表的内存空间必须马上回收(这个其实是第三个小实验的内容….)。
1 | uint64 |
Lazy allocation(中等)
修改在usertrap.c的代码,能够处理缺页中断,并且返回用户程序继续执行。修改其它的代码,能够让echo hi成功运行。
然后是一些Hints:
- 你可以通过
r_scause()是否等于13或者15来判断是否是缺页中断。 r_stval()返回的是stval的值,也即造成这次缺页中断的虚拟地址的值。- 从vm.c中抄
uvmalloc(),你还需要运行kalloc()和mappages() - 使用
PGROUNDDOWN(va)来确定虚拟地址所在页。 uvmunmap()会导致panic,如果一些页面没有被映射,让它不会panic。- 如果系统崩溃了,在kernel.asm中查看
sepc - 使用在lab3写的vmprint来帮助调试。
照着上面的代码来写其实非常简单:
1 | if(r_scause() == 13 || r_scause() == 15){ |
就是找到引发这个缺页中断的虚拟地址,然后利用kalloc找到分配物理的页,利用mappages把它们之间的映射安装到对应的页表中。
最后不要忘了修改uvmunmap,因为它会导致panic,我们这里直接把panic给注释掉,然后加上continue!!因为原来的panic可以理解为return,导致不会进行之后的判断了,而我们把panic注释掉之后,后面的if会继续判断,就会有问题。
Lazytests and Usertests(中等)
xv6准备了测试程序,修改操作系统,使之能够通过 lazytests 和 usertests 。
hints:
- 处理负数的sbrk参数(已经完成)。
- 如果一个进程的虚拟地址超过了目前的p->sz,就认为错误。
- 修改Fork的父进程向子进程拷贝的逻辑,让它能够正确处理。
- 处理进程将有效地址从sbrk传递到系统调用(例如read或write),但尚未分配该地址的内存的情况。
- 如果kalloc分配失败,那么kill掉对应的进程。
- 处理在用户的栈之下的非法情况。
本质上其实就是对上面的那个函数加上一些条件判断,还需要对fork进行稍微的修改就可以了。这里附上一张虚拟内存的图:
这里的key在于fork和read/write这俩。
fork跟踪一下可以发现它实际的实现函数是uvmcopy,所以进到对应的代码段里面,查看其中其实就是利用walk来找到对应的pte进行处理,找不到就会panic,所以应该把这些跳掉(注意下面的代码也不能执行,所以用continue)
1 | .......... |
对于read/write来说,其实就是对应的copyin和copyout,所以需要修改对应的函数。可以发现这里有好几处需要用到对应的函数,所以把之前的处理页错误的代码封装成一个函数进行处理:
1 | /** |
在对应的copyin和copyout中进行处理。后来发现还有copyinstr,也顺手改掉了。
1 | ............. |
所有的这些过后,发现有一个小test没有通过:
1 | == Test usertests: sbrkarg == |
可以看到是sbrk这个函数的参数错误。
首先来分析一下调用路径:在usertests中利用write调用 -> 系统调用sys_write -> filewrite -> writei -> either_copyin -> copyin。
在网上看到另外一种实现,就是可以发现所有的这三个copy函数其实都是pa0 = walkaddr(pagetable, va0);,那么直接修改walkaddr是不是也可以呢?就直接在里面进行修改。
总结
这部分首先留了一个问题,就是write的系统调用并没有完全通过。
然后是经过上一个lab的trap铺垫,发现其实按需分配物理页实现起来其实很简单。