代碼在github上。git
這一個實驗是要利用缺頁異常來實現懶分配(lazy allocation)。用戶態程序經過sbrk
系統調用來在堆上分配內存,而sbrk
則會經過kalloc
函數來申請內存頁面,以後將頁面映射到頁表當中。github
當申請小的空間時,上述過程是沒有問題的。可是若是當進程一次申請很大的空間,如數GB的空間,再使用上述策略來一頁頁地申請映射的話就會很是的慢(1GB/4KB=262,144)。這時候就引入了lazy allocation技術,當調用sbrk
時不進行頁面的申請映射,而是僅僅增大堆的大小,當實際訪問頁面時,就會觸發缺頁異常,此時再申請一個頁面並映射到頁表中,這是再次執行觸發缺頁異常的代碼就能夠正常讀寫內存了。app
經過lazy allocation技術,就能夠將申請頁面的開銷平攤到讀寫內存當中去,在sbrk
中進行大量內存頁面申請的開銷是不能夠接受的,可是將代價平攤到讀寫操做當中去就能夠接受了。函數
整體來講這一個實驗的難度並不大,理解了上一個trap的實驗以及缺頁異常就能比較輕鬆地完成了。ui
這一個就是要修改sbrk
函數,使其不調用growproc
函數進行頁面分配,關鍵就是p->sz += n
將堆大小增大,而後註釋掉growproc
。if(n < 0)
是後面部分的內容。this
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:當系統發生缺頁異常時,就會進入到usertrap
函數中,此時scause
寄存器保存的是異常緣由(13爲page load fault,15爲page write fault),stval
是引起缺頁異常的地址。code
在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; }
這一部分就是要強化上面寫的的lazy allocation,使其能在一些特殊狀況下工做。get
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.
當進程經過read
或write
等系統調用訪問未分配頁面的地址時,並不會經過頁表硬件來訪問,也就是說不會發生缺頁異常;在內核態時是經過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
判斷,當地址小於棧頂地址時就說明發生了非法訪問。