2022 xv6 lab: COW实验(copy-on-write)代码实现

lab链接: https://pdos.csail.mit.edu/6.1810/2022/labs/cow.html
之所以写这篇总结,是因为这次的实验的难度确实困扰了笔者很久,所以我也想将做此实验的一些收获分享出来给大家
加上现在网上大多是21的lab经验贴,很少有22的,而22的usertest相对21又增加了一些难度,所以我也想将22中的一些问题分享出来

1.修改vm.c文件中的uvmcopy

(1)分析过程
  • 要使得fork()中,父进程与子进程共享同一片存储区,主要就是要修改fork的页表分配方法
  • 在原始的fork中,是先给子进程重新申请一块内存,并将父进程的页表copy给子进程,现在我们就是要对这一部分进行修改,要让父子进程共享一块内存
  • 而这一部分的实现就是在uvmcopy函数中,修改代码如下
(2)代码修改
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");
    // 设置父进程的PTE_W为不可写,且为COW页
    *pte = ((*pte) & ~PTE_W) | PTE_C; 
    flags = PTE_FLAGS(*pte);
    pa = PTE2PA(*pte);  
    // 不为子进程分配内存,指向pa,页表属性设置为flags即可
    if(mappages(new, i, PGSIZE, pa, flags) != 0) {
      printf("uvmcopy failed \n");
      goto err;
    }
    kreferCount((void*)pa,1);   //该内存页的引用数加1(这里后面会提到)
  }
    return 0;

    err:
      panic("uvmcopy error");
      uvmunmap(new,0,i / PGSIZE,1);
      return -1;
}

2.修改kalloc.c文件,开辟一个内存区以储存每个物理页的引用数

(1)分析过程
  • lab中的tips提到,我们需要去对每一页可用内存(end~PHYSTOP)去计数,只有当该页内存没有被任何一个进程映射,才可以用kfree去释放该页内存
  • 如何去实现?tips中也给出了建议,可以去开辟一块数组,这个数组就会被存储在内核代码段中。我的做法是直接使用可用内存(end~PHYSTOP),在end起始位置开始去维护一段以进行存储数组
  • 用于计数的内存开辟完成之后,就是修改kfree和kalloc,完成我们想要的逻辑,这部分相对好理解
(2)代码修改
  • 在kalloc.c中添加一些对物理内存划分的宏定义
//内核可用内存起始位置(做了对齐处理)
#define    kstart          PGROUNDUP((uint64)end)

//利用物理地址p求数组的下标数
#define    N(p)      (((PGROUNDUP((uint64)p)-(uint64)kstart) >> 12))

//用于存储引用值的内存段结束的位置
#define   kend          (uint64)kstart+N(PHYSTOP)
  • 修改结构体kmem,加入维护内存的自旋锁和数组声明
struct {
  struct spinlock lock;
  struct run *freelist;

  //add
  struct spinlock reflock;  //维护计数数组的自旋锁
  char *paref;              //映射的用于计数的数组(起始位置kstart)
} kmem;
  • 新增两个函数,用于操作维护计数数组的自旋锁
inline void
acquire_refcnt()
{
  acquire(&kmem.reflock);
}

inline void
release_refcnt()
{
  release(&kmem.reflock);
}
  • 修改kinit,修改初始化范围
void
kinit()
{
  initlock(&kmem.lock, "kmem");
  initlock(&kmem.reflock,"reflock");
  kmem.paref = (char*)kstart;    //paref映射的用于计数的数组(起始位置kstart)


  freerange((void*)kend, (void*)PHYSTOP);   //初始化空闲列表
}
  • 在freerange函数中加入对计数数组的初始化
void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){

    acquire(&kmem.reflock);
    *(kmem.paref+N(p)) = 1;  //初始化为1,因为后面有kfree减1
    release(&kmem.reflock);

    kfree(p);
  }
}
  • 新增一个函数kreferCount(前面提到了),用于实现对某块内存的计数+1或-1
//pa为物理内存地址
//flag为指示标志,>0为+1,<0为-1
void 
kreferCount(void *pa,int flag) 
{
  acquire(&kmem.reflock);

  if(flag > 0){                        //当前页映射加1
    *(kmem.paref+N((uint64)pa)) += 1;
  }
  else if(flag < 0){									//当前页映射减1
    *(kmem.paref+N((uint64)pa)) -= 1;
  }

  release(&kmem.reflock);
}
  • 修改kfree函数,加入引用数判断,如果引用数大于0那么不对其做处理
void
kfree(void *pa)
{
  struct run *r;

  //保证释放的物理内存是对齐的(4k)
  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");


  kreferCount(pa,-1);             //减少一个引用,-1
  if(*(kmem.paref+N(pa)) != 0){   
      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);
}
  • 修改kalloc函数,每分配一页内存对该页的引用计数+1
void *
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r){
    kmem.freelist = r->next;
  }
  release(&kmem.lock);


  if(r){
    memset((char*)r, 5, PGSIZE); // fill with junk
    kreferCount((void*)r,1);     //映射该页,引用计数+1
  }
  return (void*)r;
}

3.新增函数以实现COW恢复操作

(1)分析过程

我们已实现对fork映射机制修改,那么当页异常发生,要对子进程重新开辟内存具体应该怎么做?增加如下两个函数,会在后面使用到

(2)代码修改
  • 根据tips,在riscv.h中增加pte中的cow标志
#define PTE_C (1L << 8) // copy pte
  • 在vm.c中增加函数uncopied_cow,用于对写异常页的pte进行判断其是否合法(在defs.h中也要声明该函数)
/*判断是否为未分配内存COW页*/
//PTE_C标志用于区分该页是否是没有分配独立内存的cow页
//PTE_C与PTE_W标志一定为相反
int 
uncopied_cow(pagetable_t pgtbl, uint64 va){

  if(va >= MAXVA) 
    return -1;
  pte_t* pte = walk(pgtbl, va, 0);
  if(pte == 0)             // 如果这个页不存在
    return -2;
  if((*pte & PTE_V) == 0)
    return -3;
  if((*pte & PTE_U) == 0)
    return -4;

  return ((*pte) & PTE_C); // 有 PTE_C 的代表还没复制过,并且是 cow 页
}
  • 在vm.c中增加函数cowalloc,用于进行COW具体操作(在defs.h中也要声明该函数)
/*给合法的cow页分配内存*/
int 
cowalloc(pagetable_t pgtbl, uint64 va){
  pte_t* pte = walk(pgtbl, va, 0);
  uint64 perm = PTE_FLAGS(*pte);

  if(pte == 0) return -1;
  uint64 prev_sta = PTE2PA(*pte); // 这里的 prev_sta 就是这个页帧原来使用的父进程的页表
                                  // 这里写 sta 是因为这个地址是和页帧对齐的(page-aligned)
                                  // 所以写个 sta 表示一个页帧的开始
  uint64 newpage = (uint64)kalloc();     
  if(!newpage){
    return -1;
  }
  uint64 va_sta = PGROUNDDOWN(va); // 当前页帧

  perm &= (~PTE_C); // 复制之后就不是合法的 COW 页了
  perm |= PTE_W;    // 复制之后就可以写了

  memmove((void*)newpage, (void*)prev_sta, PGSIZE); // 把父进程页帧的数据复制一遍
  uvmunmap(pgtbl, va_sta, 1, 1);      // 然后取消对父进程页帧的映射
  
  if(mappages(pgtbl, va_sta, PGSIZE, (uint64)newpage, perm) < 0){
    kfree((void*)newpage);
    return -1;
  }
  return 0;
}

4.修改usertrap拦截页异常

(1)分析过程

根据tips,我们要在trap.c中的usertrap中拦截写页异常,那么利用什么标志呢?

根据riscv手册(riscv-privileged),查到当scause寄存器为15时,为写页异常
2022 xv6 lab: COW实验(copy-on-write)代码实现_第1张图片

(2)代码修改

在usertrap函数中添加

else if(r_scause() == 15) { // 缺页错误
    if(uncopied_cow(p->pagetable,r_stval()) > 0){
      if(r_stval() < PGSIZE)  //对0起始地址等低地址直接写,那么直接退出
        p->killed = 1;
      if(cowalloc(p->pagetable,r_stval()) < 0)
        p->killed = 1;
    }

5.修改copyout

(1)分析过程

由于有些访问COW页的操作不是来自用户空间的,那么也需要对vm.c中的copyout函数进行修改(tips中也提到了这一点)

(2)代码修改
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{

  //此处发生于内核空间的复制,发生页异常时不会引起usertrap

  uint64 n, va0, pa0;

  while(len > 0){

    va0 = PGROUNDDOWN(dstva);        //目标虚拟地址的页首地址
    int res = uncopied_cow(pagetable,va0);
    
    if(res > 0){
      if(cowalloc(pagetable,va0) != 0)
        goto err;
    }
    else if(res < 0){
        // printf(" %d \n",res);
        goto err;
    }

    pa0 = walkaddr(pagetable, va0);  //获取虚拟页对应的物理页
    if(pa0 == 0)
      goto err;
    n = PGSIZE - (dstva - va0);      //该页的剩余偏移量
    if(n > len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);  //直接从物理地址copy到src

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;            //翻页
  }
  return 0;

  err:
    return -1;
}

6.结果验证

  • cowtest
    2022 xv6 lab: COW实验(copy-on-write)代码实现_第2张图片

  • usertests -q
    2022 xv6 lab: COW实验(copy-on-write)代码实现_第3张图片

7.遇到的一些问题

  • 在开辟计数数组那一块,纠结了一下是直接申请数组还是在end处开始直接维护一段连续内存。主要是不太明白直接在内核代码中申请数组,那么这个数组会被储存到哪里,之后复习lab book后发现,这个数组会被储存在kernel data中去,而end也会随之增加。
    2022 xv6 lab: COW实验(copy-on-write)代码实现_第4张图片

  • usertests中增加了一个难缠的测试函数textwrite,这个也是去年lab没有的,困扰了我很久,然后发现其实这个函数是新增了一个对cow的bug的检查

    子进程copy完父进程的页表后,会将每一页的pte的pte_W置0,pte_C置1,我们就可以通过判断pte_W和pte_C判断该页是不是cow页
    2022 xv6 lab: COW实验(copy-on-write)代码实现_第5张图片

    那么随之也会出现一个bug,每个用户进程的低地址段都会用于储存代码(即text区域),根据book描述,这一段本来也没有pte_W标志。那么如果我们对其进行COW操作就会引发一系列的错误

    所以在usertrap中对这一bug直接拦截

    if(r_stval() < PGSIZE)
    	p->killed = 1;
    

8.参考文章

  • https://ttzytt.com/2022/07/xv6_lab6_record/
  • https://blog.csdn.net/lhwhit/article/details/120669827?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166591619116800180625307%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=166591619116800180625307&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-120669827-null-null.142v56control_1,201v3control_2&utm_term=xv6%20cow&spm=1018.2226.3001.4187

你可能感兴趣的:(xv6,操作系统,嵌入式软件,linux,risc-v)