mit6.s081 lab6 Copy-on-Write Fork for xv6

270 阅读3分钟
Implement copy-on write

copy-on-write fork需要解决什么问题? xv6中的fork()调用需要拷贝所有父进程中的user space memory给子进程,如果user space memory中的空间很大,那么这个拷贝的过程需要消耗非常多的时间。而且这个拷贝的动作很多时候都是浪费的,因为在fork()后往往还会调用exec(),之前所拷贝的user space memory需要被丢弃掉。 为了解决以上问题,需要实现copy-on-write fork。在COW fork中将会推迟对物理内存的分配和拷贝操作,一开始只是创建一个pagetable给子进程,该pagetable指向父进程的物理页,COW fork会将父进程和子进程中的pte标记为不可写的,当其中的一个进程试图对物理页进行写操作室,CPU就会发生page fault,kernel中的page fault handler会对page fault进行处理,handler会为发生page fault的process分配一个物理页,并将原来的物理页中的内容拷贝到新的物理页,然后修改发生page fault的process中的pte指向新分配的物理页,并将pte标记为可写入的。物理页的删除需要通过一个引用计数来记录有多少个process正在共享当前的物理页,当引用数量为0时,才真正地释放该物理页。 为了实现cow fork需要对xv6进行以下修改: 1.修改uvmcopy()中的代码,复制页表时不再分配新的物理页,而是使子进程中的pte指向父进程的物理页,清除父进程和子进程中的pte中的PTE_W位。

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");
    pa = PTE2PA(*pte);
    
    // 关闭写入位
    *pte &= (~PTE_W);
    flags = PTE_FLAGS(*pte);
    
    // new process指向old process的物理页
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      goto err;
    }
    incref((void*)pa);
  }
  return 0;

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

2.修改usertrap中的代码,增加对page fault的处理,在处理page fault时,只需要对写入发生的page fault进行处理,对应的scause为15。当发生page fault时应该使用kalloc进行物理内存的分配,并将之前物理页的内容拷贝到新的物理页上,然后对新物理页的pte设置PTE_W位。在cowfault_handler中实现处理page fault的逻辑,我们并不需要对所有的vm都进行处理,异常的vm包括大于MAXVA的,tampoline和trapframe,这些页面都没有设置PTE_U位,所以根据PTE_U位可以判断该页面是否是要处理的cow页。

int cowfault_handler(uint64 va) {
  if (va >= MAXVA)
    return -1;

  pte_t* pte = walk(myproc()->pagetable, va, 0);

  if (pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0)
    return -1;

  uint64 cow_pa = PTE2PA(*pte);
  uint64 cow_flags = PTE_FLAGS(*pte);

  void* mem = kalloc(); 
  if (mem == 0) {
    return -1;
  }

  memmove(mem, (void*)cow_pa, PGSIZE);
  *pte = PA2PTE(mem) | cow_flags | PTE_W;
  kfree((void*)cow_pa);
  
  return 0;
}
void
usertrap(void)
{
....
else if (r_scause() == 15) {
    if (cowfault_handler(r_stval()) != 0) 
      myproc()->killed = 1;
  }
.... 

3.对物理页的引用计数进行正确的维护,当kalloc()时将新的物理的引用计数设为1,当进程调用fork()时对物理页的引用计数增加1,当进程中的pte不指向物理页时引用计数减1,当调用kfree()时只有引用计数为0时才将物理页放回free list。物理页的引用计数可以通过一个数组来维护,通过物理页的地址除以4096可以获取物理页对应的下标,通过这个下标可以获取到该物理页对应的引用计数。 首先在kalloc.c中对引用计数数组进行声明,数量为PYSTOP / PGSIZE

int refcount[PHYSTOP / PGSIZE];

对于kfree(),主要工作就是将对应物理页的引用计数减1,如果引用计数为0释放该物理页。由于refcount可能同时被多个进程同时使用,所以需要上锁

void
kfree(void *pa)
{
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  acquire(&kmem.lock);
  int page_idx = (uint64)pa / PGSIZE;
  int cur_ref = --refcount[page_idx];
  release(&kmem.lock);
  if (cur_ref > 0)
    return;
  
  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run*)pa;

  acquire(&kmem.lock);
  r->next = kmem.freelist;
  kmem.freelist = r;
  release(&kmem.lock);
}

我们另外还需要增加一个对物理页增加引用计数的函数incref(),在xv6中唯一可能发生物理页引用计数的场景就是在fork()时调用uvmcopy(),所以在uvmcopy中对vm拷贝完后需要调用incref增加对应物理页的引用计数

void incref(void *pa) {
  acquire(&kmem.lock);
  int page_idx = (uint64)pa / PGSIZE;
  refcount[page_idx]++;
  release(&kmem.lock);
}
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){
....      
    // new process指向old process的物理页
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      goto err;
    }
    incref((void*)pa);
  }
...
}

4.修改copyout()中的代码,如果发生对COW page的写入动作需要和处理page fault保持相同的动作。我们通过pte中的PTE_W位来识别该页是否为COW页,如果该页不是异常页且PTE_W位为0,那么该页是COW页。

int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);

    if (va0 >= MAXVA)
      return -1;

    pte_t* pte = walk(pagetable, va0, 0);

    if (pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0)
      return -1;

    // cow fault
    if ((*pte & PTE_W) == 0) {
      uint64 cow_pa = PTE2PA(*pte);
      uint64 cow_flags = PTE_FLAGS(*pte);

      void* mem = kalloc(); 
      if (mem == 0) {
        return -1;
      }

      memmove(mem, (void*)cow_pa, PGSIZE);
      *pte = PA2PTE(mem) | cow_flags | PTE_W;
      kfree((void*)cow_pa);
    }

    pa0 = walkaddr(pagetable, 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;
}

请添加图片描述