Files
xv6-labs/cowlab.md
2025-07-03 10:28:51 +08:00

3.9 KiB
Raw Blame History

xv6-labs: Copy-On-Write (COW) Fork 实验详细总结

实验目标

实现 xv6 操作系统的 COW写时复制fork

  • fork 时父子进程共享物理页,只有在写入时才真正分配和复制物理页。
  • 提高 fork+exec 的效率,减少不必要的内存拷贝。

主要实现步骤与详细代码

1. 物理页引用计数

文件kernel/kalloc.c

  • 在文件顶部添加:
#define NPAGE ((PHYSTOP - KERNBASE) / PGSIZE)
static int refcnt[NPAGE];
static inline int pa2idx(void *pa) { return ((uint64)pa - KERNBASE) / PGSIZE; }
  • kalloc 分配新页时:
if(r) {
  memset((char*)r, 5, PGSIZE);
  int idx = pa2idx((void*)r);
  refcnt[idx] = 1;
}
  • kfree 释放页时:
int idx = pa2idx(pa);
acquire(&kmem.lock);
if(refcnt[idx] > 1) {
  refcnt[idx]--;
  release(&kmem.lock);
  return;
}
refcnt[idx] = 0;
release(&kmem.lock);
memset(pa, 1, PGSIZE);
// ...加入空闲链表...
  • 增加辅助函数:
void incref(void *pa) { refcnt[pa2idx(pa)]++; }
void decref(void *pa) { refcnt[pa2idx(pa)]--; }
int getref(void *pa) { return refcnt[pa2idx(pa)]; }

2. PTE 标记 COW

文件kernel/riscv.h

#define PTE_COW (1L << 8) // 使用RSW位标记COW

3. 修改 uvmcopy 实现 COW fork

文件kernel/vm.c

  • 在文件头部声明:
void incref(void *pa);
  • 修改 uvmcopy
int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) {
  ...
  for(i = 0; i < sz; i += PGSIZE){
    ...
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    if(flags & PTE_W) {
      flags = (flags & ~PTE_W) | PTE_COW;
      *pte = PA2PTE(pa) | flags;
    }
    if(mappages(new, i, PGSIZE, pa, flags) != 0){
      uvmunmap(new, 0, i / PGSIZE, 0);
      return -1;
    }
    incref((void*)pa);
  }
  return 0;
}

4. usertrap 处理写页错误

文件kernel/trap.c

  • 在文件头部声明:
void kfree(void *pa);
void *kalloc(void);
  • 在 usertrap() 添加COW处理
else if((scause == 15 || scause == 13)) { // store/load page fault
  pte_t *pte = walk(p->pagetable, stval, 0);
  if(pte && (*pte & PTE_V) && (*pte & PTE_U) && (*pte & PTE_COW)) {
    uint64 pa = PTE2PA(*pte);
    char *mem = kalloc();
    if(mem == 0) {
      setkilled(p);
    } else {
      memmove(mem, (void*)pa, PGSIZE);
      *pte = PA2PTE(mem) | (PTE_FLAGS(*pte) & ~PTE_COW) | PTE_W;
      kfree((void*)pa);
    }
  } else {
    setkilled(p);
  }
}

5. copyout 处理 COW

文件kernel/vm.c

  • 修改 copyout
if((*pte & PTE_W) == 0) {
  if((*pte & PTE_COW) != 0) {
    pa0 = PTE2PA(*pte);
    char *mem = kalloc();
    if(mem == 0)
      return -1;
    memmove(mem, (void*)pa0, PGSIZE);
    *pte = PA2PTE(mem) | PTE_FLAGS(*pte) | PTE_W;
    *pte &= ~PTE_COW;
    kfree((void*)pa0);
  } else {
    return -1;
  }
}

6. 释放页时引用计数

  • uvmunmap 释放页时直接调用 kfreekfree 内部已处理引用计数。

7. walk() 边界健壮性

文件kernel/vm.c

  • 修改 walk()
if(va >= MAXVA)
  return 0;

调试与测试

  • 通过 make qemu,运行 usertestscowtest,确保所有测试通过。
  • 重点关注 MAXVAplusforktestsbrkfail 等边界和异常测试。
  • 遇到 panic("walk") 问题,修正 walk() 返回 0。
  • 通过多次 fork/exec、写入、回收等场景验证内存共享和释放的正确性。

常见问题与修复

  • panic("walk")walk() 对非法地址应返回0而不是panic。
  • refcnt数组不能用end做基址应以KERNBASE为基址保证数组大小编译期可知。
  • 重复定义PTE宏只保留riscv.h中的定义。

总结

  • COW fork 显著提升了 fork+exec 的效率。
  • 通过引用计数和 COW 标志,保证了内存的正确共享与释放。
  • 通过边界检查和优雅失败,保证了系统健壮性。

本实验加深了对操作系统内存管理、页表机制和异常处理的理解。