1. 程式人生 > 實用技巧 >MIT-6.S081-2020實驗(xv6-riscv64)五:lazy

MIT-6.S081-2020實驗(xv6-riscv64)五:lazy

實驗文件

概述

這次實驗主要實現Lazy allocation的功能,即程序在動態分配記憶體的時候先不分配,等到要用到發生缺頁中斷的時候再實際分配,核心是實現缺頁中斷的處理。xv6的文件介紹了三種缺頁中斷的應用,第一為Copy on write,即fork的時候先不復制記憶體,等到要用到發生缺頁中斷的時候再實際分配;第二為硬碟虛擬記憶體,就是當記憶體不夠大的時候將一部分硬碟區域當作記憶體交換區,虛擬地址只對映到一個無效位置,當訪問該虛擬地址發生缺頁中斷時再把一個頁的內容儲存進磁碟,然後從磁碟中載入當前這個虛擬地址指向的實際內容;第三就是本實驗的內容。

內容

Eliminate allocation from sbrk()

這個任務非常簡單,沒啥好說的:

uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  struct proc *p = myproc();
  addr = p->sz;
  if (n < 0) p->sz = uvmdealloc(p->pagetable, p->sz, p->sz + n);
  else p->sz += n;
  // if(growproc(n) < 0)
  //   return -1;
  return addr;
}

對n小於0情況的處理是第三個任務的內容,這裡可以忽略。

Lazy allocation

這個任務要求實現對缺頁中斷的處理,因為在sbrk的時候僅僅指擴大了程序的虛擬地址區域,所以在訪問這些虛擬地址時會發生缺頁中斷,這裡就需要在發生缺頁中斷的時候分配實體記憶體然後對映,中斷處理函式usertrap()對缺頁中斷進行處理:

	......
    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
      if (r_scause() == 13 || r_scause() == 15) {
          uint64 va = r_stval(); if (handle_page(va, p) == -1) 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;
      }
  }
  if(p->killed)
    exit(-1);

這裡把缺頁中斷的實際處理過程抽象成了一個函式,實際上僅從任務2考慮是沒有必要的,但是任務3中還需要對copyin、copyout這些函式中發生缺頁的情況進行處理,所以抽象成一個函式方便各處呼叫。

handle_page函式我寫在proc.c裡,因為這裡已經包含了所需要的標頭檔案:

int handle_page(uint64 va, struct proc *p) {
    uint64 base =  PGROUNDDOWN(va);
    if (va >= p->sz || va < p->trapframe->sp) return -1;
    char *mem = kalloc();
    if (mem == 0) return -1;
    memset(mem, 0, PGSIZE);
    if(mappages(p->pagetable, base, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) {
        kfree(mem); return -1;
    }
    return 0;
}

這些return -1的情況也是任務3的內容,任務2可以忽略,主要都是借鑑函式uvmalloc。然後修改一下uvmunmap(),即把一些因為缺頁導致的panic跳掉了,因為這些頁從來就沒分配過,也就不用釋放:

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

Lazytests and Usertests

這個任務主要是把上個任務遺留的一些不合法情況進行處理。

第一是sbrk的引數為負數的問題,根據growproc函式的內容,對引數為負數的情況就是釋放參數絕對值大小的記憶體,仿造growproc()就行了,見上面的程式碼。uvmdealloc本身不用修改,因為內部就是呼叫uvmunmap的。

第二是缺頁中斷中當虛擬地址不合法時應該直接返回並殺掉程序,不合法包含兩種情況,一是虛擬地址太大,大出了程序所申請的記憶體(不管實際有沒有分配),因為程序虛擬地址從0開始,所以只要保證虛擬地址小於p->sz即可;而是虛擬地址太小,比程序的棧頂還低(注意棧是從高往低增長的),這就需要知道棧頂的位置,檢視測試程式usertests,發現它獲取棧頂的方法就是讀sp暫存器,但是缺頁中斷的處理是在核心態,sp指向的也是核心棧的棧頂,想要獲得使用者棧的棧頂,可以藉助程序的中斷幀來實現,即讀取p->trapframe->sp,需要保證虛擬地址大於等於這個值。殺掉程序可以觀察usertrap函式的其他位置,發現只要令p->killed=1即可,見上面的程式碼。

第三是如果申請實體記憶體失敗時也要殺掉程序,加上對映失敗,照著uvmalloc裡寫就行了。

第四是fork的時候複製到缺頁的虛擬地址時的處理,注意到fork的這部分是呼叫的uvmcopy,所以改uvmcopy,和uvmunmap一樣,缺頁導致的panic跳掉:

  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0) continue;
      // panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0) continue;
      // panic("uvmcopy: page not present");
    ......

第五是read和write檔案的時候如果傳入了一個缺頁的虛擬地址(在將檔案讀入記憶體和將記憶體寫入檔案時需要傳入地址),追蹤這兩個函式的過程可以發現最終處理地址呼叫的是copyin、copyinstr和copyout函式,注意到這幾個函式會先walk一下傳入的虛擬地址,如果得不到實體地址就直接返回失敗,而不會經過缺頁中斷的過程,所以直接加入程式碼讓其在判斷得不到實體地址的情況下呼叫handle_page函式即可:

    va0 = PGROUNDDOWN(srcva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0) {
        if (handle_page(va0, myproc()) == -1) return -1; else pa0 = walkaddr(pagetable, va0);
      // return -1;
    }
	......

總結一下,缺頁中斷的發生時刻應該是在MMU訪問到一個PTE_V位為0的PTE時,在xv6中這個PTE的其他位是沒有意義的,而在riscv-pk(用在spike模擬器上的代理核心)則讓PTE的其他位指向一個標記結構體,裡面包含了這個缺頁的資訊,比如該缺頁是否是因為記憶體被置換到硬碟上了,置換到了哪個位置等資訊,這樣就使得該系統可以處理多種原因導致的缺頁中斷,而xv6應該是不支援硬碟虛擬記憶體的。