MIT-6.S081-2020實驗(xv6-riscv64)六:cow


實驗文檔

概述

這次實驗實現copy on write功能,和上次實驗一樣也是缺頁中斷的應用,但不同的是,這次實驗涉及的物理內存和虛擬地址的操作要比上個實驗多不少,因此難度也更大一些。

內容

首先是uvmcopy的部分,原來的操作是從老頁表中獲得虛擬地址對應的物理地址,創建一個新物理頁,然后將老物理地址的內容復制到新物理頁,再把新物理頁通過新頁表映射到虛擬地址,現在就要改成直接將老物理地址通過新頁表映射到虛擬地址,同時需要將老頁表和新頁表對應底層pte抹去PTE_W為並添加PTE_C位,這里的PTE_C是我自己定義的一個標志位。根據Riscv的標准,pte的低10位作為標志位,其中的0-7位是包括PTE_V、PTE_W之類已經被用掉的標志位,8-9位是可供用戶自定義使用的標志位,這里我選取第8位,即PTE_C = 1L << 8。0表示該pte沒有用在copy on write中,1表示有,這樣在處理缺頁中斷的時候就比較方便了,只要該pte的PTE_C位為0,說明這次缺頁中斷的原因不是copy on write,而是真的缺頁,就可以直接返回錯誤:

  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_C;
    flags = PTE_FLAGS(*pte);
    // if((mem = kalloc()) == 0)
    //   goto err;
    // memmove(mem, (char*)pa, PGSIZE);
    if(mappages(new, i, PGSIZE, pa, flags) != 0) goto err;
    add_count(pa);
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  panic("uvmcopy: map page failed");
  return -1;

這里我映射失敗就直接讓程序panic了,因為如果真的要處理的話還得把之前所有新老頁表里的底層pte的標志位改回去,事實上我也想不出mappages失敗且內部沒有panic的情況。

然后是缺頁中斷處理,和上次實驗一樣,單獨抽象成一個函數,主要過程就是獲取缺頁的物理地址,如果物理地址不存在或者PTE_C不為1就返回錯誤,然后創建新頁,把老頁的內容復制過來,並修改新頁對應pte的標志位,注意下一步需要嘗試釋放老頁,防止內存泄漏。這里的“嘗試釋放”指的是讓老頁的引用計數-1,如果引用計數為0了就真的釋放。“嘗試釋放”的過程直接就寫在kfree函數里面,因為加入copy on write機制后,所有對物理內存的操作都需要受引用計數的制約:

int handle_page(uint64 va, pagetable_t pgtbl) {
    pte_t *pte; char *mem; uint flags;
    if ((pte = walk(pgtbl, va, 0)) == 0) return -1;
    if ((*pte & PTE_C) == 0) return -1;
    if ((mem = kalloc()) == 0) return -1;
    flags = PTE_FLAGS((*pte & (~PTE_C)) | PTE_W);
    uint64 pa = PTE2PA(*pte);
    memmove(mem, (char*)pa, PGSIZE);
    *pte = PA2PTE((uint64)mem) | flags;
    kfree((void *)pa); return 0;
}

然后就是copyout函數的修改,為什么不需要修改copyin和copyinstr函數呢,因為fork涉及的都是用戶區的內存,所以缺頁也只會在寫用戶內存的情況下發生,copyout是內核內存寫到用戶內存,所以需要處理,另外兩個函數是用戶內存寫到內核內存,是讀用戶內存,所以不需要處理。另一個和上次實驗不同的地方是,上次實驗之所以copyout函數需要修改,是因為在對虛擬地址調用walkaddr函數的時候,因為實際的物理地址不存在,所以返回錯誤,因此只要在walkaddr返回不存在的物理地址時進行缺頁處理即可;而這次實驗walkaddr是可以得到合法的物理地址的,只是這個物理地址不能被寫,所以錯誤會在memmove到這個物理地址的時候才發生,而且這個缺頁中斷是在內核態發生的,走的也是kerneltrap函數,因此我們定義在usertrap函數里的處理代碼捕獲不到它。因此我們要做的,就是在調用walkaddr函數后對pte進行檢查,如果PTE_C位為1,則進行缺頁處理。

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    if (*(walk(pagetable, va0, 0)) & PTE_C) handle_page(va0, pagetable);
    pa0 = walkaddr(pagetable, va0);
    ......

這里我的代碼寫的比較矬,為了檢查標志位還重新walk一遍,最后要獲得新物理地址又walkaddr一遍,實際上可以定義另一個版本的walkaddr直接返回pte,handle_page也可以改寫讓其返回新物理地址,后面有時間再改。trap.c的代碼和上次實驗幾乎一樣,就不貼了。

然后是物理內存的處理,為了節省空間,我沒有直接用物理地址模4096,而是先將物理地址減掉內核的地址空間,再模4096,因為fork不涉及物理內存,即數組索引為(pa - KERNBASE) >> PGSHIFT,當然代價就是每次進行處理引用計數的時候需要先判斷pa必須大於等於KERNBASE,不然內核申請或釋放物理內存的時候一減變成負數,就訪問非法內存了。數組大小就可以根據memlayout.h里的值進行計算,發現物理內存的最大值PHYSTOP減KERNBASE等於128*1024*1024`,因此總頁數為128*1024/4=32768,這就是數組的大小。另外很重要的一點是引用數組的聲明:

struct {
    struct spinlock lock;
    uint a[32768];
} count;

需要用到鎖,這個實驗文檔沒講,略坑,我也是看了別人的代碼才知道,不用鎖的話會內存泄漏,應該是多進程競爭擾亂了引用計數的加減,目前還沒看到xv6文檔里關於鎖的部分,所以也不知道哪些地方可能產生資源競爭。了解這一點后面就很容易了,kfree函數:

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

  if ((uint64)pa >= KERNBASE) {
      acquire(&count.lock);
      if (count.a[((uint64)pa - KERNBASE) >> PGSHIFT] > 1) {
          count.a[((uint64)pa - KERNBASE) >> PGSHIFT]--;
          release(&count.lock); return;
      } else {
          count.a[((uint64)pa - KERNBASE) >> PGSHIFT] = 0;
          release(&count.lock);
      }
  }

alloc函數里直接在返回物理地址前使用add_count函數讓計數加1(初始時和釋放后引用計數都為0,所以加1后就是1),這里代碼不貼了,add_count函數:

void add_count(uint64 pa) {
  if (pa >= KERNBASE) {
      acquire(&count.lock);
      count.a[(pa - KERNBASE) >> PGSHIFT]++;
      release(&count.lock);
  }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM