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