参考资料:操作系统MIT6.S081:Lab6->Copy-on-write fork_mit6.s081 lab前期准备-CSDN博客
整体思路
首先明确COW的作用,该功能主要是解决fork时,子进程会复制并映射父进程的全部物理页面的情况。在shell中实现某系统功能时,例如echo hi,常常通过父进程fork产生子进程,子进程调用exec后会立马放弃复制好的内存,这就造成了不小的损失。
因此,我们可以明确COW fork的目的是:在fork时,将父进程的物理页面直接映射到子进程的页表上。在实际使用这些物理页面时,我们再copy它,并且重新建立映射。通过这种方式我们可以减少资源的浪费。
COW fork和lazy allocation的目的比较类似,都有通过延迟分配、映射物理内存达到减少资源浪费的目的。
要提前想明白的几个问题
- 父子进程对彼此应该都是透明的,我们将父进程的物理页面映射到子进程的页表,如果此时父进程或子进程通过某种方式(write)更改了物理内存的内容,那么这个更改对另一方来说是可见的。解决这个问题的思路是:在fork后,修改父进程和子进程PTE的权限为只读。这样当这些物理页面将要被修改时,系统会产生page fault。
- 如何处理这个page fault?解决思路:先例如父进程fork产生了子进程,然后子进程write想要修改物理页面中的内容。此时,子进程应该将修改的物理页面copy一份,并且将原来的虚拟内存va映射到新的物理页面。同时子进程应该解除与原物理页面的映射关系。最后重新执行write操作。
- 处理完page fault后,PTE的权限如何修改?解决思路:修改者进程(一般来说就是子进程)自身会copy一份物理内存,做了重新映射,因此该物理内存此时只映射到修改者进程,既然如此,该PTE应该是可读可写的。非修改者进程(一般来说是父进程)会等待子进程的返回,因此不会出现该父进程还有其他fork的子进程,因此该物理页面没有与其他页面共享,其PTE应该恢复成可读可写,直到其下次fork产生子进程。
- 由于蝴蝶效应,我们在使用COW fork后,如果某物理页面还未被修改,说明其是在共享在父子进程之间的,此时如果我们kfree子进程,就不可以释放该物理页面。解决思路:构建一个结构体,搭配自旋锁,原子修改物理内存映射的次数。在kfree中,当映射次数为0时,即可正常释放;否则,将其映射次数减一。在fork中,遍历物理内存时,应该将映射次数加一。
- PTE权限仅仅修改PTE_W就够了吗?当然不够,如果仅仅设置PTE_W,由于COW fork产生的page fault和由于向只读页面write产生的page fault就一模一样了,我们想要在usertrap中处理的是COW fork产生的page fault。解决思路:在PTE保留标志位中选取一位作为COW位,在fork时,将父子进程的PTE_COW置位,并且PTE_W置零。
- 映射次数如何初始化?解决思路:很简单地,我们在每分配一块物理内存的时候kalloc,就将该物理页面的映射次数设置为1。这里要注意kinit中的freerange函数,其全局只会在开始调用一次,并且其中会调用kfree,因此我们要在这里遍历物理页面的时候把所有物理页面的映射次数赋值为1,然后再kfree,这样即可保证所有物理页面的映射次数在初始状态的时候为0,而不是-1。
- 对于内核中往用户物理页面写的操作,该如何处理呢,例如copyout,它是内核调用的函数,在其函数内的memmove操作前,也没有判断PTE标志位的过程,它不会陷入usertrap,因此我们需要对其进行另外的copy页面操作。解决思路:在copyout中,判断PTE_W和PTE_COW位,然后对需要处理的物理页面进行在usertrap中一样的处理。
需要修改、添加的内容
这里给出要修改的函数名,请自行想办法针对于以上问题进行修改,答案在参考资料中可以参考。
- fork->uvmcopy:对应问题1
- usertrap:对应问题2 3 5
- kfree:对应问题4
- kalloc:对应问题6
- struct pagerefcnt:对应问题4
- incref:是一个增加映射次数的函数,对应问题4
- copyout:对应问题7