概述
這次實驗主要實現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應該是不支持硬盤虛擬內存的。