MIT 6.S081 Lab5 Copy-On-Write Fork

前言

最近绝大多数的空闲时间都拿来锤15-445了,很久没动6.S081。前几天回头看了一下一个月前锤完的Lazy Allocation,自己写的代码几乎都不认识了.......看来总结之类的东西最好还是趁着热乎的时候写啊。

不过15-445的内容实在太多了,我只是为了锤Lab粗略的看了看课件,课件里很多东西都没研究,相关的总结还是推迟到所有Lab锤完后重新整理一下再写吧。先把几天前刚做完的Copy-On-Write给写出来。最近会把前面草草写下的Lab Lazy Allocation的相关内容整改一下,补上具体的Lab。

Lab5 Copy-On-Write fork的链接:https://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段是我自己给这个段起的名字,你们谷歌查是查不到的....我实在不知道到底该怎么称呼这个段.....

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

 

 

posted @ 2020-11-05 16:00  KatyuMarisa  阅读(1200)  评论(0编辑  收藏  举报