

1.uvmcopy
根據提示,首先要修改uvmcopy() ,把子進程的虛擬地址映射到父進程的物理頁表上,同時清理父子進程頁表上的PTE_W位。這里還是比較簡單的,先根據要求,嘗試一下寫出代碼:

獲取父進程每頁對應的PTE表項,清除掉父進程的PTE_W位,提取出它的flag,在給子進程的虛擬地址建立頁表映射的時候使用這個flag,這樣子進程的各個PTE和父進程就相同了。
2.usertrap
然后根據提示,我們需要修改usertrap() 函數,當遇到page fault時要為子進程新分配一個頁並更新映射。
首先要明確幾點:當發生page fault時,引發page fault的地址存儲在寄存器stval中,引發pagefault的原因存儲在寄存器scause中,引發page fault的指令地址在sepc中。因為父子進程已經是共用數據了,因此使用if(r_scause()==15)來判斷是不是出現了page fault。(反正是只用15沒問題)嘗試實現處理COW操作的函數(不是最終版):

首先記得做好錯誤檢測,如果傳入了非法地址要及時退出。make grade是真的會測試非法地址的!然后使用walk()得到指向stval所在頁表的指針,如果得到的不是有效地址也要及時返回。使用kalloc()分配一個頁,其地址賦值給pa2,page fault頁的物理地址賦值給pa1。注意kalloc()不能保證一定能分配出一塊內存,所以要記得檢測pa2是不是0,如果分配不出來內存要退出。
最后使用memmove直接復制本頁所有內容,然后使用PA2PTE宏來創建頁表項,並打開所有的PTE位,存入剛剛walk()得到的內存地址中,這樣就覆蓋了原來的表項。
3.reference count
在COW中,會出現大量的多個虛擬地址對應同一個物理頁表的情況。如果某一個進程亂釋放物理頁表,會導致其他進程的頁表也被釋放。所以需要對每一個頁表都設置引用計數。初始狀態計數默認為1。調用kfree()釋放內存時,先把計數減去1,再檢查計數,如果為0,說明這個頁表已經是徹底沒用了,可以直接釋放。(這就像ext文件系統里的inode一樣)
按照提示,在kalloc.c里初始化一個數組int refcount[PHYSTOP/PGSIZE],初始化為0。PHYSTOP是xv6可用的全部內存值,因此refcount存儲了所有頁表的計數。
修改kalloc(),在這里初始化計數為1:
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
{
int pn=(uint64)r/PGSIZE;
if(refcount[pn]!=0)
panic("ref kalloc");
refcount[pn]=1;
kmem.freelist = r->next;
}
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
記得錯誤檢查:如果計數不為0,說明這個空閑頁表被引用,必然是出現了嚴重的bug,直接打出panic。還要注意,refcount顯然是所有進程共有的,為了避免出現race condition,一定要在鎖中操作。
修改kfree():
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
acquire(&kmem.lock);
int pn=(uint64)pa/PGSIZE;
if(refcount[pn]<1)
panic("kfree ref");
refcount[pn]-=1;
int tmp=refcount[pn];
release(&kmem.lock);
if(tmp>0)
return;
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
只要有函數調用kfree(),就說明需要對頁表pa的refcount減去1。注意錯誤處理:如果refcount已經是0,說明系統出現了多次釋放同一塊內存的bug。減完之后再檢測一下refcount是不是0,如果是0則釋放即可。記得訪問refcount的時候要加鎖。
現在make qemu一下,xv6竟然不能boot,為什么呢?因為系統在boot時調用了kinit()初始化內存,kinit()調用freerange(),而freerange()使用了kfree()來清空內存。這時的refcount是0,已經是空閑頁了,kfree()無法釋放它。因此需要修改freerange():
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
{
refcount[(uint64)p/PGSIZE]=1;
kfree(p);
}
}
4.使用reference count
每當一個進程fork()一次,它每個頁表的reference count都應該加1,因為有一個子進程使用了它的物理頁表。因此可以設計一個incref函數用來給refcount+1:
void incref(uint64 pa)
{
int pn=pa/PGSIZE;
acquire(&kmem.lock);
if(pa>PHYSTOP||refcount[pn]<1)
panic("incref");
refcount[pn]+=1;
release(&kmem.lock);
}
修改uvmcopy(),調用incref(pa),每mappage一次都需要增加一次計數:
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
//char *mem;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
*pte=(*pte)&~PTE_W;
flags = PTE_FLAGS(*pte);
// if((mem = kalloc()) == 0)
// goto err;
// memmove(mem, (char*)pa, PGSIZE);
incref(pa);
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
//kfree(mem);
goto err;
}
}
return 0;
現在如果直接執行cowtest的話會出現內存不足的bug。因為在COW操作完成之后,子進程不再和父進程共用發生過page fault的頁表,因此父進程中原來的頁表引用需要減去1。還要修改cowfault(),對父進程的頁表進行一次kfree()操作來減少一次引用計數:
int cowfault(pagetable_t pagetable,uint64 va)
{
if(va>=MAXVA)
return -1;
pte_t *pte=walk(pagetable,va,0);
if(pte==0)
return -1;
//檢測地址是否合法
if((*pte&PTE_U)==0||(*pte&PTE_V)==0)
return -1;
uint64 pa1=PTE2PA(*pte);
uint64 pa2=(uint64)kalloc();
if(pa2==0)
{
printf("kalloc failed\n");
return -1;
}
memmove((void*)pa2,(void*)pa1,PGSIZE);
*pte=PA2PTE(pa2)|PTE_V|PTE_U|PTE_R|PTE_W|PTE_X;
kfree((void*)pa1);
return 0;
}
5.copyout
按照提示,我們需要給copyout()增加COW的功能。首先要獲取dstva所在頁表的虛擬地址va0和表項pte。如果此頁表的PTE_W是無效的不允許寫入,則需要COW創建一個新的頁表給子進程寫入。有了前面的代碼,我們可以直接調用cowfault為va0分配一個新的獨立頁。
這時va0對應的表項已經被更新,對應的物理地址已經被改變,所以使用PTE2PA獲取新分配的物理地址,稍后傳給memmove()。
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
if(va0>=MAXVA)
return -1;
pa0 = walkaddr(pagetable, va0);
pte_t* pgfault=walk(pagetable,va0,0);
if(pgfault==0||(*pgfault&PTE_V)==0||(*pgfault&PTE_U)==0)
return -1;
if(pa0 == 0)
return -1;
if((*pgfault&PTE_W)==0)
{
if(cowfault(pagetable,va0)<0)
{
return -1;
}
}
pa0=PTE2PA(*pgfault);
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}
這里還是要注意錯誤處理!注意va0是否合法,pgfault是否存在,如果存在PTE_U和PTE_V是否合法,cowfault是否能正常返回。