1. 程式人生 > 其它 >5. Lab: Copy-on-Write Fork for xv6

5. Lab: Copy-on-Write Fork for xv6

https://pdos.csail.mit.edu/6.S081/2021/labs/cow.html

1. 要求

Your task is to implement copy-on-write fork in the xv6 kernel. You are done if your modified kernel executes both the cowtest and usertests programs successfully.

Here's a reasonable plan of attack.

  1. Modify uvmcopy() to map the parent's physical pages into the child, instead of allocating new pages. Clear PTE_W in the PTEs of both child and parent.
  2. Modify usertrap() to recognize page faults. When a page-fault occurs on a COW page, allocate a new page with kalloc(), copy the old page to the new page, and install the new page in the PTE with PTE_W set.
  3. Ensure that each physical page is freed when the last PTE reference to it goes away -- but not before. A good way to do this is to keep, for each physical page, a "reference count" of the number of user page tables that refer to that page. Set a page's reference count to one when kalloc() allocates it. Increment a page's reference count when fork causes a child to share the page, and decrement a page's count each time any process drops the page from its page table. kfree() should only place a page back on the free list if its reference count is zero. It's OK to to keep these counts in a fixed-size array of integers. You'll have to work out a scheme for how to index the array and how to choose its size. For example, you could index the array with the page's physical address divided by 4096, and give the array a number of elements equal to highest physical address of any page placed on the free list by kinit() in kalloc.c.
  4. Modify copyout() to use the same scheme as page faults when it encounters a COW page.

Some hints:

  • The lazy page allocation lab has likely made you familiar with much of the xv6 kernel code that's relevant for copy-on-write. However, you should not base this lab on your lazy allocation solution; instead, please start with a fresh copy of xv6 as directed above.
  • It may be useful to have a way to record, for each PTE, whether it is a COW mapping. You can use the RSW (reserved for software) bits in the RISC-V PTE for this.
  • usertests explores scenarios that cowtest does not test, so don't forget to check that all tests pass for both.
  • Some helpful macros and definitions for page table flags are at the end of kernel/riscv.h.
  • If a COW page fault occurs and there's no free memory, the process should be killed.

簡單來說就是實現寫時複製。
傳統的fork()系統呼叫直接把所有的資源複製給新建立的程序。這種實現過於簡單並且效率低下,因為它拷貝的資料也許並不共享,更糟的情況是,如果新程序打算立即執行一個新的映像,那麼所有的拷貝都將前功盡棄。Linux 的 fork() 使用寫時拷貝(copy-on-write)頁實現。寫時拷貝是一種可以推遲甚至免除拷貝資料的技術。核心此時並不複製整個程序地址空間,而是讓父程序和子程序共享同一個拷貝。只有在需要寫入的時候,資料才會被複制,從而使各個程序擁有各自的拷貝。也就是說,資源的複製只有在需要寫入的時候才進行,在此之前,只是以只讀方式共享。

2. 分析

需要修改的點有如下:

  • uvmcopy() 的功能是拷貝父程序的頁表給子程序,此處需要把拷貝操作替換成,子程序和父程序都對映同一個頁面,並且將頁屬性 PTE_W 置為 0,這樣寫頁面時,可以觸發缺頁異常,進而處理資料複製。
  • 觸發 pagefault 的時候,可以在 usertrap() 處理缺頁異常,通過 r_scause() 獲取中斷號,如果 15 表示為寫異常。當觸發寫異常時,需要判斷這個異常頁是否為通過 fork 操作特意取消掉 PTE_W 許可權,以便確定該操作是需要進行 copy-on-write ,而不是錯誤。頁的屬性中有 2 位保留位,RSW 位。可以供我們使用作於標記。
  • 由於單個物理頁可能會有多個虛擬頁進行對映,因此,在釋放程序的記憶體時,需要判斷對應的頁是否還有其它程序在佔用。可以通過增加引用計數,kalloc() 分配記憶體的時候,增加引用技術,kfree() 釋放記憶體時,減少引用計數,當計數為 0 時,釋放該物理頁。
  • 由於核心和使用者程序使用的不是同一個頁表,當有資料要拷貝到使用者程序時,通常利用 copyout 介面,該介面在核心執行時,會先根據使用者頁表和目標虛擬地址,翻譯出該虛擬地址的實際實體地址,由於核心的記憶體空間是直接對映(虛擬地址 == 實體地址),因此核心可以直接將資料 copy 到該翻譯出來的實體地址上。但是此處需要考慮到該目標虛擬地址可能是不可寫的,因此 copyout 需要復刻下 usertrap() 處理缺頁異常時的操作。

3. 實現

3.1 初始化引用計數

由於 xv6 初始化記憶體時,使用了 kfree 介面,因此 reset_page_ref() 初始化時會將引用技術先置為 1。其次引用計數對應的引用陣列大小,參考 xv6 記憶體佈局。記憶體只到 PHYSTOP,約 128GB 記憶體。

void reset_page_ref();

struct {
  struct spinlock lock;
  int ref[(PHYSTOP - KERNBASE) / PGSIZE];
} page_ref;

void kinit()
{
  initlock(&kmem.lock, "kmem");
  reset_page_ref();
  freerange(end, (void*)PHYSTOP);
}

void reset_page_ref()
{
  int cnt = sizeof(page_ref.ref) / sizeof(int);
  printf("cnt = %d\n", cnt);
  for (int i = 0; i < cnt; i++)
  {
    page_ref.ref[i] = 1;
  }
}

int get_pa_index(uint64 pa)
{
  return ((pa & ~(PGSIZE - 1)) - KERNBASE) / PGSIZE;
}

void inc_page_ref(uint64 pa)
{
  acquire(&page_ref.lock);
  int idx = get_pa_index(pa);
  page_ref.ref[idx] += 1;
  release(&page_ref.lock);
}

void dec_page_ref(uint64 pa)
{
  acquire(&page_ref.lock);
  int idx = get_pa_index(pa);
  page_ref.ref[idx] -= 1;
  release(&page_ref.lock);
}

int get_ref_cnt(uint64 pa)
{
  int idx = get_pa_index(pa);
  return page_ref.ref[idx];
}

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

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

  acquire(&kmem.lock);
  int ref_cnt = get_ref_cnt((uint64)pa);
  if (ref_cnt == 0){
    release(&kmem.lock);
    panic("ref cnt == 0"); // release page double times
  }

  if(ref_cnt == 1){
    // Fill with junk to catch dangling refs.
    memset(pa, 1, PGSIZE);

    r = (struct run*)pa;

    r->next = kmem.freelist;
    kmem.freelist = r;
  }
  
  dec_page_ref((uint64)pa);
  release(&kmem.lock);
}

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void * kalloc(void)
{
  struct run *r;

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

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}

3.2 處理 fork 拷貝

這裡需要注意幾點:

  • 當頁是可寫許可權時,父程序的寫許可權也要去除,如果不去除會導致,fork 之後,父程序修改某個資料,而子程序會依舊同步到該資料。此外還要加上 PTE_RSW 許可權,用於標誌該頁是否需要被拷貝。
  • 對映完頁面之後,需要增加引用計數
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);
    int flags = PTE_FLAGS(*pte); // copy flag , remove write permission and add rsw flag
    if (flags & PTE_W){
      flags = (flags & (~PTE_W)) | PTE_RSW;
      if(mappages(old, i, PGSIZE, (uint64)pa, flags) != 0){ // modify old page attr
        goto err;
      }
    }

    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      goto err;
    }

    inc_page_ref(pa);
  }
  return 0;

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

3.3 處理缺頁異常

這裡需要注意如下:

  • 檢查是否有 PTE_RSW 位,如果沒有表示該異常不是通過 copy-on-write 操作引發的,需要 kill 掉該程序。
  • 一些常規的邊界檢查
  • 從舊頁面拷貝到新頁面時,需要整頁拷貝,因為分配新頁面時,裡面都是垃圾資料,如果只拷貝修改的資料,會導致其他資料異常。
  • 拷貝完畢,建立完新頁面和程序頁表的對映之後,還需要做如下操作:
    • 執行釋放舊物理頁的操作,因為程序不再使用該頁面了,需要減少引用計數
    • 清除 PTE_RSW 位,因為此時已不在需要 copy-on-write 操作了,可以直接寫入。
void usertrap(void)
{
  // ... some code
  else if (r_scause() == 15){ // write page fault
    uint64 va = r_stval();
    if(va >= MAXVA || (va <= PGROUNDDOWN(p->trapframe->sp) && va >= PGROUNDDOWN(p->trapframe->sp) - PGSIZE)){
        p->killed = 1;
    } else {
      if (pagefault(p->pagetable, va) < 0)
        p->killed = 1;
    }
  }
  // ... some code 
}

// vm.c
int pagefault(pagetable_t pagetable, uint64 fault_va)
{
  pte_t* pte = walk(pagetable, fault_va, 0);
  if ((*pte & PTE_RSW) == 0)
    return -1;

  // step 1 : copy origin page
  uint64 fault_pa = walkaddr(pagetable, fault_va);
  void* dst_pa = kalloc();
  if (dst_pa == 0){
    return -1;
  }
  memmove(dst_pa, (void*)fault_pa, PGSIZE);

  // step 2 : copy page flag and allow write
  int flag = (PTE_FLAGS(*pte) | PTE_W) & ~PTE_RSW;  // remove rsw flag
  *pte = PA2PTE(dst_pa) | flag;
  //printf("page fault, stval=%x\n", fault_va);

  kfree((void*)fault_pa);
  return 0;
}

4. 小結

  • 該實驗思路整體較為簡單,但是需要注意一些邊界條件,防止執行 usertests 的時候不通過
  • 需要對地址翻譯對映過程比較瞭解,可以參考 lab3-pgtbl 的預備內容。