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时,为写页异常
在这里插入图片描述

(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
    在这里插入图片描述

  • usertests -q
    在这里插入图片描述

7.遇到的一些问题

  • 在开辟计数数组那一块,纠结了一下是直接申请数组还是在end处开始直接维护一段连续内存。主要是不太明白直接在内核代码中申请数组,那么这个数组会被储存到哪里,之后复习lab book后发现,这个数组会被储存在kernel data中去,而end也会随之增加。
    在这里插入图片描述

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

    子进程copy完父进程的页表后,会将每一页的pte的pte_W置0,pte_C置1,我们就可以通过判断pte_W和pte_C判断该页是不是cow页
    在这里插入图片描述

    那么随之也会出现一个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
  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: 在xv6中,copy-on-write fork是一种优化技术,它可以在子进程创建时避免不必要的内存复制。具体来说,当父进程调用fork()创建子进程时,子进程会共享父进程的内存页表,而不是复制一份父进程的内存。只有当子进程尝试修改共享的内存时,才会发生实际的复制操作。这种技术可以减少内存使用和复制时间,提高系统性能。 ### 回答2: xv6是一个操作系统教学项目,这是一个现代化风格的UNIX第六版。copy-on-write fork是xv6实现的一种机制,它与fork系统调用有关。这种机制可减少在进行进程复制时所涉及的空间和时间开销,从而增加操作系统的效率。 在fork系统调用中,操作系统会复制原始进程,创建一个独立的进程。传统方法是,操作系统会将原有进程的内存空间全部复制一份给新进程,并在新进程中对地址进行修正。这样做会消耗大量的空间和时间,尤其是当进程较大时,复制整个内存空间会非常耗时。 copy-on-write fork的实现与传统方法不同。当原始进程需要创建新进程时,操作系统会将进程的内存空间标记为只读状态,并保留原内存页的映射关系。这样,当进程尝试写入内存时,操作系统将会产生一个缺页异常。在此时,操作系统会创建一个新页,将原内存页的内容复制到新页中,并在新页上进行写入操作。这样可以减少空间和时间开销,因为新页仅在需要写入时被复制,而不是在进程创建时。 copy-on-write fork有许多优点。首先,这种机制使系统更高效。使用copy-on-write fork可以显著降低进程复制的时间和空间开销。其次,这种机制还可以提高系统的可扩展性。当进程需要更多内存时,操作系统会重新映射新的内存页,而不是将整个进程复制一次。因此,系统可以更轻松地扩展。 总之,copy-on-write fork是xv6中非常有用的一个机制。它可以减少进程复制所需的时间和空间开销,从而提高操作系统的效率和可扩展性。 ### 回答3: 在操作系统课程xv6中,实现了一种名为“copy-on-write fork”的操作,这种操作可以让父进程和子进程在初始时共享相同的物理内存。当父进程或子进程试图修改内存时,内存页会被复制并分配新的物理内存,以避免父进程和子进程之间的竞争条件。 这种“copy-on-write”技术可以减少系统中的内存浪费,并且在分配内存时减少了复制操作,从而提高了系统的性能。在实现中,当父进程调用fork()创建一个新的子进程时,子进程将直接引用父进程的地址空间。父进程和子进程都共享相同的物理内存,但是它们各自有自己的页目录和页表来管理地址空间和虚拟内存。 当父进程或子进程尝试读取数据时,它们可以访问共享的物理内存。然而,当父进程或子进程试图修改数据时,操作系统会将所涉及的内存页复制到另一个物理内存地址,并使涉及的进程引用新的物理内存地址。这样,父进程和子进程将各自拥有自己的数据副本,一个进程修改数据不会影响另一个进程。 这种技术在许多操作系统中都有广泛应用,因为它可以提供更高效的内存管理和更好的性能。实现copy-on-write”fork操作在操作系统课程中具有教育意义,因为它可以让学生更深入了解xv6的内部机制和操作系统的基本理论。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值