2020 6.s081——Lab6:Copy-on-Write Fork for xv6踩坑历程

对夜的长吼 我胸口 的伤口 随风陈旧

你我终会沦为尘埃漂流

等待花季烟雨稠

——愿与愁

完整代码见:SnowLegend-star/6.s081 at cow (github.com) 

让GPT阅读了部分实验说明,它的解释如下:

1、COW fork() 的目标是延迟分配和复制子进程的物理内存页面,直到实际需要时才进行,如果需要的话。

2、当执行COW fork()时,子进程会创建一个页表,其中包含了用户内存的页面表项(Page Table Entry,PTE),这些PTE指向父进程的物理页面

fork()开始只给子进程分配一张页表,这张页表的PTE都指向父进程的物理页面。这样就有一个问题——如果子进程只有一张顶级页表,那怎么可以映射父进程所有的物理页面呢?

在COW fork()过程中,子进程的level2层次的页表是与父进程的level2层次的页表相同的。这意味着子进程的level2页表中的每个页表项(PTE)都指向父进程的相应level1页表。

换句话说,子进程初始时使用的页表结构与父进程完全相同。这包括指向父进程的level1页表的指针。因此,子进程的level2页表中的PTE实际上指向父进程的level1页表,而不是直接指向父进程的物理页面。

只有当子进程尝试写入共享的页面时,COW机制才会触发,为子进程创建新的物理页面,并将子进程的level2页表中相应的PTE更新为指向新分配的物理页面。这样,子进程就可以在需要修改时拥有自己的页面副本,而不会影响父进程的页面。

我感觉这个提示可能有问题,那就是不应该只为子进程分配level2层次的页表,而是应该分配level2、level1、level0三层页表。

3、COW fork() 将父进程和子进程的所有用户PTE标记为不可写。这意味着当任一进程尝试写入这些COW页面时,CPU会触发一个页面错误(page fault)。

这里更是确定了子进程的三层页表应该和父进程的一一对应。

4、内核的页面错误处理程序会检测到这种情况,为产生页面错误的进程分配一个物理内存页面,并将原始页面复制到新页面中

如何将原始内存页面复制到新页面中呢?通过stval获取发生trap的va,再把va转化为pa即可。最后通过memmove进行物理页面的复制。

  1. 例如要写入一个刚创建的子进程,此时子进程页表的PTE被标记为不可写。写入会导致页面出现写入错误(假设发生错误的物理地址pa。
  2. 此时需要创建一块新的物理内存页,把发生写入错误的物理页内容复制到新的物理页中pa2。
  3. 好像得取消子进程发生错误的va和父进程物理页pa1的映射,不然会出现remap问题。
  4. 把子进程发生写入错误的va和pa2进行映射。则原来对父进程的物理页pa1的引用需要减1。

5、处理程序还会修改产生页面错误的进程中的相关PTE,使其指向新页面,这次将PTE标记为可写。这样,进程就可以写入其页面的副本了。

6、当页面错误处理程序返回时,用户进程将能够写入其页面的副本

这个新副本要被标记为可写的。

7、COW fork() 使得释放实现用户内存的物理页面变得稍微棘手。一个特定的物理页面可能被多个进程的页面表所引用,应该只有在最后一个引用消失时才能释放。

这里其实我有预感是要进行race处理的,但是偷懒就没去深究。后面发现不进行并发处理的话就会存在race的问题,实在是让人头大。

再来看看官方给出的四个attack plans:

1、Modify uvmcopy() to map the parent's physical pages into the child, instead of allocating new pages. Clear PTE_W in the PTEs of both child and parent.

这里的意思是给子进程创建新的三层页表,但是不额外分配页表映射到的物理页面。而是直接让子进程的页表映射到父进程的物理页面中。同时,把level0层的PTE置为不可写的状态。我们发现父进程和子进程的底层PTE内容极其相似,有差异的地方就是标志位。

2、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.

这里的“install the new page in the PTE with PTE_W set”这句话太抽象了。其实通过如下两个步骤就可以做到:

  1. 申请一块物理页pa2,把发生trap的物理页pa1的内容复制到物理页pa2中
  2. 再获取出错的va,把va和p2进行映射就可以

一开始我在处理va和pa2的映射时,打算先取消va和pa1之间的映射,想当然地就用uvmunmap()来完成。但是这样产生的效果和预期并不相符

仔细阅读了uvmunmap()后,发现它就是一个walk()加上kfree(),直接把物理页pa给释放掉了,这显然是不符合我们的要求的。但是如果不取消va和pa1的映射,后续进行va和pa2映射时就会有remap的问题。观察产生remap的几种原因后,可以发现是在当前PTE的PTE_V为1的情况下再次进行映射产生的问题。

一种可行的做法是把当前level0层PTE的PTE_V置为0,这有点类删除steam游戏的机制。直接用新的内容覆盖旧的内容。

3、Ensure that each physical page is freed when the last PTE reference to it goes away -- but not before. A good way to do this is to keep, for each physical page, a "reference count" of the number of user page tables that refer to that page. Set a page's reference count to one when kalloc() allocates it. Increment a page's reference count when fork causes a child to share the page, and decrement a page's count each time any process drops the page from its page table. kfree() should only place a page back on the free list if its reference count is zero. It's OK to to keep these counts in a fixed-size array of integers. You'll have to work out a scheme for how to index the array and how to choose its size. For example, you could index the array with the page's physical address divided by 4096, and give the array a number of elements equal to highest physical address of any page placed on the free list by kinit() in kalloc.c.

这里让我们对物理页的引用进行计数,直到当前物理页的引用数为0就可以释放了。我最初的想法是先设置一个全局数组用来记录每个物理页的索引情况,在每个涉及到kfree()和kalloc()的地方维护这个数组。后来发现这样是治标不治本,如果后续要添加新的函数也要引用kfree()或者kalloc(),那还得继续手动维护索引数组。

后来发现可以直接在kalloc.c文件下的kfree()和kalloc()等内部维护索引数组,从根源上解决问题。这里一定要注意并发性问题的处理,否则也会出现这种bug。我就是被加锁的情况折磨了一下午。因为一个锁的释放问题导致眼睛都看花了还没发发现问题所在。下面的语句原来没有问题。下列语句可以通过usertests,却倒在了threetest…

出现这个现象的原因还是uvmunmap()在作祟。

4、Modify copyout() to use the same scheme as page faults when it encounters a COW page.

乍一看感觉copyout()和COW机制好像没啥关联啊,问了GPT之后我才恍然大悟。

在给定的copyout()函数实现中,并没有直接涉及到COW机制。但是,在进行写操作时,如果目标页面是一个COW页面,那么写操作会触发页面错误,操作系统会通过页错误处理机制来创建该页面的副本,并将副本映射到写操作发生的进程的地址空间中。因此,在实际的操作系统中,copyout()函数可能会与COW机制一起使用,以实现延迟复制的效果。

看完了上述内容后,实现框架其实就很明了了,我以为接下来就是随意拿下。谁知道这个lab大致实现框架确实是简单,但是个中陷阱不计其数,让我一次又一次地掉进坑里,西巴。

首先是当发生trap的va位于guard page的判断

这句话还是我自作聪明地加上的,谁知成了坑害我的第一个大坑。如果加上这句话,就会发生下列问题:不管运行什么进程都会最终转为初始化sh。去群里问了下,发现原因可能是init.c中含有的运行在当前stack地址下面的代码段。这个判断会直接阻止init.c的正常完成。

判断当前页面是否需要操作也出了问题,这个错误是真不应该,也是太想当然了。

下一个问题是如果当前页面不符合处理规则,一定要及时kill掉进程,不然会卡在sbrkfail()。即要在上述判断后加上

else exit(-1)

最后一个问题是还是和guard page有关。课上老师说过guard page确实是不会有相应的va与之映射的,所以我们得换一种方式来进行这个判断。如果出错的va对应的pa为0,那就说明出错的va可能位于guard page了。不做这个判断的话stacktest就会出现bug。

定义的结构体如下

struct {
  int pgtbl_index[PHYSTOP/PGSIZE];   //物理内存最多可以分成128MB/4KB=32K
  struct spinlock lock;        //物理页表引用锁
}ref_cnt;

kfree()如下

void
kfree(void *pa)
{
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  acquire(&ref_cnt.lock);
  ref_cnt.pgtbl_index[(uint64)pa/PGSIZE]--;
  // release(&ref_cnt.lock); //什么时候释放都可以(X)   关于锁的释放问题一定要谨慎
  if(ref_cnt.pgtbl_index[(uint64)pa/PGSIZE]==0){
  release(&ref_cnt.lock); //什么时候释放都可以
  // 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;
  release(&kmem.lock);
  }
  else
    release(&ref_cnt.lock);
}

 由于别的函数也会获取当前物理页面分配情况的计数,所以这里抽象出一个函数

int physiaclPage_refcnt(void* pa){
  return ref_cnt.pgtbl_index[(uint64)pa/PGSIZE];
}
void modify_pgtbl(void* pa){
  if( (uint64)pa%PGSIZE!=0 || (char*)pa < end || (uint64)pa > PHYSTOP) 
    return ;
  acquire(&ref_cnt.lock);
  ref_cnt.pgtbl_index[(uint64)pa/PGSIZE]++;
  release(&ref_cnt.lock);
}

usertrap的修改如下

else if(r_scause()==15 || r_scause()==13 ){
    pte_t*  pte;
    char*   mem;
    int     perm;                     //pte低十位的标志
    uint64  pa;                       //发生trap的物理地址
    uint64 addr=r_stval();            //获取出错的地址
    // printf("发生trap的va是: %p\n", addr);
    // //判断出错地址的合法性
    
    if(addr > p->sz || addr > MAXVA)    
      exit(-1);
    // if(addr < p->trapframe->sp )                   //即出错的地址位于guard page   傻逼判断害苦了我啊!
    //   exit(-1);
    
    // printf("当前运行进程为%s, pid为%d\n", p->name, p->pid);
    pte=walk(p->pagetable, addr, 0);                  //获取出错地址对应的pte
    if(pte==0)
      exit(-1);

    // printf("发生trap的PTE为: %p\n",*pte);
    if( (*pte & PTE_RSW) && (*pte & PTE_V) ){         //只处理COW页表   if((*pte) & PTE_RSW & PTE_V) 我是傻逼
    // if(cowpage(p->pagetable,addr)==0){
      addr=PGROUNDDOWN(r_stval());
      pte=walk(p->pagetable, addr, 0);
      // pa=PTE2PA(*pte);
      pa = walkaddr(p->pagetable, addr);
      if(pa==0)                                       //为了确保映射到guard page时不出错
        exit(-1);
      // printf("发生trap的pa是: %p\n", pa);
      // printf("当前运行进程为%s, pid为%d\n", p->name, p->pid);
 
      if(physiaclPage_refcnt((void*)pa) ==1){         //如果对发生trap的页面引用是1
        *pte=(*pte | PTE_W) & ~PTE_RSW;
      }
      else{
        mem=kalloc();
        if(mem==0)
          exit(-1);
        *pte &= ~PTE_V;                                //不加这句会有remap
          //开始给这个出错的pte分配的实际的物理地址
        memmove(mem, (char*)pa, PGSIZE);
        // uvmunmap(p->pagetable, addr, 1, 0);
        perm=(PTE_FLAGS(*pte) | PTE_W) & ~PTE_RSW;
        if(mappages(p->pagetable, addr, PGSIZE, (uint64)mem, perm) <0){
          kfree((void*)mem);
          *pte=*pte&~PTE_V;
        }
        kfree((void*)PGROUNDDOWN(pa));                 //发生trap的pa引用应该减一
      }
    }
    else                                               //一定要及时kill有问题的进程  不然会卡在sbrkfail
      exit(-1);
  }

 uvmcopy的修改如下

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);
    //当父进程的PTE本身是可写的才设置父进程的level0层页表是不可写的
    //COW是建立在页面可写的基础上的
    if(flags & PTE_W){
      *pte=( (*pte) & ~PTE_W ) | PTE_RSW;
      flags = (flags & ~PTE_W) | PTE_RSW;     //添加标志位一定得用 | 而不是 &
    }
    // printf("***************\n");
    // printf("父进程的页表如下: \n");
    // vmprint(old);

    if(mappages(new, i, PGSIZE, pa, flags) < 0){
      return -1;
    }

    modify_pgtbl((void*) pa);

    // printf("子进程的页表如下: \n");
    // vmprint(new);
    // printf("\n");
  }
  //设置父子进程的level2层页表都是不可写的
  // for(i=0; i<512; i++){
  //   old[i]=(old[i] & ~PTE_W) | PTE_RSW;
  //   new[i]=old[i];
  // }
  return 0;
}

copyout()的修改如下
 

int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;
  pte_t *pte;
  char  *mem;
  int   perm;
  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;

    pte=walk(pagetable, va0, 0);
    // 如果dstva是一个COW页表,则需要申请新的页表来进行写入
    if(pte!=0 &&(*pte & PTE_V)!=0 && va0<MAXVA){
      if( (*pte & PTE_RSW) && (*pte & PTE_V) ){
        if(physiaclPage_refcnt((void*)pa0) ==1){
          *pte=(*pte | PTE_W) & ~PTE_RSW;
        }
        else{
          perm=(PTE_FLAGS(*pte)|PTE_W) & (~PTE_RSW);    //这个perm也是坏我大事
          mem=kalloc();
          if(mem!=0){
          // uvmunmap(pagetable, va0, 1, 0);
          *pte &= ~PTE_V;
          memmove(mem, (char*)pa0 ,PGSIZE);
          if(mappages(pagetable, va0, PGSIZE, (uint64)mem, perm) <0){
            // pgtbl_index[(uint64)mem/4096]--;
            // if(pgtbl_index[(uint64)mem/4096]==0)
            kfree(mem);
            *pte=*pte&~PTE_V;
            return -1;
          }
          kfree((void*) PGROUNDDOWN(pa0));
          pa0=(uint64)mem;
          }
        }
      }
    }
    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还是相当困难的,希望大家能耐下心来慢慢思考。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值