MIT xv6 2020系列實驗:Lab6 Copy-on-Write Fork

Thaudmin發表於2024-11-24

實驗六:fork懶更新頁表。

這次最佳化的內容是針對fork時記憶體複製的最佳化。在fork後,子程序很可能只用到了父程序中記憶體資源的一小部分,但是卻完整地複製了父程序的記憶體,除那一小部分,剩下的資源都被浪費掉了,憑空增加了開銷。

這就很不合理,我們結合提示想想應該怎麼減少不必要的開銷。有了實驗五懶更新的基礎,實驗六應該還是挺好做的,只需要將懶更新的思路應用到頁表上,對於一個程序,在fork時我們將子程序頁表的葉子節點與其共享,並將其修改為只讀,當讀取到只讀頁,核心陷入頁表異常,我們就在頁表異常處將該頁對應的物理資源正式複製一份可寫頁表給子程序。

前置trick:pte中8~9位是保留位,這次把第8位作為fork懶複製的標誌位,記為PTE_COW。

順藤摸瓜,首先修改fork中的uvmcopy:

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

  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;
    *pte |= PTE_COW;

    if(mappages(new, i, PGSIZE, (uint64) pa, PTE_FLAGS(*pte)) != 0){
      goto err;
    }
  }
  return 0;

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

將物理頁的屬性修改為只讀,標上懶更新標記。

copy有了,考慮釋放程序時只讀標記的變化,當物理頁沒有引用時,就應該刪除他。

為此建立一個結構體記錄每個物理頁的引用:

struct {
  struct spinlock lock;
  int mem_map[(PHYSTOP - KERNBASE) >> PGSHIFT];
}kernel_tagger;

在對映與解對映時都對引用進行加減操作:

int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
  uint64 a, last;
  pte_t *pte;

  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + size - 1);
  for(;;){
    if((pte = walk(pagetable, a, 1)) == 0)
      return -1;
    if(*pte & PTE_V)
      panic("remap");
    if(perm & PTE_COW){
      acquire(&kernel_tagger.lock);
      if (kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] == 0)
        kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] += 2;
      else
        kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] += 1;
      release(&kernel_tagger.lock);
    }
    *pte = PA2PTE(pa) | perm | PTE_V;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
  uint64 a;
  pte_t *pte;

  if((va % PGSIZE) != 0)
    panic("uvmunmap: not aligned");

  for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
    if((pte = walk(pagetable, a, 0)) == 0)
      panic("uvmunmap: walk");
    if((*pte & PTE_V) == 0)
      panic("uvmunmap: not mapped");
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");

    uint64 pa = PTE2PA(*pte);
    if(do_free){
      if (*pte & PTE_COW) {
        acquire(&kernel_tagger.lock);
        if (--kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] == 0)
          kfree((void*)pa);
        release(&kernel_tagger.lock);
      } else
        kfree((void*)pa);
    }
    *pte = 0;
  }
}

在一個頁面被複制且標記數為0時,我們才釋放它,如果沒有被複制過,就不考慮標記數。

接下來在trap中增加複製的部分:

...
  } else if((which_dev = devintr()) != 0){
    // ok
  } else if (r_scause() == 15) {
    if (uvmmapfork(p->pagetable, PGROUNDDOWN(r_stval())) != 0)
      p->killed = 1;
  } 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;
  }
...

如果有頁面異常,透過呼叫重分配函式給虛擬地址重新分配一個物理頁表,複製出原物理頁。

複製函式如下:

int
uvmmapfork(pagetable_t pagetable, uint64 va){

  pte_t *pte;

  if((va % PGSIZE) != 0)
    panic("uvmmapfork: not aligned");
  
  if(va >= MAXVA)
    return -1;
  
  if((pte = walk(pagetable, va, 0)) == 0)
    panic("uvmmapfork: walk");
  if((*pte & PTE_V) == 0)
    panic("uvmmapfork: not mapped");
  if((*pte & PTE_COW) == 0)
    panic("uvmmapfork: not copy-on-write");
  if(PTE_FLAGS(*pte) == PTE_V)
    panic("uvmmapfork: not a leaf");
  if((*pte & PTE_U) == 0)
    return -1;

  uint64 pa = PTE2PA(*pte);
  acquire(&kernel_tagger.lock);
  char *mem;
  if (kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] > 1) {
    if((mem = kalloc()) == 0)
      goto err;
    memmove(mem, (void*)pa, PGSIZE);
  } else if (kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] == 1) {
    mem = (char *)pa;
  } else
    goto err;
  uint64 perm = PTE_FLAGS(*pte);
  perm &= ~PTE_COW;
  perm |= PTE_W;
  *pte = PA2PTE(mem) | perm;
  kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT]--;
  release(&kernel_tagger.lock);
  return 0;

err:
  release(&kernel_tagger.lock);
  return -1;
}

uvm的部分都完成了,還有copyout需要修改,這是一個大坑,因為是核心修改使用者頁,不會經過使用者頁表合法標記位的審查,所以這裡對copyout的每一頁都要進行地址重分配。

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)
      return -1;
    if((*pte & PTE_V) == 0)
      return -1;
    if((*pte & PTE_COW) && (uvmmapfork(pagetable, va0) != 0))
      return -1;
    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;
}

結束,編譯!本次實驗告一段落

相關文章