Mit6.s081-2020-lab6-copy-on-write

76 阅读4分钟

一些有用的宏和页表标志的定义在kernel/riscv.h

步骤一:修改fork()为写时复制机制

修改uvmcopy():查找到parent的物理内存,然后令child的pte也指向这块物理内存,而不是分配新的页面。对child和parent都要清除PTE_W(变成只读)

// uvmcopy
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags; 
   // old-parent; new-child
   // find pa corresponding to parent'pte 
  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); // parent's
    flags = PTE_FLAGS(*pte); // parent's
    // change parent's PTE->only read, not write
    flags |= PTE_R;
    flags &= ~PTE_W;

    // don't need to copy pa   
    // memmove(mem, (char*)pa, PGSIZE);
    // only copy page table, create map

    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      kfree(mem);
      goto err;
    }
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;

into:解决计数和PTE标志位问题

  1. page fault时, kernel page fault handler如何确认出错原因是不是由cow引起? ——添加表示COW的PTE位,然后在usertrap()中检测pte的bit 8。如果此位为1,即cow引起的。

    #define PTE_COW (1L << 8) // 1->cow fault

  • 先在uvmcopy()中添加代码,设置parent和child的PTE_COW为1
 flags |= PTE_COW;
 if(mappages(...))
  • 然后添加判断是否为cow fault的函数
/* vm.c */
/* 相比原来增加了一些新的特殊情况检测--这里类lab5*/
uint64 cow_check(uint64 va){
  // va是传进来的发生page fault的地址--需要页对齐
  va = PGROUNDUP(va);
  struct proc* p = myproc();
  pte_t* pte = walk(p->pagetable, va, 1);
  return va < p->sz && (pte != 0) && (*pte & PTE_V) && ((*pte & PTE_COW) != 0); 
}

  1. 关于物理内存的引用计数如何保存? ---使用数组。
    如何索引数组:使用页面的物理地址/PGSIZE来索引数组。
    如何选择其大小:kalloc.ckinit()--PHYSTOP
/* kalloc.c */

struct spinlock pgreflock; // 用于 pageref 数组的锁,防止竞态条件引起内存泄漏
#define PA2PGRE_ID(p)  (((p)-KERNBASE)/PGSIZE)
#define PGREF_MAX PA2PGREF_ID(PHYSTOP)
int p_refered[PGREF_MAX]; // 引用计数数组

// 获得引用计数
#define PA2PGREF(p) p_refered[PA2PGREF_ID((uint64)(p))] 
  • 修改kinit来初始化锁
/* kalloc.c */
void kinit()
{
  initlock(&pgreflock,"pgref"); // 初始化锁
  initlock(&kmem.lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}
  • kfree: 只有当计数为0时,才能释放这块内存
/* kalloc.c */
  // 内存释放的时候调用freewalk,freewalk里又调用了kfree
  // 添加一个判断数组计数是否为0
  acquire(&pgreflock);
  if(--PA2PGREF(pa) <= 0){
    // 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);
  }
  release(&pgreflock);
  • 进程在kalloc的时候会让这块新内存的地址计数变为1;
// kalloc()
if(r){
    memset((char*)r, 5, PGSIZE); // fill with junk
     PA2PGREF(r) = 1;
  }
  • 添加两个函数:一个是增加计数;一个是减少计数
void *kcopy_n_deref(void *pa) {
  acquire(&pgreflock);

// 只有 1 个引用,无需复制--这里应该对应的是如果是parent页面有page fault时,且childs都有自己的物理页面了
  if(PA2PGREF(pa) <= 1) { 
    release(&pgreflock);
    return pa;
  }

  // 分配新的内存页,并复制旧页中的数据到新页
  uint64 newpa = (uint64)kalloc();
  if(newpa == 0) {
    release(&pgreflock);
    return 0; // out of memory
  }
  memmove((void*)newpa, (void*)pa, PGSIZE);

  // 旧页的引用减 1
  PA2PGREF(pa)--;

  release(&pgreflock);
  return (void*)newpa;
}

// 为 pa 的引用计数增加 1
void krefpage(void *pa) {
  acquire(&pgreflock);
  PA2PGREF(pa)++;
  release(&pgreflock);
}

步骤二:实现page fault里的处理函数(分配新内存)

cow_alloc时减少计数

/* 原来在cow_alloc里面进行的分配页面操作和减少计数操作在kcopy_n_deref()中完成,这里是实现了重新映射*/
void cow_alloc(uint64 va)
   struct proc* p = myproc();
   pte_t* pte;
   uint64 pa;
   if((pte = walk(p->pagetable, va, 0)) == 0){
      panic("cow_alloc: walk\n");
   }
   pa = PTE2PA(*pte);
   // 减少对旧页的映射,计数-1
   uint64 new = (uint64)kcopy_n_deref((void*)pa);
   if(new == 0){
     p->killed = 1;
   }
   // 将child的PTE从旧页变成新页
   // 先取消对原来旧页的映射再映射新页的
   // 不用do_free;
   uint64 flags = (PTE_FLAGS(*pte) | PTE_W) & ~PTE_COW;
   uvmunmap(p->pagetable, PGROUNDDOWN(va), 1, 0); 
  
   if(mappages(p->pagetable, va, PGSIZE, new, flags) != 0){
	    printf("cowalloc: fail to mappages\n");
	    p->killed = 1;
    }
}

cow时要添加计数

 // uvmcopy()--增加计数
 krefpage((void*)pa);

步骤三:修改usertrap()和copyout()

else if(r_scause() == 13 || r_scause() == 15){
    uint64 va = r_stval();
    if(cow_check(va)){
      cow_alloc(va);
    }
}

遇到的bug以及解决方法

(错误记录是之前写lab的时候记下的,可能有一些不准确or解释得不好)

  • 可能内存泄漏导致无法启动 4950c4e4d99985077d96f2e693d779a.png

发现了uvmcopy里的错误并修改:

flags &= ~PTE_W; // not change pte

flags |= PTE_COW;

修改后

     if(*pte & PTE_W){
      *pte = (*pte & ~PTE_W) | PTE_COW;
     }
     flags = PTE_FLAGS(*pte);
  • sbrk failed: cow_alloc的mappages里的映射改为1

  • copyout failed: panic walk-->猜测是因为特殊情况:va >= MAXVA引起的(原来是在walk()之后才检测的话就没起到作用),所以需要先检测位置是否正常再调用walk()查找:

uint64 cow_check(uint64 va){
  struct proc* p = myproc();
  if(va >= MAXVA) return 0;
  pte_t* pte = walk(p->pagetable, va, 0);
  return va < p->sz && (pte != 0) && (*pte & PTE_V) && (*pte & PTE_COW); // 1-cow page
}

修改之后成功

Pasted image 20230711215441.png