1. 程式人生 > 其它 >6.s081 Lab6 cow

6.s081 Lab6 cow

Lab: Copy-on-Write Fork for xv6

當shell處理指令時, 會先通過fork建立一個子程序, 子程序的第一件事就是呼叫exec來執行一些其他程式, exec會將之前複製的地址空間丟棄這會造成資源的浪費. 所以建立子程序時, 與其建立, 分配並複製內容到新的實體記憶體, 不如直接和父程序共享實體記憶體(通過設定子程序的pte只想父程序對應的實體記憶體page). 但由於子程序和父程序是獨立的, 所以當修改子程序記憶體時, 不能影響父程序. 所以將pte設定為只讀的.

當父程序或子程序試圖修改記憶體時, 會page fault(因為向一個只讀pte裡寫資料). page fault後, handler需要複製相應的物理page到新分配的實體記憶體page中, 該page只對子程序地址空間可見, 所以可以將pte設為讀寫, 然後重新執行write

指令. 由於該實體記憶體此時只對父程序可見, 所以父程序相應的pte為讀寫.

當發生page fault時, 核心需要能夠分辨是一個copy-on-write fork的場景. 所以在cow時, 要在pte標誌位新增一個PTE_COW的標誌來表明發生page fault時, 是因為cow.

釋放相應的物理page也要小心, 因為要判斷是否能夠立即釋放. 如果有其他程序正在使用物理page, 而釋放了該page就會出現問題. 所以需要對每一個物理page的引用計數.

Implement copy-on write (hard)

在xv6核心實現cow fork, 通過cowtest和usertests.

  1. 為每個物理頁維護一個reference count, 指明有多少pte指向該頁.

    struct {
      struct spinlock lock;
    	int ref_count[PHYSTOP/PGSIZE];
    } count;
    
  2. ref_count初始化, 使其每個元素為0. 初始化函式kinit會呼叫freerange, freerange會呼叫kfree, 但kfree之後會被修改, 所以重新寫一個用於初始化的kfree_init函式, 用於初始化ref_count.

    void
    freerange(void *pa_start, void *pa_end)
    {
      char *pa;
      p = (char*)PGROUNDUP((uint64)pa_start);
      for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
        kfree_init(p);
    }
    
    void
    kfree_init(void *pa)
    {
      struct run *r;
    
      if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
        panic("kfree");
    
      // 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;
    	count.ref_count[(uint64)pa / PGSIZE] = 0;
      release(&kmem.lock);
    }
    
  3. 每次呼叫kalloc, 說明該物理頁第一次被使用, 所以將相應物理頁的ref_count設定為1.

    void *
    kalloc(void)
    {
      struct run *r;
    
      acquire(&kmem.lock);
      r = kmem.freelist;
      if(r) {
        kmem.freelist = r->next;
    		count.ref_count[(uint64)r/PGSIZE] = 1;
    	}
      release(&kmem.lock);
    
      if(r)
        memset((char*)r, 5, PGSIZE); // fill with junk
      return (void*)r;
    }
    
  4. 由於其他函式會呼叫kfree, 由於cow的存在, 一個物理頁可能有多個虛擬頁對映著, 當呼叫kfree時, 需要判定, 當該物理頁的ref_count為1時, 才會清除該物理頁. 每次呼叫kfree都會對該物理頁的ref_count減1.

    void
    kfree(void *pa)
    {
      struct run *r;
    
      if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
        panic("kfree");
    
      // 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;
    */
    	if ((--count.ref_count[(uint64)pa / PGSIZE]) == 0) {
    	  memset(pa, 1, PGSIZE);
    		r->next = kmem.freelist;
    		kmem.freelist = r;
    	}
      release(&kmem.lock);
    }
    
  5. 最後再在kalloc.c中新增一個inc函式, 每次將虛擬頁對映到物理頁時都需要將該物理頁的ref_count加1.

    void
    inc(uint64 pa)
    {
      acquire(&count.lock);
    	count.ref_count[pa/PGSIZE]++;
    	release(&count.lock);
    }
    
  6. 由於fork呼叫的是uvmcopy函式來建立子程序, 所以需要修改uvmcopy函式, 使其不實際分配實體記憶體, 而是將子程序的虛擬頁對映到父程序的物理頁上, 並將該物理頁的ref_count加1. 當父程序的pte是可寫的時候, 需要清除父子pte的PTE_W, 並給父子pte設定PTE_COW.

    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");
        pa = PTE2PA(*pte);
    		flags = PTE_FLAGS(*pte);
    		if (flags & PTE_W) {
    		  *pte = (*pte & ~PTE_W) | PTE_COW;
    		  flags = PTE_FLAGS(*pte);
    		}
    		inc(pa);
    		if (mappages(new, i, PGSIZE, (uint64)pa, flags) != 0) {
    		  goto err;
    		}
    /*
        if((mem = kalloc()) == 0)
          goto err;
        memmove(mem, (char*)pa, PGSIZE);
        if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
          kfree(mem);
          goto err;
        }
    */
    
      }
      return 0;
    
     err:
      uvmunmap(new, 0, i / PGSIZE, 1);
      return -1;
    }
    
  7. 寫一個用於處理cow頁未分配實體記憶體的函式cowalloc, 該函式通過頁表和虛擬地址va來獲取相應的ptepa, 並將pa複製到新分配的物理頁mem上, 此時pte是可寫的, 並且不是一個cow頁.

    int
    cowalloc(pagetable_t pagetable, uint64 va)
    {
      pte_t *pte;
    	uint64 pa;
      struct proc *p = myproc();
    
      if (va >= MAXVA) {
    	  return -1;
    	}
    	if (va <= PGROUNDDOWN(p->trapframe->sp) && va >= PGROUNDDOWN(p->trapframe->sp) - PGSIZE) {
    	  return -1;
    	}
    
    	va = PGROUNDDOWN(va);
    
    //	if (va > p->sz || va < PGROUNDDOWN(p->trapframe->sp)) {
    //	  return -1;
    //	}
    	if ((pte = walk(pagetable, va, 0)) == 0) {
    	  return -1;
    	}
    	if ((*pte & PTE_V) == 0) {
    	  return -1;
    	}
    	if ((*pte & PTE_COW) == 0) {
    	  return -1;
    	}
    	if ((pa = PTE2PA(*pte)) == 0) {
    		return -1;
    	}
      char *mem = kalloc();
    	if (mem == 0)
    		return -1;
    	memmove(mem, (char *) pa, PGSIZE);
    	kfree((void *) pa);
    	uint flags = (PTE_FLAGS(*pte) | PTE_W) & (~PTE_COW);
    	*pte = PA2PTE((uint64) mem) | flags;
    	return 0;
    }
    
  8. 最後在copyoutusertrap中呼叫該函式來處理cow頁.

    int
    copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
    {
      uint64 n, va0, pa0;
    
      while(len > 0){
        va0 = PGROUNDDOWN(dstva);
    
    		cowalloc(pagetable, va0);
    
        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;
    }
    
    void 
    usertrap(void)
    {
    ...
      else if (r_scause() == 13 || r_scause() == 15) {
        uint64 va = r_stval();
        if (cowalloc(p->pagetable, va) < 0)
          p->killed = 1;
      }
    }