6.s081 Lab6 cow
Lab: Copy-on-Write Fork for xv6
當shell處理指令時, 會先通過fork
建立一個子程序, 子程序的第一件事就是呼叫exec
來執行一些其他程式, exec
會將之前複製的地址空間丟棄這會造成資源的浪費. 所以建立子程序時, 與其建立, 分配並複製內容到新的實體記憶體, 不如直接和父程序共享實體記憶體(通過設定子程序的pte只想父程序對應的實體記憶體page). 但由於子程序和父程序是獨立的, 所以當修改子程序記憶體時, 不能影響父程序. 所以將pte設定為只讀的.
當父程序或子程序試圖修改記憶體時, 會page fault(因為向一個只讀pte裡寫資料). page fault後, handler需要複製相應的物理page到新分配的實體記憶體page中, 該page只對子程序地址空間可見, 所以可以將pte設為讀寫, 然後重新執行write
當發生page fault時, 核心需要能夠分辨是一個copy-on-write fork的場景. 所以在cow時, 要在pte標誌位新增一個PTE_COW
的標誌來表明發生page fault時, 是因為cow.
釋放相應的物理page也要小心, 因為要判斷是否能夠立即釋放. 如果有其他程序正在使用物理page, 而釋放了該page就會出現問題. 所以需要對每一個物理page的引用計數.
Implement copy-on write (hard)
在xv6核心實現cow fork, 通過cowtest和usertests.
-
為每個物理頁維護一個reference count, 指明有多少pte指向該頁.
struct { struct spinlock lock; int ref_count[PHYSTOP/PGSIZE]; } count;
-
對
ref_count
初始化, 使其每個元素為0. 初始化函式kinit
會呼叫freerange
,freerange
會呼叫kfree
, 但kfree
之後會被修改, 所以重新寫一個用於初始化的kfree_init
函式, 用於初始化ref_count
.void freerange(void *pa_start, void *pa_end) { char *pa; p = (char*)PGROUNDUP((uint64)pa_start); for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE) kfree_init(p); } void kfree_init(void *pa) { struct run *r; if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) panic("kfree"); // 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; count.ref_count[(uint64)pa / PGSIZE] = 0; release(&kmem.lock); }
-
每次呼叫
kalloc
, 說明該物理頁第一次被使用, 所以將相應物理頁的ref_count
設定為1.void * kalloc(void) { struct run *r; acquire(&kmem.lock); r = kmem.freelist; if(r) { kmem.freelist = r->next; count.ref_count[(uint64)r/PGSIZE] = 1; } release(&kmem.lock); if(r) memset((char*)r, 5, PGSIZE); // fill with junk return (void*)r; }
-
由於其他函式會呼叫
kfre
e, 由於cow的存在, 一個物理頁可能有多個虛擬頁對映著, 當呼叫kfree
時, 需要判定, 當該物理頁的ref_count
為1時, 才會清除該物理頁. 每次呼叫kfree
都會對該物理頁的ref_count
減1.void kfree(void *pa) { struct run *r; if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) panic("kfree"); // 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; */ if ((--count.ref_count[(uint64)pa / PGSIZE]) == 0) { memset(pa, 1, PGSIZE); r->next = kmem.freelist; kmem.freelist = r; } release(&kmem.lock); }
-
最後再在
kalloc.c
中新增一個inc函式, 每次將虛擬頁對映到物理頁時都需要將該物理頁的ref_count
加1.void inc(uint64 pa) { acquire(&count.lock); count.ref_count[pa/PGSIZE]++; release(&count.lock); }
-
由於fork呼叫的是
uvmcopy
函式來建立子程序, 所以需要修改uvmcopy
函式, 使其不實際分配實體記憶體, 而是將子程序的虛擬頁對映到父程序的物理頁上, 並將該物理頁的ref_count
加1. 當父程序的pte是可寫的時候, 需要清除父子pte的PTE_W
, 並給父子pte設定PTE_COW
.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); flags = PTE_FLAGS(*pte); if (flags & PTE_W) { *pte = (*pte & ~PTE_W) | PTE_COW; flags = PTE_FLAGS(*pte); } inc(pa); if (mappages(new, i, PGSIZE, (uint64)pa, flags) != 0) { goto err; } /* if((mem = kalloc()) == 0) goto err; memmove(mem, (char*)pa, PGSIZE); if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){ kfree(mem); goto err; } */ } return 0; err: uvmunmap(new, 0, i / PGSIZE, 1); return -1; }
-
寫一個用於處理cow頁未分配實體記憶體的函式
cowalloc
, 該函式通過頁表和虛擬地址va
來獲取相應的pte
和pa
, 並將pa
複製到新分配的物理頁mem
上, 此時pte是可寫的, 並且不是一個cow頁.int cowalloc(pagetable_t pagetable, uint64 va) { pte_t *pte; uint64 pa; struct proc *p = myproc(); if (va >= MAXVA) { return -1; } if (va <= PGROUNDDOWN(p->trapframe->sp) && va >= PGROUNDDOWN(p->trapframe->sp) - PGSIZE) { return -1; } va = PGROUNDDOWN(va); // if (va > p->sz || va < PGROUNDDOWN(p->trapframe->sp)) { // return -1; // } if ((pte = walk(pagetable, va, 0)) == 0) { return -1; } if ((*pte & PTE_V) == 0) { return -1; } if ((*pte & PTE_COW) == 0) { return -1; } if ((pa = PTE2PA(*pte)) == 0) { return -1; } char *mem = kalloc(); if (mem == 0) return -1; memmove(mem, (char *) pa, PGSIZE); kfree((void *) pa); uint flags = (PTE_FLAGS(*pte) | PTE_W) & (~PTE_COW); *pte = PA2PTE((uint64) mem) | flags; return 0; }
-
最後在
copyout
和usertrap
中呼叫該函式來處理cow頁.int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len) { uint64 n, va0, pa0; while(len > 0){ va0 = PGROUNDDOWN(dstva); cowalloc(pagetable, va0); pa0 = walkaddr(pagetable, va0); if(pa0 == 0) return -1; 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; } void usertrap(void) { ... else if (r_scause() == 13 || r_scause() == 15) { uint64 va = r_stval(); if (cowalloc(p->pagetable, va) < 0) p->killed = 1; } }