• MIT6.S081 Lab cow


    Lab cow

    The problem

    xv6 中的 fork 系统调用复制所有的父进程的用户空间内存到子进程。如果父进程用户空间内存很大,复制会很耗时。有时这个复制是不必要的,如果 fork 之后子进程调用 exec,会释放掉复制的内存,可能大部分复制的内存都没有使用。如果父子进程共享一页,其中一个要写这个页,这才真正需要复制。

    The Solution

    COW fork 的目标是延迟为子进程分配物理页直到需要复制的时候。

    COW fork 只为子进程创建一个页表,其对应用户内存的 PTEs 指向父进程的物理页。COW fork() 标记父子进程的所有的用户 PTEs 不可写。当其中一个进程想要写 COW pages时,CPU产生一个 page fault。内核的 page-fault handler 处理这种情况:

    • 为错误进程分配一个物理页。
    • 复制原指向的页的内容到新页。
    • 修改 PTE,指向新页的物理地址,并标记为可写。
    • 当 page fault handler 返回时,用户空间能够写复制的页。

    释放物理页的时候需注意:一个物理页可能有多个进程页表引用,只有当引用数为 \(1\) 时才可以释放。

    Implement copy-on write

    vm.c

    • 将父子进程的 PTE 标记为 COW page 和 不可写。
      需要判断该页是否是 PTE_U,因为 trapframe 页在 fork 处理完用户页的映射后,会单独处理。且 uvmcopy 的参数 \(sz\) 是进程用户空间内存的大小,不包含 trapframe。
    • 父进程的物理页引用数加 \(1\)
    • 子进程的 PTE 指向父进程的物理页(不为子进程分配物理页)。
    // Given a parent process's page table, copy
    // its memory into a child's page table.
    // Copies only the page table and
    // clear their PTE_W.
    // 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);
    
        // trapframe(PTE_U=0) copied separately after call uvmcopy in fork()
        // uvmcopy only pay attention to user memory(PTE_U set)
        if ((flags & PTE_W) && (PTE_U | flags)) {
          flags = (flags & (~PTE_W)) | PTE_RSW_COW;
          *pte = ((*pte) & (~PTE_W)) | PTE_RSW_COW;
        }
    
        krefinc(pa);
    
        if(mappages(new, i, PGSIZE, pa, flags) != 0){
          goto err;
        }
      }
      return 0;
    
     err:
      uvmunmap(new, 0, i / PGSIZE, 1);
      return -1;
    }
    

    trap.c:

    • cowpagefault() 处理写不可写的 COW page。
    • 通过 kalloc() 分配新的页。
    • 重要优化:在复制之前可以判断该页的引用数,如果只有本进程在用,则可以直接设置该页可写。
    • 将旧页的内容复制到新页上。
    • 设置 PTE :可写,取消 COW page 标记,用新页的物理地址。
    • 释放旧页(引用为 \(1\) 回收物理页,否则引用数减 \(1\))。
    // only handler cow fault.
    // write the COW page(PTE_RSW_COW) of PTE_W on 0.
    int
    cowpagefault(pagetable_t pagetable, uint64 va)
    {
      pte_t *pte;
      uint64 oldpa, newpa;
    
      // va >= MAXVA cause walk() panic
      if (va >= MAXVA)
        return -1;
    
      if ((pte = walk(pagetable, va, 0)) == 0)
        return -1;
    
      if ((*pte & PTE_RSW_COW) == 0)
        return -1;
    
      if (*pte & PTE_W)
        return -1;
    
      // guard page, trampoline, trapframe
      if ((*pte & PTE_U) == 0) {
        printf("PTE_U=0\n");
        return -1;
      }
    
      oldpa = PTE2PA(*pte);
    
      // Important optimization: if a store page fault and
      // the ref which pointed this phy-page is one, no copy.
      if (krefnum(oldpa) == 1) {
        *pte = (*pte | PTE_W) & (~PTE_RSW_COW);
        return 0;
      }
    
      if ((newpa = (uint64)kalloc()) == 0) {
        printf("cowpagefault: kalloc fault.\n");
        return -1;
      }
    
      // copy the old page to the new page
      memmove((void*)newpa, (void*)oldpa, PGSIZE);
    
      *pte = (PA2PTE(newpa) | PTE_FLAGS(*pte) | PTE_W) & (~PTE_RSW_COW);
    
      kfree((void*)oldpa);
    
      return 0;
    }
    

    trap.c : usertrap():
    完善 trap path。

    else if (r_scause() == 15) {
      if (cowpagefault(p->pagetable, r_stval()) < 0) {
        p->killed = 1;
      }
    }
    

    kalloc.c

    • 释放物理页时判断引用计数,引用数为 \(1\) 时才可以真正释放物理页。
    • 基于 kfree() 的设计,在空闲物理页链表的初始化 freerange 中,必须先为引用计数设置为 \(1\) , 否则 kfree()panic
    // 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)
    {
      struct run *r;
    
      if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
        panic("kfree");
    
    
      int phypageindex = PHYPAGEINDEX((uint64)pa);
      uint16 refcnt;
    
      acquire(&kmem.lock);
      refcnt = kmem.ppagerefcnt[phypageindex];
      if (refcnt < 1)
        panic("kfree ref = 0.");
    
      kmem.ppagerefcnt[phypageindex]--;
      release(&kmem.lock);
    
      if (refcnt > 1)
        return;
    
      // 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);
    }
    
    void
    krefinc(uint64 pa)
    {
      int phypageindex = PHYPAGEINDEX(pa);
    
      acquire(&kmem.lock);
      if (pa >= PHYSTOP || kmem.ppagerefcnt[phypageindex] < 1)
        panic("krefinc pa invalid.");
      kmem.ppagerefcnt[phypageindex]++;
      release(&kmem.lock);
    }
    
    uint16
    krefnum(uint64 pa)
    {
      int phypageindex = PHYPAGEINDEX(pa);
      uint16 sz;
    
      acquire(&kmem.lock);
      sz = kmem.ppagerefcnt[phypageindex];
      release(&kmem.lock);
    
      return sz;
    }
    

    vm.c

    copyout() 将内核中的数据复制到用户空间,获取虚拟地址 dstva 对应的物理地址的时候,并不是 MMU 在转换地址,而是内核通过 walkaddr 模拟硬件读取页表内容转换虚拟地址为物理地址,内核直接对物理地址进行操作,所以 copyout 时需要对 COW page 进行复制。否则会直接对不可写的 COW page 进行修改而不会出现 page-fault ,出现难以排查的 bug。

    // 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;
    
      while(len > 0){
        va0 = PGROUNDDOWN(dstva);
    
        pa0 = walkaddr(pagetable, va0);
        if(pa0 == 0)
          return -1;
    
        pte_t *pte = walk(pagetable,va0, 0);
        if (!(*pte & PTE_W) && (*pte & PTE_RSW_COW)) {
          if (cowpagefault(pagetable, va0) < 0)
            return -1;
          pa0 = PTE2PA(*pte);
        }
    
        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;
    }
    

    Code

    Code: Lab cow

  • 相关阅读:
    docker 常用
    vue-cli 的静态资源
    ubuntu 恢复模式
    virtualbox
    cmake 使用技巧
    VLC播放pcm
    Ice-Lite 理解
    webrtc ice流程
    Mediasoup_Demo信令过程, client and server
    Mediasoup
  • 原文地址:https://www.cnblogs.com/seaupnice/p/15843709.html
Copyright © 2020-2023  润新知