XV6學習(9)Lab cow: Copy-on-write fork
阿新 • • 發佈:2021-01-31
程式碼在[github](https://github.com/weijunji/xv6-6.S081/tree/cow)上。總體來說如果理解了COW機制的話,這個實驗的完成也沒有很複雜。
這一個實驗是要完成COW(copy on write)fork。在原始的XV6中,fork函式是通過直接對程序的地址空間完整地複製一份來實現的。但是,拷貝整個地址空間是十分耗時的,並且在很多情況下,程式立即呼叫`exec`函式來替換掉地址空間,導致`fork`做了很多無用功。即使不呼叫`exec`函式,父程序和子程序的程式碼段等只讀段也是可以共享的,從而達到節省記憶體空間的目的。同時COW也可以將地址空間拷貝的耗時進行延遲分散,提高作業系統的效率。
首先就是要對`fork`函式進行修改,使其不對地址空間進行拷貝。`fork`函式會呼叫`uvmcopy`進行拷貝,因此只需要修改`uvmcopy`函式就可以了:刪去`uvmcopy`中的`kalloc`函式,將父子程序頁面的頁表項都設定為不可寫,並設定COW標誌位(在頁表項中保留了2位給作業系統,這裡用的是第8位`#define PTE_COW (1L << 8)`)
```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);
flags = PTE_FLAGS(*pte);
*pte = ((*pte) & (~PTE_W)) | PTE_COW; // set parent's page unwritable
// printf("c: %p %p %p\n", i, ((flags & (~PTE_W)) | PTE_COW), *pte);
// map child's page with page unwritable
if(mappages(new, i, PGSIZE, (uint64)pa, (flags & (~PTE_W)) | PTE_COW) != 0){
goto err;
}
refcnt_incr(pa, 1);
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
```
之後設定一個數組用於儲存記憶體頁面的引用計數,由於會涉及到並行的問題,因此也需要設定一個鎖,同時定義了一些輔助函式:
```c
struct {
struct spinlock lock;
uint counter[(PHYSTOP - KERNBASE) / PGSIZE];
} refcnt;
inline
uint64
pgindex(uint64 pa){
return (pa - KERNBASE) / PGSIZE;
}
inline
void
acquire_refcnt(){
acquire(&refcnt.lock);
}
inline
void
release_refcnt(){
release(&refcnt.lock);
}
void
refcnt_setter(uint64 pa, int n){
refcnt.counter[pgindex((uint64)pa)] = n;
}
inline
uint
refcnt_getter(uint64 pa){
return refcnt.counter[pgindex(pa)];
}
void
refcnt_incr(uint64 pa, int n){
acquire(&refcnt.lock);
refcnt.counter[pgindex(pa)] += n;
release(&refcnt.lock);
}
```
修改`kfree`函式,使其只有在引用計數為1的時候釋放頁面,其他時候就只減少引用計數:
```c
void
kfree(void *pa)
{
struct run *r;
// page with refcnt > 1 should not be freed
acquire_refcnt();
if(refcnt.counter[pgindex((uint64)pa)] > 1){
refcnt.counter[pgindex((uint64)pa)] -= 1;
release_refcnt();
return;
}
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
refcnt.counter[pgindex((uint64)pa)] = 0;
release_refcnt();
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
```
修改`kalloc`函式,使其在分配頁面時將引用計數也設定為1:這裡注意要判斷`r`是否為0,`kalloc`實現時沒有當`r==0`時就返回。
```c
void *
kalloc(void)
{
...
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
if(r)
refcnt_incr((uint64)r, 1); // set refcnt to 1
return (void*)r;
}
```
在`usertrap`中加入判斷語句,這裡只需要處理`scause==15`的情況,因為13是頁面讀錯誤,而COW是不會引起讀錯誤的。
```c
void
usertrap(void)
{
...
} else if(r_scause() == 15){
// page write fault
uint64 va = r_stval();
if(cowcopy(va) == -1){
p->killed = 1;
}
} else if((which_dev = devintr()) != 0){
...
}
```
在`cowcopy`函式中先判斷COW標誌位,當該頁面是COW頁面時,就可以根據引用計數來進行處理。如果計數大於1,那麼就需要通過`kalloc`申請一個新頁面,然後拷貝內容,之後對該頁面進行對映,對映的時候清除COW標誌位,設定`PTE_W`標誌位;而如果引用計數等於1,那麼就不需要申請新頁面,只需要對這個頁面的標誌位進行修改就可以了:
```c
int
cowcopy(uint64 va){
va = PGROUNDDOWN(va);
pagetable_t p = myproc()->pagetable;
pte_t* pte = walk(p, va, 0);
uint64 pa = PTE2PA(*pte);
uint flags = PTE_FLAGS(*pte);
if(!(flags & PTE_COW)){
printf("not cow\n");
return -2; // not cow page
}
acquire_refcnt();
uint ref = refcnt_getter(pa);
if(ref > 1){
// ref > 1, alloc a new page
char* mem = kalloc_nolock();
if(mem == 0)
goto bad;
memmove(mem, (char*)pa, PGSIZE);
if(mappages(p, va, PGSIZE, (uint64)mem, (flags & (~PTE_COW)) | PTE_W) != 0){
kfree(mem);
goto bad;
}
refcnt_setter(pa, ref - 1);
}else{
// ref = 1, use this page directly
*pte = ((*pte) & (~PTE_COW)) | PTE_W;
}
release_refcnt();
return 0;
bad:
release_refcnt();
return -1;
}
```
在對引用計數進行讀寫時注意鎖的設定。在`mappages`函式中會觸發一個remap的panic,這裡只要註釋掉就行了,因為COW就是要對頁面進行重新映