# COW

cow 就是 copy on write 的简称,当进程通过 fork 创建子进程时候,因为子进程和父进程的数据还有文件描述符等数据结构都是一样的,所以需要把父进程的所有数据拷贝到子进程,就必须申请和父进程相同的内存。但是比如说 sh 进程,它首先 fork 生成了一个子进程,子进程接着去执行 exec,exec 会清除进程数据然后 load 新的数据和指令。原来 fork 操作的数据拷贝就显得浪费了,所以就有了 cow 技术。

image-20220428161229686

当 fork 的时候并不执行新的内存申请,而是页表项映射到 parent 的实地址中。这样我们就省下了申请新的空间并拷贝数据的开销,但是这样也有一个问题,那就是如果子进程虽然由父进程 fork 得到,但是他们之间应该存在数据隔离,如果修改了父进程的数据子进程不应该察觉到。因此我们需要把父进程和子进程的页表项的 flag 同时设定为只读。

当子进程或父进程需要修改这些 cow 内存页那么就会触发 page fault,然后 trap 会重新分配一个空闲页,并把数据拷贝进去。这样就完成了 cow。

这里还有一个问题,我们应该如何回收这些 cow 页。xv6 给出的办法是维护一个数组,内容为内存页的 reference 个数。但 ref 为 0 时候就可以回收这个 cow 页。

# riscv.h 添加宏

#define PTE_COW  (1L << 8) //cow flag,用于表示该页表项为 cow 页表项
#define PA2PGREF_ID(pa) (((pa) - KERNBASE)/PGSIZE) // 该 physical address 对应 ref 数组的索引
#define REF_MAX PA2PGREF_ID(PHYSTOP) //ref 数据长度

# kalloc.c 代码

struct page_ref { //ref 数组元素数据结构
  int cnt;
  struct spinlock lock;
};
struct page_ref page_ref_list[REF_MAX]; //ref 数组
int
ref_page(uint64 pa, int i){ // 修改 ref, 并返回修改后的 ref 数
  acquire(&page_ref_list[PA2PGREF_ID((uint64)pa)].lock);
  page_ref_list[PA2PGREF_ID((uint64)pa)].cnt += i;
  release(&page_ref_list[PA2PGREF_ID((uint64)pa)].lock);
  return page_ref_list[PA2PGREF_ID((uint64)pa)].cnt;
}
void
kinit()
{
  for(int i=0; i<REF_MAX; ++i){ // 初始化 ref 为 1, 这里初始化为 0 会报错可能 xv6 内核还可以通过其他方式分配内存?
    initlock(&(page_ref_list[i].lock), "kpage_ref");
    page_ref_list[i].cnt =1;
  }
  ...
}
void
kfree(void *pa)
{
  ...
  int ref = ref_page((uint64)pa, -1);
  if(ref > 0){
    return;
  }
  ...
}
void *
kalloc(void)
{
  ...
  if(r){
    memset((char*)r, 5, PGSIZE); // fill with junk
    acquire(&page_ref_list[PA2PGREF_ID((uint64)r)].lock);
    page_ref_list[PA2PGREF_ID((uint64)r)].cnt =1; // 分配的时候设置为 1
    release(&page_ref_list[PA2PGREF_ID((uint64)r)].lock);
  }
  ...
}

# trap.c 代码

void
usertrap(void)
{
  ...
  }else if(r_scause() == 13 || r_scause()==15){
    //cow
    uint64 addr = r_stval();
    if(cow_check(p->pagetable, addr)){
      if(cow(addr)==0){
        p->killed = 1; 
      }
    }else{
      p->killed = 1;
    }
  }else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }
}
extern pte_t* walk(pagetable_t pagetable, uint64 va, int alloc);
uint64 cow(uint64 addr){
  pte_t* pte;
  uint64 va = PGROUNDDOWN(addr);
  struct proc* p = myproc();
  pte = walk(p->pagetable, addr, 0);
  uint64 pa = PTE2PA((uint64)*pte);
  char* mem = kalloc();
  if(mem == 0){
    return 0;
  }else{ 
    memset(mem, 0, PGSIZE);
    memmove((void*)mem, (void*)pa, PGSIZE);
    *pte = *pte & ~PTE_V; // 申请该页表项无效,防止 remap
    uint64 flag = PTE_FLAGS(*pte);
    flag = flag | PTE_W;
    flag = flag & (~PTE_COW);
    if(mappages(p->pagetable, va ,PGSIZE , (uint64)mem, flag)!=0){
      kfree((void*)mem);
      return 0;
    }
  }
  kfree((void*)PGROUNDDOWN(pa));
  /* panic("cow"); */
  return (uint64)mem;
}
int cow_check(pagetable_t pagetable, uint64 va){ // 检查是否为有效 cow
    if (va > MAXVA) {
    return 0;
  }
  pte_t *pte = walk(pagetable, va, 0);
  if (pte == 0) {
    return 0;
  }
  if (( (*pte) & PTE_V) == 0) {
    return 0;
  }
  return ((*pte) & (PTE_COW));
}

# vm.c 代码

int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  /* char *mem; */
  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");
    *pte = (*pte|PTE_COW) & ~(PTE_W); // 只是将该页表项设置为 cow 页表项,并且设置不可写。
    pa = PTE2PA(*pte);
    ref_page((uint64)pa, 1);
    flags = PTE_FLAGS(*pte);
    /* if((mem = kalloc()) == ) */
    /*   goto err; */
    /* memmove(mem, (char*)pa, PGSIZE); */
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      /* kfree(mem); */
      goto err;
    }
  }
  return 0;
 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;
  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);
    if(cow_check(pagetable, va0)){ // 
      /* panic("copy"); */
      pa0 = cow(va0);
    }
    if(pa0 == 0)
      return -1;
    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);
    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }
  return 0;
}

这里 copyout 需要修改,但用户的虚地址指向的是 cow 地址,说明这个地址不能写,那么为什么这里不能让 trap 来处理呢?因为我们只是修改了 usertrap 函数来处理用户态的缺页中断,而这里 copy_out 是发生在内核态的,而 kerneltrap 并没有处理缺页中断的函数,因此需要在 copyout 这里手动进行修改。

至此,整个实验已经全部结束了,难度真的高。。。

# 结果

image-20220428163903522

# 参考资料

MIT 6.s081 xv6-lab6-cow - 大尾巴羊的文章 - 知乎 https://zhuanlan.zhihu.com/p/429821940

更新于

请我喝[茶]~( ̄▽ ̄)~*

Kalice 微信支付

微信支付

Kalice 支付宝

支付宝