Lab5 Copy on Write


Lab链接:Lab: Copy-on-Write Fork for xv6

Lab源码:momo/MIT-6S081/cow - Gitee.com

题目

xv6 fork 系统调用会将父进程的所有内存数据复制给子进程。若父进程内存占用大,复制耗时久,且若子进程后续执行exec,复制的内存大概率被丢弃,造成资源浪费。

本实验要求实现写时复制(Copy-on-Write, COW)功能,优化 fork 系统调用:初始时,子进程与父进程共享物理内存页,仅当任一进程尝试写入共享页面时,才真正为该进程分配新的物理页并复制内容,从而减少不必要的内存拷贝,提升性能。

思路

物理页引用计数

使用写时复制机制时,多个页表项可能映射到同一物理页。因此,kfree 不能立即释放物理页,而应通过引用计数记录当前被引用的次数。仅当引用计数降为零时,才真正释放该物理页。

引用计数结构体

#define FREE_MAX_CNT PHYSTOP/PGSIZE
struct ref_cnt {
  struct spinlock lock[FREE_MAX_CNT];
  int cnt[FREE_MAX_CNT];
};

初始化

void kinit() {
  for(i = 0; i < FREE_MAX_CNT; i++) {
    initlock(&kmem.counter.lock[i], "");
    kmem.counter.cnt[i] = 1;
  }	
}

分配

void *kalloc(void) {
  if(r) {
    ref_cnt_idx = (uint64)((void*)r) / PGSIZE;
    kmem.counter.cnt[ref_cnt_idx] = 1;
    memset((char*)r, 5, PGSIZE); // fill with junk
  }
}

释放

void kfree(void *pa) {
  ref_cnt_idx = (uint64)pa / PGSIZE;

  acquire(&kmem.counter.lock[ref_cnt_idx]);
  kmem.counter.cnt[ref_cnt_idx]--;
  if(kmem.counter.cnt[ref_cnt_idx] > 0) {
    release(&kmem.counter.lock[ref_cnt_idx]);
    return;
  }
  release(&kmem.counter.lock[ref_cnt_idx]);
}

Copy-on-Write 初始化

系统调用 fork 创建子进程时,并不会立即为子进程申请新页并复制父进程的每一页,而是让子进程的页表直接映射到父进程原有的物理页上

同时,在父子进程的页表中,清除所有数据页的写入权限并添加 COW 标志,以此将它们设置为 COW 页

int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) {
  pte_t *pte;
  uint64 pa, i;
  uint flags;

  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0)
      panic("uvmcopy: pte should exist");

    if((*pte & PTE_V) == 0)
      panic("uvmcopy: page not present");

    // clean parent's pte flag PTE_W & add parent's pte flag PTE_COW
    *pte = (*pte | PTE_COW) & ~PTE_W;

    // child process map to parent process's physical address pa
    // grant child's pte flag permission the same as parent
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      goto err;
    }
    krefcnt_add((void*)pa);
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}

Copy-on-Write 处理

当父进程或子进程任意一方尝试写入 COW 共享页时,会触发 page fault ,并进入 usertrapkerneltrap

此时进程既可能处于用户态,也可能处于内核态:

应用程序向 COW 页写会触发 page fault ,进入usertrap

if(r_scause() == 15) {
    if(cowhandler(r_stval()) != 0)
      p->killed = 1;
}

应用程序在内核态使用copyout()向写用户内存空间的COW页写入,也会触发 page fault ,进而进入kerneltrap

int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len) {
  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    // ensure the user address va0 is handled for copy-on-write.
    cowhandler(va0);
    // ...
  }
  return 0;
}

将 COW 造成的 page fault 处理流程封装为 cowhandler() 函数,包括以下步骤:

  1. 获取缺页地址:通过 r_stval() 获取触发缺页的虚拟地址
  2. 查找并验证页表项:使用 walk() 获取该地址的页表项
  3. 验证合理性:检查发生缺页的进程的页表项是否有效且具有 COW 标志,排除对代码区等真正只读页的误写
  4. 申请新页,复制原页内容
  5. 释放原页引用:调用 kfree() 释放原物理页的引用(递减计数,计数归零则真正释放)
  6. 更新发生缺页的进程的页表项的映射位置,指向新页
  7. 更新两个进程的页表项的权限:赋予写权限,清除 COW 标志
// copy-on-write fault handler
int cowhandler(uint64 va) {
  struct proc *p;
  uint64 pa;
  uint flags;
  pte_t *pte;
  char *mem;

  p = myproc();

  if(va >= MAXVA) {
    printf("cow: faulting virtual address exceeds MAXVA\n");
    return -1;
  }
  va = PGROUNDDOWN(va);

  // find the page table entry (pte) of the faulting virtual address (va)
  if((pte = walk(p->pagetable, va, 0)) == 0) {
    printf("cow: the page table entry (pte) of va does not exist\n");
    return -1;
  }

  // verify if the page is marked as Copy-On-Write (COW)
  if((*pte & PTE_COW) == 0) {
    return 1;
  }

  // copy contents from original page to new page, then free the original page
  if((mem = kalloc()) == 0) {
    printf("cow: failed to allocate new physical page\n");
    return -1;
  }
  pa = PTE2PA(*pte);
  memmove(mem, (char*)pa, PGSIZE);
  kfree((void*)pa);

  // clear COW flag, grant write permission
  flags = (PTE_FLAGS(*pte) | PTE_W) & ~PTE_COW;

  // remap virtual address to new physical page with updated flags
  uvmunmap(p->pagetable, va, 1, 0);
  if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, flags) != 0){
    uvmunmap(p->pagetable, va, 1, 0);
    printf("cow: failed to remap virtual address\n");
    return -1;
  }
  return 0;
}

要点

为什么要添加COW标记?

向代码区写入也可能触发 page fault ,向本来就没有写入权限的页写入就是一个异常,不能使用cowhandler添加写入权限,因此使用 COW 标记以示区分。

为什么不在kernelvec直接调用COW处理函数?

处理 COW 缺页的过程涉及内存分配、页面复制和页表更新等相对耗时的操作,不适合在 kernelvec 的中断上下文中直接执行。因此,选择在可能触发 COW 缺页的内核函数(例如 copyout)中主动进行检查和处理,以避免陷入中断上下文带来的复杂性。

kalloc ref 计数 不在 原有结构体中 添加计数字段(巧妙设计,为什么能够在r->next中写入下一个页的物理地址)而是使用数组保存计数值

重新mmap时,要先unmap,否则mmap发现&PTE_V == 1 报错


文章作者: AthenaCrafter
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AthenaCrafter !
  目录