代碼在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
將堆大小增大,然后注釋掉growproc
。if(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.
當進程通過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
判斷,當地址小於棧頂地址時就說明發生了非法訪問。