Lab6: Copy-on-Write Fork for xv6

虚拟内存提供了一定程度的间接寻址:内核可以通过将PTE标记为无效或只读来拦截内存引用,从而导致页面错误,还可以通过修改PTE来更改地址的含义。在计算机系统中有一种说法,任何系统问题都可以用某种程度的抽象方法来解决。Lazy allocation实验中提供了一个例子。这个实验探索了另一个例子:写时复制分支(copy-on write fork)。

在开始本实验前,将仓库切换到cow分支

  1. $ git fetch
  2. $ git checkout cow
  3. $ make clean

问题

xv6中的fork()系统调用将父进程的所有用户空间内存复制到子进程中。如果父进程较大,则复制可能需要很长时间。更糟糕的是,这项工作经常造成大量浪费;例如,子进程中的fork()后跟exec()将导致子进程丢弃复制的内存,而其中的大部分可能都从未使用过。另一方面,如果父子进程都使用一个页面,并且其中一个或两个对该页面有写操作,则确实需要复制。

解决方案

copy-on-write (COW) fork()的目标是推迟到子进程实际需要物理内存拷贝时再进行分配和复制物理内存页面。

COW fork()只为子进程创建一个页表,用户内存的PTE指向父进程的物理页。COW fork()将父进程和子进程中的所有用户PTE标记为不可写。当任一进程试图写入其中一个COW页时,CPU将强制产生页面错误。内核页面错误处理程序检测到这种情况将为出错进程分配一页物理内存,将原始页复制到新页中,并修改出错进程中的相关PTE指向新的页面,将PTE标记为可写。当页面错误处理程序返回时,用户进程将能够写入其页面副本。

COW fork()将使得释放用户内存的物理页面变得更加棘手。给定的物理页可能会被多个进程的页表引用,并且只有在最后一个引用消失时才应该被释放。

Implement copy-on write (hard)

[!TIP|label:YOUR JOB] 您的任务是在xv6内核中实现copy-on-write fork。如果修改后的内核同时成功执行cowtestusertests程序就完成了。

为了帮助测试你的实现方案,我们提供了一个名为cowtest的xv6程序(源代码位于user/cowtest.c)。cowtest运行各种测试,但在未修改的xv6上,即使是第一个测试也会失败。因此,最初您将看到:

  1. $ cowtest
  2. simple: fork() failed
  3. $

“simple”测试分配超过一半的可用物理内存,然后执行一系列的fork()fork失败的原因是没有足够的可用物理内存来为子进程提供父进程内存的完整副本。

完成本实验后,内核应该通过cowtestusertests中的所有测试。即:

  1. $ cowtest
  2. simple: ok
  3. simple: ok
  4. three: zombie!
  5. ok
  6. three: zombie!
  7. ok
  8. three: zombie!
  9. ok
  10. file: ok
  11. ALL COW TESTS PASSED
  12. $ usertests
  13. ...
  14. ALL TESTS PASSED
  15. $

这是一个合理的攻克计划:

  1. 修改uvmcopy()将父进程的物理页映射到子进程,而不是分配新页。在子进程和父进程的PTE中清除PTE_W标志。
  2. 修改usertrap()以识别页面错误。当COW页面出现页面错误时,使用kalloc()分配一个新页面,并将旧页面复制到新页面,然后将新页面添加到PTE中并设置PTE_W
  3. 确保每个物理页在最后一个PTE对它的引用撤销时被释放——而不是在此之前。这样做的一个好方法是为每个物理页保留引用该页面的用户页表数的“引用计数”。当kalloc()分配页时,将页的引用计数设置为1。当fork导致子进程共享页面时,增加页的引用计数;每当任何进程从其页表中删除页面时,减少页的引用计数。kfree()只应在引用计数为零时将页面放回空闲列表。可以将这些计数保存在一个固定大小的整型数组中。你必须制定一个如何索引数组以及如何选择数组大小的方案。例如,您可以用页的物理地址除以4096对数组进行索引,并为数组提供等同于kalloc.ckinit()在空闲列表中放置的所有页面的最高物理地址的元素数。
  4. 修改copyout()在遇到COW页面时使用与页面错误相同的方案。

提示:

  • lazy page allocation实验可能已经让您熟悉了许多与copy-on-write相关的xv6内核代码。但是,您不应该将这个实验室建立在您的lazy allocation解决方案的基础上;相反,请按照上面的说明从一个新的xv6开始。
  • 有一种可能很有用的方法来记录每个PTE是否是COW映射。您可以使用RISC-V PTE中的RSW(reserved for software,即为软件保留的)位来实现此目的。
  • usertests检查cowtest不测试的场景,所以别忘两个测试都需要完全通过。
  • kernel/riscv.h的末尾有一些有用的宏和页表标志位的定义。
  • 如果出现COW页面错误并且没有可用内存,则应终止进程。

可选的挑战练习

  • 修改xv6以同时支持lazy allocation和COW。
  • 测量您的COW实现减少了多少xv6拷贝的字节数以及分配的物理页数。寻找并利用机会进一步减少这些数字。