写时复制(COW )机制是操作系统中一种常见的惰性机制,在很多场景下可以提供较好 的性能和比物理资源多很多的虚拟资源量,因而机制在内存管理中使用较多。实现 COW 机制 需要我们将前几个实验的涉及的概念(进程、分页和中断)融会贯通,从这个实验开始,我们 的工作将遍及 xv6 内核的各个部分。

开始之前切换到 cow 分支

5.1 实现写时复制的 Fork 系统调用

我们早在第三章 Lab Utilities 中就已经接触了 xv6 中用于生成子进程的系统调用 fork ,该系统调用由于其实用的设计被后世的类 Unix 系统一直沿用,可谓 Unix v6 留下的宝贵遗产。在原始的 xv6 的实现中,fork 会将父进程的所有页面完整地复制到一份,映射到子进程中。然而很多时候,子进程只会读取这些页面的内容,而不会写入这些页面。为了节约内存的实际用量,我们可以在子进程(或父进程)真正需要写入页面时才分配新的页面并进行数据的复制,这种机制被称为 COW fork 。

下面是一些建议:

  1. 修改 uvmcopy() ,将父进程的物理页映射到子进程,以避免分配新的页。同时清楚子进程和父进程 PTE 的 PTE_W 位,使它们标记为不可写。
  2. 修改 usertrap() ,以此来处理页面错误 (page fault)。当在一个本来可写的 COW 页发生“写页面错误” (write page-falut) 的时候,使用 kalloc() 分配一个新的页,将旧页中的数据拷贝到新页中,并将新页的 PTE 的 PTE_W 位置为1. 值得注意的是,对于哪些本来就使只读的(例如代码段),不论在旧页还是新页中,应该依旧保持它的只读性,那些试图对这样一个只读页进行写入的进程应该被杀死。
  3. 确保没有一个物理页被 PTE 引用之后,再释放它们,不能提前释放。一个好的做法是,维持一个“引用计数数组” (reference count) ,使用索引代表对应的物理页,值代表被引用的个数。当调用 kalloc() 分配一个物理页的时候,将其 reference count 设置为 1 。当 fork 导致子进程共享同一个页的时候,递增其页的引用计数;当任何一个进程从它们的页表中舍弃这个页的时候,递减其页的引用计数。kfree() 仅当一个页的引用计数为零时,才将这个页放到空闲页列表中。将这些引用计数放到一个 int 类型的数组中是可以的,你必须思考,如何去索引这个数组,以及如何选择这个数组的大小。例如,你可以用一个页的物理地址/4096 来索引数组。
  4. 修改 copyout() ,使用类似的思路去处理 COW 页。

一些注意的点:

  1. 记录一个 PTE 是否是 COW 映射是有帮助的。你可以使用 RISC-V PTE 的 RSW (reserved for software) 位来指示。
  2. usertests -q 测试一些 cowtest 没有测试的东西,所以不要忘记两个测试都要通过。
  3. 一些对于页表标志 (page table flags) 有用的宏和定义可以在 kernel/riscv.h 中找到。
  4. 如果一个 COW 页错误发生时,没有剩余可用内存,那么该进程应该被杀死。

首先需要处理引用计数的问题。在 kernel/kalloc.c 中定义一个全局变量和锁。

// the reference count of physical memory page
int useReference[PHYSTOP/PGSIZE];
struct spinlock ref_count_lock;

然后在函数 kalloc() 中,初始化新分配的物理页的引用计数为 1.

void *
kalloc(void)
{
  ...
  if(r) {
    kmem.freelist = r->next;
    acquire(&ref_count_lock);
    // initialization the ref count to 1
    useReference[(uint64)r / PGSIZE] = 1;
    release(&ref_count_lock);
  }
  release(&kmem.lock);
  ...
}

在该代码段中,if (r) 是一个条件语句,用于判断 kmem.freelist 是否为空。kmem.freelist 是一个指向空闲物理内存页面链表的指针。如果 kmem.freelist 不为空,则说明有空闲的物理内存页面可以被分配给进程使用。