MIT-6.s081-OS lab lazy: Copy-on-Write Fork for xv6

本实验仍然是虚拟内存,所以暂且跳过了中间的内容(指lec和课本)

分析

copy on write fork :

在之前的code walk through(https://blog.csdn.net/RedemptionC/article/details/107709618)中提到了,fork的实现里,有一个uvmcopy的调用,作用是将父进程的内存复制到子进程中,这个复制包括申请物理内存,设置pte

但是在下列情况下:

  • 父进程和子进程都不修改某些内存页
  • fork之后子进程马上调用exec,而由exec的实现可知,子进程的内存马上会由可执行文件的内容代替

这个复制,就显得很浪费资源,而且这两种情况不在少数,因此我们需要copy on write

fork时不申请额外的内存,只申请一个页表,并且把子进程页表的pte指向父进程的物理内存(和父进程页表的pte一样),仅当父进程/子进程修改其内存时,才复制页

这里的做法和lazy allocation差不多,都是在异常处理时申请内存:在fork时,把父进程的pte的PTE_W clear,这样写的时候,就会trap

在异常处理处,申请内存,真正复制页之后,将分别的pte的PTE_W都set,然后就能顺利write了

此时我们在free的时候,如果没有真正复制页,也就是多个进程的pte指向同一个物理页,就需要等ref count为0,才能free掉

当然如果已经真正复制了,就可以直接free掉(由此可见我们需要将这两种页进行区分)

书里给出的建议方案:

  • 修改uvmcopy,使其将父进程的物理页面映射到子进程的地址空间,而不是分配新页面,并且在两者中都clear PTE_W
  • 修改usertrap,识别page fault,当cow page出现page fautl时,分配一个新页面,将旧页面分配到新的,并且设置两者的PTE_W为set
  • 确保cow page只有在ref count为0时才会被free(可能可以在kalloc.c中实现ref count)
  • 修改copyout ,即内核在修改用户地址空间的页面时,也要做与usertrap相同的工作
  • 增加一个PTE flag标识页面是否时cow page

关于如何实现ref count,阅读kalloc.c可知,物理页是通过一个结构体的链表来管理空闲的物理页的,并且这个结构体本身就保存在空闲页中

问题是,ref count 需要保存在非空闲页中?直接保存信息不会被覆盖吗?该如何设计?

比如:在kalloc里保存一个数组,数组长度为物理页的个数(kinit中得出),然后根据地址知道自己应该是哪一个物理页,从而得到对应页的ref count?测试了一下 有32723个物理页,开辟一个char型数组内存压力不大?

update:

本来以为一上午就能做完的,没想到磨磨唧唧的用了两天才完成,但即使这样,这也是我做的最快最顺利(相对)的一个lab了!

前面60分照着上面的建议方案做,比较顺利,第一个fail是filetest,其实如果留心filetest的注释的话,就知道问题多半是出在copyout()

因为filetest里用到了pipe,所以涉及到内核内存空间和用户内存空间的交换,copyout正是从内核到用户

copyout本来的实现是:

copyout(pagetable,dstva,src,len)

其中dstva是用户内存空间的虚拟地址,src是内核内存空间的,因为是direct map,所以也是物理地址

首先将dstva PGROUNDDOWN,即找到对应内存页的起始地址,然后用walkaddr找到对应的物理地址,然后用memmove,在两个物理地址之间复制(其中有一些比较细节的问题,比如这一段,也不是很懂,不过可以不管

    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    //memmove((void *)(pa0 + (dstva - va0)), src, n);

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;

因为我们要实现从写时复制(copy on write),所以这里首先要判断这是不是一个cow page,如果不是,那么还是老做法

如果是,那么我们就要做和usertrap里类似的事,首先申请一个新页(kalloc),然后把老页的内容复制到新页,然后修改pte的flag:clear pte_cow, set pte_w,即标识这个页不是cow page了,是一个进程独享的,并且可以写

注意len可能大于PGSIZE,因此上述工作也要放在while循环里做,所以最后的代码是这样的:

while(len>0){
    uint64 n, va0, pa0;
    va0 = PGROUNDDOWN(dstva);
    pte_t *pte=walk(pagetable,va0,0);
    if(pte==0||((*pte)&PTE_V)==0||((*pte)&PTE_U)==0){
      printf("copyout : pte not exist or not accessible to user\n");
      return -1;
    }
    pa0=PTE2PA(*pte);
    if((*pte)&PTE_COW){
      char *mem=kalloc();
      if(mem==0){
        printf("copyout: out of memory\n");
        return -1;
      }
      memmove(mem,(char *)pa0,PGSIZE);
      // set PTE_W , unset PTW_COW
      (*pte)|=PTE_W;
      (*pte)&=~PTE_COW;
      int perm=PTE_FLAGS((*pte));
      (*pte)&=~PTE_V;
      if(mappages(pagetable, va0, PGSIZE, (uint64)mem, perm) != 0){
        kfree(mem);
        return -1;
      }
      (*pte)|=PTE_V;
      pa0=(uint64)mem; // NOT SURE
    }
    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }

曾经遇到的一个bug是:filetest中,通过Pipe,应该要读到的是0,但是读到了5,后来发现5是kalloc申请内存时填充的所谓junk

也就是实际上我是没有把pipe中的内容写到buffer中的,因为我一开始把是否是cow page,作为copyout中的一个分支,但是实际上不管你是不是cow page,都需要保留原来的代码:把源的数据写到目的位置,而不是仅仅是复制页

另一个遇到的bug是:usertest中,pgbug,这里使用了一个大于MAXVA的地址,所以在copyout中是应该要返回-1(标识error)的,要加上这一句

还有一个是:usertrap中,处理异常时,我只处理了SSCAUSE为15的,把原来的这一段注释了:

      printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
      printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
      p->killed = 1;

但是实际上很多不是15的异常,也需要被kill,但是没有,就导致不能通过一些测试

另外,最后还需要新建一个文件,保存你完成实验的时间,不然只能得99分=-=

顺利通过啦~

最后,一些debug的小技巧:

1.很多时候,如果按next,会进入不可知的??代码,所以在本实验中,是不能完全依赖next 单步调试的,应该要自己理清头绪,找到下一步运行到的地方(函数调用等),打断点,然后continue

2.这是一个hint里给出的提示:有时候遇到panic,我们可以直接在一开始 b panic,然后continue,最后利用bt(backtrace)查看函数调用‘栈’(?),不过有时候函数调用太深,也会遇到??,所以可能printf才是最好的debug方法吧qaq 但是我懒 所以尽量尝试gdb

写的比较简单 代码放这吧:

https://github.com/RedemptionC/xv6-riscv-6s081/tree/cow

猜你喜欢

转载自blog.csdn.net/RedemptionC/article/details/107737515