MIT 6.S081 Lab5 Copy-On-Write Fork
- 2020 年 11 月 5 日
- 筆記
前言
最近絕大多數的空閑時間都拿來錘15-445了,很久沒動6.S081。前幾天回頭看了一下一個月前錘完的Lazy Allocation,自己寫的程式碼幾乎都不認識了…….看來總結之類的東西最好還是趁著熱乎的時候寫啊。
不過15-445的內容實在太多了,我只是為了錘Lab粗略的看了看課件,課件里很多東西都沒研究,相關的總結還是推遲到所有Lab錘完後重新整理一下再寫吧。先把幾天前剛做完的Copy-On-Write給寫出來。最近會把前面草草寫下的Lab Lazy Allocation的相關內容整改一下,補上具體的Lab。
Lab5 Copy-On-Write fork的鏈接://pdos.csail.mit.edu/6.828/2019/labs/cow.html
最近xv6-riscv-2019好像被改動過了,我git clone下來的時候發現usertest中多了不少的新測試,trap.c的程式碼也被改過了。
xv6進程的結構
想要處理好這個Lab,需要我們對xv6的進程結構有一個大致的了解。而了解xv6的進程結構,最好的辦法是閱讀kernel/exec.c的程式碼。xv6的進程結構大致如上圖所示。我們下面對每個進程段進行簡要分析:
ELF LOAD 段
ELF LOAD段是我自己給這個段起的名字,你們Google查是查不到的….我實在不知道到底該怎麼稱呼這個段…..
ELF LOAD段是在調用exec時,由ELF文件載入進來的段。我們可以查看一下_sh這個ELF文件的描述:
ms@ubuntu:~/public/MIT 6.S081/Lab5 cow/xv6-riscv-fall19$ readelf -a user/_sh ELF 頭: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 類別: ELF64 數據: 2 補碼,小端序 (little endian) 版本: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 類型: EXEC (可執行文件) 系統架構: RISC-V 版本: 0x1 入口點地址: 0xa60 程式頭起點: 64 (bytes into file) Start of section headers: 39464 (bytes into file) 標誌: 0x5, RVC, double-float ABI 本頭的大小: 64 (位元組) 程式頭大小: 56 (位元組) Number of program headers: 1 節頭大小: 64 (位元組) 節頭數量: 19 字元串表索引節頭: 18 節頭: [號] 名稱 類型 地址 偏移量 大小 全體大小 旗標 鏈接 資訊 對齊 [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000078 000000000000127e 0000000000000000 WAX 0 0 2 [ 2] .rodata PROGBITS 0000000000001280 000012f8 0000000000000159 0000000000000000 A 0 0 8 [ 3] .sdata PROGBITS 00000000000013e0 00001458 000000000000000e 0000000000000000 WA 0 0 8 [ 4] .sbss NOBITS 00000000000013f0 00001466 0000000000000008 0000000000000000 WA 0 0 8 [ 5] .bss NOBITS 00000000000013f8 00001466 0000000000000078 0000000000000000 WA 0 0 8 [ 6] .comment PROGBITS 0000000000000000 00001466 0000000000000012 0000000000000001 MS 0 0 1 [ 7] .riscv.attributes LOPROC+0x3 0000000000000000 00001478 0000000000000035 0000000000000000 0 0 1 [ 8] .debug_aranges PROGBITS 0000000000000000 000014b0 00000000000000f0 0000000000000000 0 0 16 [ 9] .debug_info PROGBITS 0000000000000000 000015a0 00000000000021c2 0000000000000000 0 0 1 [10] .debug_abbrev PROGBITS 0000000000000000 00003762 00000000000006f1 0000000000000000 0 0 1 [11] .debug_line PROGBITS 0000000000000000 00003e53 0000000000002018 0000000000000000 0 0 1 [12] .debug_frame PROGBITS 0000000000000000 00005e70 0000000000000880 0000000000000000 0 0 8 [13] .debug_str PROGBITS 0000000000000000 000066f0 000000000000039f 0000000000000001 MS 0 0 1 [14] .debug_loc PROGBITS 0000000000000000 00006a8f 0000000000002386 0000000000000000 0 0 1 [15] .debug_ranges PROGBITS 0000000000000000 00008e15 0000000000000080 0000000000000000 0 0 1 [16] .symtab SYMTAB 0000000000000000 00008e98 00000000000008b8 0000000000000018 17 26 8 [17] .strtab STRTAB 0000000000000000 00009750 0000000000000218 0000000000000000 0 0 1 [18] .shstrtab STRTAB 0000000000000000 00009968 00000000000000bc 0000000000000000 0 0 1 ........
可以看出,ELF LOAD段包含了程式的程式碼段、靜態數據段、只讀數據段、全局變數段、DEBUG資訊等。exec函數一次申請一頁,然後讀取ELF文件的一頁,讀取完成後,將虛實映射關係添加到該進程的頁表中。當ELF文件讀取完後,ELF LOAD段也被載入到了進程的最低虛地址上,相應的虛實映射關係也被添加到了頁表中。
stack guard page 段
xv6作為一個教學系統,它有很多設計不合理的地方。最明顯的一點莫過於棧和堆的設置。很多OS書上告訴我們,進程的stack和heap是共用一塊空間的,但它們的增長方向相反。當其中一個逾越到另一個的空間時,即認為空間已滿,觸發overflow。但xv6的棧卻放在了低地址段,棧向下增長,堆放在了高地址段,向上增長……
吐槽完xv6,回過頭來討論這個段。因為stack是向低地址增長的,很可能會踩到ELF LOAD段不應該訪問的區域,因此可以考慮在stack的PAGE和ELF LOAD的PAGE間設立GUARD PAGE,當用戶訪問這一段的時候觸發page fault,告知用戶棧溢出。
在kernel/exec.c中相關程式碼如下:
// kernel/exec.c
// Allocate two pages at the next page boundary. // Use the second as the user stack. sz = PGROUNDUP(sz); if((sz = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0) goto bad; uvmclear(pagetable, sz-2*PGSIZE); sp = sz; stackbase = sp - PGSIZE;
// kernel/vm.c
// mark a PTE invalid for user access. // used by exec for the user stack guard page. void uvmclear(pagetable_t pagetable, uint64 va) { pte_t *pte; pte = walk(pagetable, va, 0); if(pte == 0) panic("uvmclear"); *pte &= ~PTE_U; }
uvmclear清理掉stack guard page的PTE_U位,這樣當訪問到stack guard page時,就會觸發page fault。這也是user/usertest.c下的stacktest的原理。
但是…..stack guard page真的能防止棧溢出么?
至少下面這種情況,不能。我們檢查一下載入ELF LOAD段的程式碼,它是通過調用uvmalloc函數,確立ELF LOAD段的相關頁面的虛實映射關係的。那麼ELF LOAD段的許可權是什麼?
uint64 uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz) { char *mem; uint64 a; if(newsz < oldsz) return oldsz; oldsz = PGROUNDUP(oldsz); a = oldsz; for(; a < newsz; a += PGSIZE){ mem = kalloc(); if(mem == 0){ uvmdealloc(pagetable, a, oldsz); return 0; } memset(mem, 0, PGSIZE); if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){ kfree(mem); uvmdealloc(pagetable, a, oldsz); return 0; } } return newsz; }
用戶可讀可寫可執行!如果棧溢出沒有溢出到stack guard page上,而是溢出到了.text段上,那相當於程式直接修改了程式程式碼…….(人工智慧進化難道就是這麼完成的么?)
可能有人會認為,在執行exec時,我們可以知道ELF LOAD段的結束地址,那麼把這段地址記錄到進程中,作為檢測棧溢出的條件不行么?
有時候行,但別忘了,ELF LOAD段還有全局變數段啊!這個段應該是可讀可寫的!
如果一個用戶程式沒有全局變數,那麼可以以這個條件判斷棧溢出。這也是為什麼Lab4 Lazy Allocation可以通過的原因,查看user/lazytest.c的程式碼,它是沒有全局變數的,因此程式碼不會訪問到ELF GUARD段的內容。
我們就不要再深究這其中的安全問題了,討論到這裡只是為了能幫大家理清stack guard page到底能做什麼,不能做什麼。在本實驗的測試程式碼(user/cowtest.c)中和user/usertest.c中都有對全局變數的訪問:
// part of user/cowtest.c char junk1[4096]; int fds[2]; char junk2[4096]; char buf[4096]; char junk3[4096]; // test whether copyout() simulates COW faults. void filetest() { printf("file: "); buf[0] = 99; for(int i = 0; i < 4; i++){ if(pipe(fds) != 0){ printf("pipe() failed\n"); exit(-1); ...... }
因此修改程式碼時要十分小心對越界訪問的判斷。
TRAMPOLINE段和TRAPFRAME段
這兩個段幫助進程實現trap。
先說簡單的TRAPFRAME段。這個段用於記錄進程發生trap時的現場,因此這個段必須是每個進程獨有的。trapframe的物理空間分配並不是在exec或者fork中完成,而是在allocproc中就已經完成了。某個進程執行exec時,TRAPFRAME -> &p->tf 間的映射關係由proc_pagetable添加:
// kernel/exec.c
// Check ELF header if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf)) goto bad; if(elf.magic != ELF_MAGIC) goto bad; if((pagetable = proc_pagetable(p)) == 0) // 此時TRAPFRAME -> &p->tf間的關係已經寫入到pagetable中 goto bad;
TRAMPOLINE段就比較難理解了。首先要明確,TRAMPOLINE是一個虛地址,而單獨討論虛地址是沒有意義的,只有這個虛地址存在到實地址的映射時,才有意義。這個映射在xv6的進程中是存在的,也是由proc_pagetable來完成添加,將所有進程的TRAMPOLINE段映射到了同一塊程式碼區域,這塊區域的程式碼邏輯就是/kernel/trampoline.S,即trap處理的入口。
STACK段和HEAP段
首先思考一個問題:xv6進程每個段的起始虛地址在哪裡?
ELF LOAD段的虛地址是從0開始的,TRAMPOLINE段永遠放在虛地址的最高處(MAXVA),佔據一頁,因此虛地址就是MAXVA – PGSIZE。TRAPFRAME和TRAMPOLINE是緊挨著的(它們之間有沒有guard page我忘了,當沒有吧),因此起始虛地址就是MAXVA – 2*PGSIZE。
那麼STACK段和HEAP段的起始虛地址呢?
閱讀kernel/exec.c源碼後我們可以得出結論:STACK段的起始虛地址只有在載入完ELF LOAD段後才能確定。這個地址位於stack guard page的上方一頁,而stack guard page也是在ELF LOAD段載入完後才確定的。
當ELF LOAD段、STACK段、stack guard page段、TRAMPOLINE段、TRAMFRAME段都確認後,中間那塊剩餘的區域,就是HEAP段了,也就是動態記憶體所處的段。查看一下kernel/proc.h中定義的進程的數據結構:
struct proc { struct spinlock lock; // p->lock must be held when using these: enum procstate state; // Process state struct proc *parent; // Parent process void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed int xstate; // Exit status to be returned to parent's wait int pid; // Process ID // these are private to the process, so p->lock need not be held. uint64 kstack; // Bottom of kernel stack for this process uint64 ustackbase; uint64 sz; // Size of process memory (bytes) pagetable_t pagetable; // Page table struct trapframe *tf; // data page for trampoline.S struct context context; // swtch() here to run process struct file *ofile[NOFILE]; // Open files struct inode *cwd; // Current directory char name[16]; // Process name (debugging) };
注意到sz這個變數。對於一個進程p來說,虛地址空間[0, p->sz),覆蓋了ELF LOAD段、stack guard page段、STACK段的虛地址空間,以及當前已經分配的堆空間。
調用exec結束時,p->sz的初始值就被設定為了user stack的結尾處,即當前已分配的堆空間大小為0.
Copy-On-Write fork的引入
回顧完xv6的進程結構之後,我們可以思考一下,一個fork要完成哪些操作?
首先,必須要調用allocproc,當allocproc成功返回時,TRAMPOLINE和TRAPFRAME這兩個段已經完成了初始化,因此無需處理。剩餘的四個段(HEAP、STACK、STACK GUARD、ELF LOAD)都需要拷貝一份給新的進程。
這樣我們會遇到以下問題:
(1)很多時候,我們調用fork,就是為了調用exec,而exec會釋放掉[0, p->sz)之間的所有內容,那麼[0, p->sz)這塊空間,還沒有被訪問過一次,就被我們丟掉了。
(2)即使我們調用了fork而不調用exec,對於HEAP段的數據,大多時候的操作很可能是讀操作,這個時候HEAP段完全可以和父進程共用。
(3)ELF LOAD段包含了不可寫的程式碼段(.text),至少這個段是可以父子進程共享的。
Copy-On-Write fork的引入可以解決上述問題。當發生fork時,子進程並不會將父進程的[0, p->sz)這塊空間拷貝一份留給自己,而是僅僅修改自己的進程頁表,將[0, p->sz)映射到相同的實存區域,這塊區域我們暫且稱之為F區域。當父進程或者子進程對F區域的任意一頁進行寫操作時,才複製這一頁。
分析與設計
Lab的主頁已經告訴了我們大致的做法:
Modify uvmcopy() to map the parent's physical pages into the child, instead of allocating new pages, and clear PTE_W in the PTEs of both child and parent.
Modify usertrap() to recognize page faults. When a page-fault occurs on a COW page, allocate a new page with kalloc(), copy the old page to the new page,
and install the new page in the PTE with PTE_W set.
大致的設計如下:
(0)發生pagefault的原因有很多,可能是越界訪問,也可能是訪問了F區域的頁面。為了能區分,我們需要給F區域的頁面添加一個標誌位PTE_F。若一個頁面存在PTE_F位,那麼這個頁面一定發生了cow fork。
(1)由於存在多個虛地址映射到同一個實地址的情況,因此進程在結束後釋放頁面時,如果頁面還在被其他進程所引用,那麼就不能釋放掉這個頁面。為此我們需要添加數據結構,記錄每個頁面的引用計數。
(2)修改uvmcopy程式碼:
如果一個頁面即沒有PTE_W位也沒有PTE_F位,那這個頁面必定不可寫,我們可以放心的讓本進程引用這個頁面,給它添不添加PTE_F並不會影響結果
如果一個頁面存在PTE_W位,這個頁面的引用計數必定為1,我們需要同時修改父子進程的相應的pte,清除掉它們的PTE_W位,替換成PTE_F位。
如果一個頁面存在PTE_F位,那麼就說明其它進程尚未對這個頁面執行過寫操作,新進程也可以放心的引用這個頁面,並拷貝這個頁面的許可權就可以了;
(3)在trap中添加cow_handler,通過檢查PTE_F位是否存在來判斷是否是訪問了F區域的頁面。如果是,那麼拷貝一份這個頁面,清除這個頁面的PTE_F位,重置為PTE_W位,刪除掉舊頁面的虛實映射關係,添加新頁面到該虛地址的虛實映射關係,讓就業面的引用計數減1。
(4)在usertrap中添加了cow_handler後,我們處理來自用戶態訪問F區域頁面引起的page fault。但如果用戶程式是調用write來對F區域頁面進行寫操作時,對F區域頁面的寫是在內核態完成的(sys_write)。因此需要另行處理。具體方法是修改kernel/vm.c下的copyout函數,查看用戶進程的頁表並檢查flags。
實現
(0)創立新的頁表位 PTE_F:
// kernel/riscv.h
#define PTE_V (1L << 0) // valid #define PTE_R (1L << 1) #define PTE_W (1L << 2) #define PTE_X (1L << 3) #define PTE_U (1L << 4) // 1 -> user can access #define PTE_F (1L << 8) // copy-on-write flag
(1)修改kernel/kalloc.c,為每個頁面添加引用計數。
我在做這一部分時腦子真的進水了。剛開始的想法是設計一個鏈表結構,同時記錄下頁面的物理地址和引用計數,然後調試起來非常複雜。後來查閱其他部落格才明白,頁面的物理地址本身就可以作為一個索引。修改過後調試了幾次就pass了…..良好的設計可以真的可以節省不少的時間。


extern char end[]; // first address after kernel. // defined by kernel.ld. struct run { struct run *next; }; struct { struct spinlock lock; struct run *freelist; int refcount[1 << 15]; int used; } kmem; // Allocate one 4096-byte page of physical memory. // Returns a pointer that the kernel can use. // Returns 0 if the memory cannot be allocated. int pa2pageid(void* pa); void freerange(void *pa_start, void *pa_end); void* allocpage(); void freepage(void* pa); void kinit() { initlock(&kmem.lock, "kmem"); freerange(end, (void*)PHYSTOP); memset(&kmem.refcount, 0, sizeof(kmem.refcount)); kmem.used = 0; } void freerange(void *pa_start, void *pa_end) { char *p; p = (char*)PGROUNDUP((uint64)pa_start); for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE) freepage(p); } // Free the page of physical memory pointed at by v, // which normally should have been returned by a // call to kalloc(). (The exception is when // initializing the allocator; see kinit above.) void kfree(void* pa) { if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) panic("kfree"); acquire(&kmem.lock); if(decrease_ref(pa)) { kmem.used--; freepage(pa); // printf("free page %p\n", pa); } release(&kmem.lock); } void * kalloc(void) { acquire(&kmem.lock); void* page = allocpage(); if(page) { increase_ref(page, 0); } release(&kmem.lock); return page; } int pa2pageid(void* pa) { return (pa - (void*)end) / PGSIZE; } void* allocpage() { struct run* r = kmem.freelist ? kmem.freelist : 0; if(r) { kmem.used++; kmem.freelist = r->next; memset((char*)r, 5, PGSIZE); } return r; } void freepage(void* pa) { memset(pa, 1, PGSIZE); struct run* r = (struct run*)pa; r->next = kmem.freelist; kmem.freelist = r; } void increase_ref(void* pa, int exist) { int idx = pa2pageid(pa); if(exist && kmem.refcount[idx] <= 0) { printf("page %p should exist", pa); panic("increase ref"); } else if(!exist && kmem.refcount[idx] > 0) { printf("page %p should not exist", pa); panic("increase ref"); } kmem.refcount[idx]++; } int decrease_ref(void* pa) { int idx = pa2pageid(pa); if(kmem.refcount[idx] <= 0) { printf("page %p should exist"); panic("decrease ref"); } return --kmem.refcount[idx] == 0; }
kernel/kalloc.c
(2)修改uvmcopy。fork時,僅僅修改兩個進程的頁表,而不申請新的記憶體空間:
// Given a parent process's page table, copy // its memory into a child's page table. // Copies both the page table and the // physical memory. // returns 0 on success, -1 on failure. // frees any allocated pages on failure. 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); // if PTE_W or PTE_F is set, then this page doesn't cow-fork and maybe be shared // so simply increase refcount is ok // if page could write, set PTE_F flag and clean PTE_W flag for both old and new pagetable if(*pte & PTE_F || *pte & PTE_W) { if(0 != mappages(new, i, PGSIZE, pa, (flags & (~PTE_W)) | PTE_F)) goto err; // we need also adjust page perm for parent process *pte = PA2PTE(pa) | (flags & (~PTE_W)) | PTE_F; } else { if(0 != mappages(new, i, PGSIZE, pa, flags)) goto err; } increase_ref((void*)pa, 1); } return 0; err: uvmunmap(new, 0, i, 1); // withdraw cow-fork's change on origin thread for(int j = 0; j <= i; j += PGSIZE) { pte = walk(old, j, 0); flags = PTE_FLAGS(*pte); if(flags & PTE_F) { flags = (flags & (~PTE_F)) | PTE_W; pa = PTE2PA(*pte); *pte = PA2PTE(pa) | flags; } } return -1; }
(3)在usertrap中添加cow_handler。訪問F區域時發生的trap編號仍然是13或15。
我在做這個lab時犯的另一個巨大失誤是把panic當做assert一樣胡亂使用。panic的原意是「內核不知道自己應該怎麼做」,換句話說,只有在出現「內核進入了不應該進入的狀態」的情況下,才應該使用panic。大部分情況下,我們認為的「錯誤」是用戶自己作死搞出來的,並不是內核的錯,遇到這種情況應當把這個用戶進程kill掉,而不是調用panic。
還有一個重大失誤,算是寫類似程式碼寫多了產生的誤區。我寫程式碼的時候總是習慣「return as early as possible」,即儘早返回特殊情況。這樣程式碼邏輯會清晰很多(最主要的是縮進會變少,我只要看到某段程式碼縮進超過4個心口就會隱隱作痛 →_→)。所以cow_handler剛開始的結構大概是這樣的:
int cowhandler(pagetable_t pagetable, uint64 va) { if(situation1) return -1; if(situation2) return -1; ........ finally , this should be a cow-page-fault if is not a cow-page-fault { panic("not a cow-pate-fault"); } handle it }
然後跑usertest的時候,瘋狂panic…..
思考後發現,這個cowhandler出現的條件是非常嚴格的,即相應頁面必須有PTE_F位,其他情況下都應當把這個進程kill掉,而不是內核去panic。最終修改後的程式碼如下,如果不符合cow_handler的條件,直接返回-1,把這個進程kill掉。
// kernel/vm.c
// success return 0, failed return -1 int cow_handler(pagetable_t pagetable, uint64 va) { // return 0; if(0 == pagetable) panic("no page table\n"); // out of range access if(myproc()->sz <= va) { printf("process %d , assess %d\n", myproc()->pid, (uint64)va); printf("process size %d, stack guard %d\n", myproc()->sz, myproc()->ustackbase); printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), myproc()->pid); printf(" sepc=%p stval=%p\n", r_sepc(), r_stval()); myproc()->killed = 1; return -1; } // find corresponding pte uint64 vabase = PGROUNDDOWN(va); pte_t* pte; if((pte = walk(pagetable, vabase, 0)) == 0) panic("page should exist"); // cow pagefault situation: uint flags = PTE_FLAGS(*pte); if(!(flags & PTE_F && (flags & PTE_W) == 0)) { return -1; } // allocate a new page and recalculate it's perm void* mem = kalloc(); if(0 == mem) panic("no memory avaliable"); // free origin pte and page memmove(mem, (void*)PTE2PA(*pte), PGSIZE); kfree((void*)PTE2PA(*pte)); *pte = 0; // map it flags = (flags & (~PTE_F)) | PTE_W; if(mappages(pagetable, vabase, PGSIZE, (uint64)mem, flags)) return -1; return 0; }
將這個handler添加到usertrap中:
void usertrap(void) { int which_dev = 0; if((r_sstatus() & SSTATUS_SPP) != 0) panic("usertrap: not from user mode"); // send interrupts and exceptions to kerneltrap(), // since we're now in the kernel. w_stvec((uint64)kernelvec); struct proc *p = myproc(); // save user program counter. p->tf->epc = r_sepc(); if(r_scause() == 8){ // system call if(p->killed) exit(-1); // sepc points to the ecall instruction, // but we want to return to the next instruction. p->tf->epc += 4; // an interrupt will change sstatus &c registers, // so don't enable until done with those registers. intr_on(); syscall(); } else if((which_dev = devintr()) != 0){ // ok } else if(r_scause() == 13 || r_scause() == 15) { // printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid); // printf(" sepc=%p stval=%p\n", r_sepc(), r_stval()); if(cow_handler(myproc()->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()); vmprint(p->pagetable); p->killed = 1; } if(p->killed) exit(-1); // give up the CPU if this is a timer interrupt. if(which_dev == 2) yield(); usertrapret(); }
(4)處理內核態下對F區域頁面的寫操作:
其實就是修改copyout的程式碼,添加上對flags的檢查,然後使用(3)中的cow_handler函數就可以了:
// kernel/vm.c
// Copy from kernel to user. // Copy len bytes from src to virtual address dstva in a given page table. // Return 0 on success, -1 on error. int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len) { uint64 n, va0, pa0; pte_t* pte; if(dstva > MAXVA) return -1; while(len > 0){ va0 = PGROUNDDOWN(dstva); pte = walk(pagetable, va0, 0); if(0 == pte) { panic("copyout : pte should exist"); } if(*pte & PTE_F) { if(cow_handler(pagetable, va0) != 0) { panic("copyout : handle cow failed"); } pa0 = walkaddr(pagetable, va0); } else { 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; }
還有不少瑣碎的地方…..就不貼上來了。這個Lab應該是近幾個Lab中最簡單的了。
測試全部通過:
xv6 kernel is booting virtio disk init 0 hart 2 starting hart 1 starting init: starting sh $ cowtest simple: ok simple: ok three: ok three: ok three: ok file: ok ALL COW TESTS PASSED
$ usertests usertests starting test reparent2: OK test pgbug: OK test sbrkbugs: usertrap(): unexpected scause 0x000000000000000c pid=3220 sepc=0x0000000000004430 stval=0x0000000000004430 page table 0x00000000861f5000 ..0: pte 0x0000000021882801 pa 0x000000008620a000 perm : PTE_V| .. ..0: pte 0x0000000021882c01 pa 0x000000008620b000 perm : PTE_V| ..255: pte 0x0000000021fd1001 pa 0x0000000087f44000 perm : PTE_V| .. ..511: pte 0x0000000021882401 pa 0x0000000086209000 perm : PTE_V| .. .. ..510: pte 0x00000000218830c7 pa 0x000000008620c000 perm : PTE_V|PTE_R|PTE_W| .. .. ..511: pte 0x000000002000204b pa 0x0000000080008000 perm : PTE_V|PTE_R|PTE_X| usertrap(): unexpected scause 0x000000000000000c pid=3221 sepc=0x0000000000004430 stval=0x0000000000004430 page table 0x00000000861e2000 ..0: pte 0x0000000021877c01 pa 0x00000000861df000 perm : PTE_V| .. ..0: pte 0x0000000021878001 pa 0x00000000861e0000 perm : PTE_V| .. .. ..0: pte 0x00000000216d2d1b pa 0x0000000085b4b000 perm : PTE_V|PTE_R|PTE_X|PTE_U|PTE_F| ..255: pte 0x0000000021878c01 pa 0x00000000861e3000 perm : PTE_V| .. ..511: pte 0x0000000021876c01 pa 0x00000000861db000 perm : PTE_V| .. .. ..510: pte 0x00000000218830c7 pa 0x000000008620c000 perm : PTE_V|PTE_R|PTE_W| .. .. ..511: pte 0x000000002000204b pa 0x0000000080008000 perm : PTE_V|PTE_R|PTE_X| OK test badarg: OK test reparent: OK test twochildren: OK ........ test sbrkarg: OK test validatetest: OK test stacktest: OK test opentest: OK test writetest: OK test writebig: OK test createtest: OK test openiput: OK test exitiput: OK test iput: OK test mem: OK test pipe1: OK test preempt: kill... wait... OK test exitwait: OK test rmdot: OK test fourteen: OK test bigfile: OK test dirfile: OK test iref: OK test forktest: OK test bigdir: OK ALL TESTS PASSED