6.S081-2021-lab5 Copy-on-Write Fork
阿新 • • 發佈:2022-04-20
Copy-on-Write Fork
主要根據hins
來一步一步修改。cow的思想是在fork的時候,子程序與父程序共享物理頁,當需要修改頁面內容的時候才會真正分配自己的頁表空間,也就是 lazy allocation
cow使得多個va
對映到了同一個pa
上,所以 free 的時候我們要特別小心,因為可能別的程序還依賴這個物理頁,所以我們需要對每一個 pa 對應的物理頁進行引用計數。
如何計數呢?雖然硬體為作業系統保留了三個位元位讓其自由發揮,但很明顯這點位置是不夠應用計數的,只適合用其中一位來表示當前頁是否是 cow 頁。
但我們知道 xv6 能夠使用的最大實體地址,以及頁表大小。所以只需要定義一個大小為 PGPYHA / PGSIZE 的一維陣列來記錄每一頁的引用數即可
// riscv.h
#define PTE_COW (1L << 8) // copy on write flag
// kalloc.c
int refcount[PHYSTOP/PGSIZE];
struct spinlock reflock;
kalloc 和 kfree 會導致引用計數的增加和減少
// kalloc.c 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); } void * kalloc(void) { struct run *r; acquire(&kmem.lock); r = kmem.freelist; if(r){ kmem.freelist = r->next; int pn = (uint64)r / PGSIZE; acquire(&reflock); if(refcount[pn]!=0){ panic("kalloc ref"); } refcount[pn] = 1; release(&reflock); } release(&kmem.lock); if(r) memset((char*)r, 5, PGSIZE); // fill with junk return (void*)r; } void kfree(void *pa) { ... 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); // 計數不為0說明還有程序需要這一頁,不能free if(tmp>0){ return; } // Fill with junk to catch dangling refs. ... }
同時我們需要在 kinit 中初始化引用計數陣列為1。因為 freerange 會呼叫 kfree,會導致陣列變為負數,丟擲 painc 。
// kalloc.c void kinit() { initlock(&kmem.lock, "kmem"); char *p; p = (char*)PGROUNDUP((uint64)end); // 將引用計數陣列初始化為1 for(; p + PGSIZE <= (char*)PHYSTOP; p += PGSIZE){ refcount[(uint64)p/ PGSIZE] = 1; } freerange(end, (void*)PHYSTOP); }
因為 fork 使用了 uvmcopy 所以我們要修改這個函式,讓子程序的地址空間延遲分配,先與父程序共享相同的物理頁
// vm.c
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
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);
// 將父親和孩子都置為防寫,並標識該頁為cow
*pte &= ~PTE_W;
*pte |= PTE_COW;
flags = PTE_FLAGS(*pte);
//引用計數++
incref(pa);
//直接把pa給孩子的頁表項,也就是說現在父子的va都對應父親的pa
if(mappages(new, i, PGSIZE, pa, flags) != 0){
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
因為我們將PTE_W抹去了,所以當程序需要進行寫操作的時候,會發生 page fault。這時候需要在 usertrap 中處理,為子程序分配新的實體地址。
// trap.c
// 為cow page分配新頁
int
cowfault(pagetable_t pagetable, uint64 va)
{
if(va >= MAXVA){
return -1;
}
pte_t *pte;
pte = walk(pagetable,va,0);
if(pte == 0 ) return -1;
if ((*pte & PTE_U) == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_COW) == 0){
return -1;
}
uint64 pa1,pa2;
pa1 = PTE2PA(*pte);
pa2 = (uint64)kalloc();
if(pa2 == 0){
return -1;
}
memmove((char*)pa2,(char*)pa1,PGSIZE);
// 只有當引用計數為0的時候才會真正free
kfree((void*)pa1);
uint flags = PTE_FLAGS(*pte);
*pte = PA2PTE(pa2) | flags | PTE_W;
*pte &= ~PTE_COW;
return 0;
}
void
usertrap(void)
{
...
else if((which_dev = devintr()) != 0){
// ok
}else if(r_scause()==15 || r_scause()==13){ // 處理page fault
if(cowfault(p->pagetable,r_stval())<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;
}
...
}
最後需要注意 copyout 將 kernel 的實體地址複製給使用者程序,但沒有校驗 PTE_W 和 PTE_COW,所以有可能直接覆蓋了防寫的COW頁。 而這一操作不經過 MMU ,沒辦法引發 page fault ,從而在 usertrap 中處理。所以我們需要修改 copyout 函式,處理的辦法跟在 usertrap 中一樣,為使用者程序分配一個新頁,以供寫入覆蓋。
// vm.c
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
pte_t *pte = walk(pagetable,va0,0);
if(pte == 0 || (*pte & PTE_V )==0 || (*pte & PTE_U) ==0){
return -1;
}
// 如果防寫,並且是因為COW造成的
if((*pte & PTE_W) == 0 && (*pte && PTE_COW) == 1){
// 為當前虛擬地址分配一個新的實體地址以供寫入
if(cowfault(pagetable,va0)<0){
return -1;
}
}
pa0 = PTE2PA(*pte);
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;
}