XV6學習(7)Lab lazy


代碼在github上。

這一個實驗是要利用缺頁異常來實現懶分配(lazy allocation)。用戶態程序通過sbrk系統調用來在堆上分配內存,而sbrk則會通過kalloc函數來申請內存頁面,之后將頁面映射到頁表當中。

當申請小的空間時,上述過程是沒有問題的。但是如果當進程一次申請很大的空間,如數GB的空間,再使用上述策略來一頁頁地申請映射的話就會非常的慢(1GB/4KB=262,144)。這時候就引入了lazy allocation技術,當調用sbrk時不進行頁面的申請映射,而是僅僅增大堆的大小,當實際訪問頁面時,就會觸發缺頁異常,此時再申請一個頁面並映射到頁表中,這是再次執行觸發缺頁異常的代碼就可以正常讀寫內存了。

通過lazy allocation技術,就可以將申請頁面的開銷平攤到讀寫內存當中去,在sbrk中進行大量內存頁面申請的開銷是不可以接受的,但是將代價平攤到讀寫操作當中去就可以接受了。

總體來說這一個實驗的難度並不大,理解了上一個trap的實驗以及缺頁異常就能比較輕松地完成了。

Eliminate allocation from sbrk() (easy)

這一個就是要修改sbrk函數,使其不調用growproc函數進行頁面分配,關鍵就是p->sz += n將堆大小增大,然后注釋掉growprocif(n < 0)是后面部分的內容。

uint64
sys_sbrk(void)
{
  int addr;
  int n;
  if(argint(0, &n) < 0)
    return -1;

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

Lazy allocation (moderate)

接下來就是真正實現Lazy allocation:當系統發生缺頁異常時,就會進入到usertrap函數中,此時scause寄存器保存的是異常原因(13為page load fault,15為page write fault),stval是引發缺頁異常的地址。

usertrap判斷scause為13或15后,就可以讀取stval獲取引發異常的地址,之后調用lazy_alloc對該地址的頁面進行分配即可。在這里不需要進行p->trapframe->epc += 4操作,因為我們要返回發生異常的那條指令並重新執行。

void
usertrap(void)
{
  ...
  } else if((which_dev = devintr()) != 0){
    // ok
  } else if (r_scause() == 13 || r_scause() == 15) {
    // 13: page load fault; 15: page write fault
    // printf("page fault\n");
    uint64 addr = r_stval();
    if (lazy_alloc(addr) < 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;
  }
  ...
}

lazy_alloc函數中,首先判斷地址是否合法,之后通過PGROUNDDOWN宏獲取對應頁面的起始地址,然后調用kalloc分配頁面,memset將頁面內容置0,最后調用mappages將頁面映射到頁表中去。

int
lazy_alloc(uint64 addr) {
  struct proc *p = myproc();
  // page-faults on a virtual memory address higher than any allocated with sbrk()
  // this should be >= not > !!!
  if (addr >= p->sz) {
    // printf("lazy_alloc: access invalid address");
    return -1;
  }

  if (addr < p->trapframe->sp) {
    // printf("lazy_alloc: access address below stack");
    return -2;
  }
  
  uint64 pa = PGROUNDDOWN(addr);
  char* mem = kalloc();
  if (mem == 0) {
    // printf("lazy_alloc: kalloc failed");
    return -3;
  }
  
  memset(mem, 0, PGSIZE);
  if(mappages(p->pagetable, pa, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
    kfree(mem);
    return -4;
  }
  return 0;
}

Lazytests and Usertests (moderate)

這一部分就是要強化上面寫的的lazy allocation,使其能在一些特殊情況下工作。

Handle negative sbrk() arguments.

這一個就是在上面的sys_sbrk函數中的if(n < 0)部分,當參數為負數時,調用uvmdealloc取消分配。

Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().

這一個即lazy_alloc函數中的addr >= p->sz部分,當訪問的地址大於堆的大小時就說明訪問了非法地址,注意這里是>=而不是>

Handle the parent-to-child memory copy in fork() correctly.

fork函數中通過uvmcopy進行地址空間的拷貝,我們只要將其中panic的部分改為continue就行了,當頁表項不存在時並不是說明出了問題,直接跳過就可以了。

Handle the case in which a process passes a valid address from sbrk() to a system call such as read or write, but the memory for that address has not yet been allocated.

當進程通過readwrite等系統調用訪問未分配頁面的地址時,並不會通過頁表硬件來訪問,也就是說不會發生缺頁異常;在內核態時是通過walkaddr來訪問用戶頁表的,因此在這里也要對缺頁的情況進行處理。
當出現pte == 0 || (*pte & PTE_V) == 0時,就說明發生了缺頁,這時只要調用lazy_alloc進行分配,之后再次使用walk就能正確得到頁表項了。

uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;
  uint64 pa;

  if(va >= MAXVA)
    return 0;

  pte = walk(pagetable, va, 0);
  if(pte == 0 || (*pte & PTE_V) == 0) {
    if (lazy_alloc(va) == 0) {
      pte = walk(pagetable, va, 0);
    } else {
      return 0;
    }
  }
  if((*pte & PTE_U) == 0)
    return 0;
  pa = PTE2PA(*pte);
  return pa;
}

Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.

kalloc失敗時,lazy_alloc就會返回負值,此時判斷返回值然后p->killed = 1就行了。

Handle faults on the invalid page below the user stack.

這一個可以通過addr < p->trapframe->sp判斷,當地址小於棧頂地址時就說明發生了非法訪問。


免責聲明!

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



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