首页 > 其他分享 >Mit6.S081笔记Lab6: Lab6: Copy-on-Write Fork for xv6 写时复制

Mit6.S081笔记Lab6: Lab6: Copy-on-Write Fork for xv6 写时复制

时间:2024-11-07 14:43:41浏览次数:4  
标签:Fork 引用 COW Mit6 PTE Lab6 进程 pte 物理

课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.html
Lab 地址:https://pdos.csail.mit.edu/6.S081/2020/labs/cow.html
我的代码地址:https://github.com/Amroning/MIT6.S081/tree/cow
xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf
相关翻译:http://xv6.dgs.zone/labs/requirements/lab6.html
参考博客:https://blog.miigon.net/posts/s081-lab6-copy-on-write-fork/

学习的笔记记录,如有错误恳请各位大佬指正

Lab6: Copy-on-Write Fork for xv6

​ 本次实验还是利用缺页故障实现一个有趣的机制:写时复制(Copy-on-Write,COW)。

​ 在原始的XV6中,fork函数是通过直接对进程的地址空间完整地复制一份来实现的。但是,拷贝整个地址空间是十分耗时的,并且在很多情况下,程序立即调用exec函数来替换掉地址空间,导致fork做了很多无用功

​ 该实验的改进:COW fork()只为子进程创建一个页表,用户内存的PTE指向父进程的物理页。COW fork()将父进程和子进程中的所有用户PTE标记为不可写。当任一进程试图写入其中一个COW页时,CPU将强制产生页面错误。内核页面错误处理程序检测到这种情况,将为出错进程分配一页物理内存,将原始页复制到新页中,并修改出错进程中的相关PTE指向新的页面,将PTE标记为可写。当页面错误处理程序返回时,用户进程将能够写入其页面副本。

​ COW fork()将使得释放用户内存的物理页面变得更加棘手。给定的物理页可能会被多个进程的页表引用,并且只有在最后一个引用消失时才应该被释放,所以针对这个问题需要有引用计数机制

Lab

​ 仅复制题目部分内容,完整要求请去该帖子顶部链接查看

Implement copy-on write (hard)

您的任务是在xv6内核中实现copy-on-write fork。如果修改后的内核同时成功执行cowtestusertests程序就完成了。

​ 首先给页表添加一个新的标志位,代表这个页是否是写时复制页。其他的标志位就是PTE_WPTE_R这些,这里新的标志位就叫PTE_COW,以下就把写时复制页称为COW页:

// kernel/riscv.h
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_COW (1L << 8) 
//是否为COW页,使用页表项 flags 中保留的第 8 位表示(页表项 flags 中,第 8、9、10 位均为保留给操作系统使用的位,可以用作任意自定义用途)

​ 父进程使用fork创建子进程时,会调用uvmcopy,把父进程映射的物理页全都复制一份新的给子进程。写时复制是写的时候才复制,所以这里去掉复制的功能,只给对应的页添加上PTE_COW标志位。这里原本父进程中只读的页,不做修改,也就是说,只读的页在父进程和子进程中一直都会是共享的。只有原本是可写的页做修改:

// kernel/vm.c
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
//   char *mem;			//用不到了,去掉,避免编译报错

  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);

    // 清除父进程中所有PTE的PTE_W位,并且设置PTE_COW位,表示所在页是一个写时复制页(多个进程引用同个物理页)
    // 如果该页本身就是不可写(只读),则不会添加这个标志位
    if (*pte & PTE_W)
        *pte = (*pte & ~PTE_W) | PTE_COW;

    flags = PTE_FLAGS(*pte);        //获取当前父进程pte的标志位

    //将父进程映射的物理页直接map到子进程中,权限保持和父进程一致(注意现在都是不可写,而且原本是可写的页会有新增的PTE_COW写时复制标志)
    if (mappages(new, i, PGSIZE, (uint64)pa, flags) != 0)
        goto err;

    krefpage((void*)pa);         //映射的物理页的引用数+1

    // 去掉以下复制内存的代码
    // if((mem = kalloc()) == 0)
    //   goto err;
    // memmove(mem, (char*)pa, PGSIZE);
    // if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
    //   kfree(mem);
    //   goto err;
    // }
  }
  return 0;

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

krefpage函数是使物理页引用数+1。为了避免内存泄漏,多个进程映射同一个物理页的时候,记录一下这个物理页的引用数,调用kfree释放物理页的时候,只对引用数-1,当引用数为0的时候,才释放物理页。这里先有个概念,krefpage函数下面实现。

​ 现在使用fork函数不会复制内存了,父进程和子进程都映射到同一片物理内存,且所有的页都是不可写的状态。这个时候某一个进程尝试执行写操作,就会出现页面错误被usertrap捕获。接下来修改usertrap函数,实现在修改页的时候,才创建和映射新的物理页

​ 和上一个实验惰性分配类似,在usertrap中添加对页面错误的检测,如果当前访问的地址符合COW页的条件时,就进程复制内存操作:

// kernel/trap.c
void
usertrap(void)
{
  ......
    
  } else if((which_dev = devintr()) != 0){
    // ok
  }else if ((r_scause() == 13 || r_scause() == 15) && uvmcheckcowpage(r_stval())) {
      // 发生页面错误,并且检测出错误是写时复制机制导致的页面不可写,则执行写时复制
      if (uvmcowcopy(r_stval()) == -1)
          p->killed = 1;
  }else {
      printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }

  ......
}

​ 调用r_scause判断是否是页面错误,实现一个uvmcheckcowpage函数判断是否是COW机制导致的页面错误,符合这些条件,再实现一个uvmcowcopy函数执行写时复制操作。

​ 实现uvmcheckcowpage函数,检查这个地址是否在COW页:

// kernel/vm.c
//检查虚拟地址所在页是否是COW页
int uvmcheckcowpage(uint64 va) {
    pte_t* pte;
    struct proc* p = myproc();

    return va < p->sz
        && ((pte = walk(p->pagetable, va, 0)) != 0)
        && (*pte & PTE_V)
        && (*pte & PTE_COW);
    //地址在进程内存范围内  &&  地址有映射  && 地址有效且是COW页
}

​ 实现uvmcowcopy函数,复制一个新的物理页,创建新的映射。上面说到原本是只读的页不会添加PTE_COW标志位,所以恢复可写权限时,要检查一下pte是否有PTE_COW标志位,也就是说,原本是只读的页不会有PTE_COW标志位,不用修改写权限;原本是可写的页会带有PTE_COW标志位(此时是不可写的状态),要恢复写权限,并清除掉PTE_COW标志位:

// kernel/vm.c
// 实现写时复制
int uvmcowcopy(uint64 va) {
    pte_t* pte;
    struct proc* p = myproc();

    if ((pte = walk(p->pagetable, va, 0)) == 0)     //获取虚拟地址的页表项
        panic("uvmcowcopy: walk");

    uint64 pa = PTE2PA(*pte);                     //获取映射的物理地址
    uint64 new = (uint64)kcopy_n_deref((void*)pa);  //获取新分配的物理页(如果原本的物理页引用数为1,则获取到的还是原本的物理页)
    if (new == 0)       //内存不足的情况
        return -1;

    //修改新的映射,若当前页有COW标识,权限修改为可写,清除COW标识
    uint64 flags = PTE_FLAGS(*pte);
    if (*pte & PTE_COW)
        flags = (PTE_FLAGS(*pte) | PTE_W) & ~PTE_COW;
    uvmunmap(p->pagetable, PGROUNDDOWN(va), 1, 0);              //清除旧的映射
    if (mappages(p->pagetable, va, 1, new, flags) == -1)        //新的映射
        panic("uvmcowcopy: mappages");

    return 0;
}

kcopy_n_deref函数的思路是,当有进程通过COW复制新的物理页时,把原本物理页的引用数-1,返回新创建的物理页地址。如果原本的物理页引用数就是1了,那可以直接使用这个物理页了,因为也没有其他进程会使用这个物理页了。kcopy_n_deref函数下面实现。

copyout函数是软件访问页表,不会触发缺页异常,这里添加上检测代码,检测复制的页是否是一个COW页,是的话就执行复制操作:

// kernel/vm.c
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
    uint64 n, va0, pa0;

    while (len > 0) {

        if (uvmcheckcowpage(dstva))     //检查每一个页是否为COW页
            uvmcowcopy(dstva);

        va0 = PGROUNDDOWN(dstva);
        pa0 = walkaddr(pagetable, va0);
        ......
}

​ 到此,已实现了大致的逻辑:调用fork的时候不复制内存,只是把子进程的映射和父进程保持一致,并把可写的页变为不可写,添加PTE_COW标志位。某一进程进行写操作时产生页面错误,此时才调用相关函数实现复制内存,并把新的页面恢复写权限。

​ 接下来需要对页的生命周期进行管理,确保页没有被任何进程使用的时候才释放

​ 原本的xv6中,一个物理页只会在一个进程中有映射,kalloc用来分配物理页,kfree用来回收物理页。在COW机制中,一个物理页会在多个进程中有映射,所以要在最后一个映射释放的时候,才真正释放回收该物理页,结合这些,需要实现以下操作:

  • kalloc: 分配物理页,将其引用数置为 1
  • krefpage: 创建物理页的一个新映射的时候,引用数+1
  • kcopy_n_deref: 将原物理页的数据复制到一个新物理页上(引用数为 1),返回得到的新物理页;并将原物理页的引用数-1
  • kfree: 释放物理页的一个映射,引用数减 1;如果引用数变为 0,则释放回收物理页

​ 一个物理页首先会被父进程使用 kalloc创建,fork 的时候,新创建的子进程的时候会调用krefpage 声明对应物理页的引用数+1。当尝试修改父进程或子进程中的页时,kcopy_n_deref 负责将想要修改的页复制到独立的副本,并记录解除旧的物理页的映射(引用数-1)。最后 kfree 保证只有在所有的引用者都释放该物理页的映射时,才释放回收该物理页

​ 在kallo.c定义一些相关的宏:

#define PA2PGREF_ID(p) (((p)-KERNBASE)/PGSIZE)      // 由物理地址获取物理页id
#define PGREF_MAX_ENTRIES PA2PGREF_ID(PHYSTOP)      // 物理页数上限

int pageref[PGREF_MAX_ENTRIES];         //每个物理页的引用数 数组(pageref[i]表示第i个物理页的引用数)
struct spinlock pgreflock;              //用于pageref数组的锁,防止竞态条件引起内存泄漏

#define PA2PGREF(p) pageref[PA2PGREF_ID((uint64)(p))]       //获取地址对应物理页引用数

​ 初始化锁:

void
kinit()
{
    initlock(&kmem.lock, "kmem");
    initlock(&pgreflock, "pgref");      //初始化锁
    freerange(end, (void*)PHYSTOP);
}

​ 修改kfree

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

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("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

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
    PA2PGREF(r) = 1;        //将新分配的物理页的引用数设置为1(刚分配的页还没映射,不会有进程来使用,不用加锁)
  }
  return (void*)r;
}

​ 实现krefpage

//物理页引用数+1
void krefpage(void* pa) {
    acquire(&pgreflock);
    PA2PGREF(pa)++;
    release(&pgreflock);
}

​ 实现kcopy_n_deref

// 写时复制一个新的物理地址返回
// 如果该物理页的引用数>1,则将引用数-1,并分配一个新的物理页返回
// 如果引用数 <=1,则无需操作直接返回该物理页
void* kcopy_n_deref(void* pa) {
    acquire(&pgreflock);

    //当前物理页的引用数为1,无需分配新的物理页
    if (PA2PGREF(pa) <= 1) {
        release(&pgreflock);
        return pa;
    }

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

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

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

​ 这里定义了一个自旋锁pgreflock,通过acquire(&pgreflock)release(&pgreflock)获取锁和释放锁来保护操作的代码,锁的作用是防止竞态条件下导致的内存泄漏

比如,父进程的一个物理页p,此时引用数是1。

父进程执行fork创建子进程,此时p的引用数是2。

父进程执行写操作,触发页面异常。因为p的引用数大于1,开始执行复制p的操作(p的引用数还是2)。

进程调度,切换到子进程(父进程复制操作中断)。子进程马上执行exec,释放所有旧的物理页,物理页引用数-1(p的引用数为1)。

进程调度,切换到父进程。父进程继续复制p的数据到新的物理页。复制完毕,p的引用数-1。之后回到uvmcowcopy函数中,通过uvmunmap解除了对p的映射。物理页p并没有被释放回收,所有进程都丢失了对p的映射(页表中均没有指向 p 的页表项),造成内存泄漏

​ 加上锁之后,可以防止这个现象发生。在使用kalloc分配内存时可以不用加锁,因为这个地址还没返回,没有哪个进程可以操作这个新分配的地址。

​ 此时可以执行make grade验证实验是否完成。

PS:有一个 Cannot read time.txt 的检查项目,应该是原课程要求学生上传一个time.txt文件说明完成实验花了多长时间。在项目主目录xv6-labs-2020/创建这个文件,随便写一个数字就能通过这个检查项

标签:Fork,引用,COW,Mit6,PTE,Lab6,进程,pte,物理
From: https://www.cnblogs.com/Amroning/p/18532220

相关文章

  • Mit6.S081笔记:页表笔记
    xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf相关翻译:http://xv6.dgs.zone/labs/requirements/lab5.html感觉页表很多地方没理解,学习的时候把一些关键地方记录起来,如有错误恳请各位大佬指正。页表笔记​​ 页表是操作系统为每个进程提供私有地址......
  • Mit6.S081笔记Lab5: Lazy Page Allocation 惰性分配
    课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.htmlLab地址:https://pdos.csail.mit.edu/6.S081/2020/labs/lazy.html我的代码地址:https://github.com/Amroning/MIT6.S081/tree/lazyxv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf相关翻......
  • 《Linux系统编程篇》fork/wait/waitpid/exit函数——基础篇
    文章目录引言fork()函数概述父子进程兄弟进程fork函数fork()的常见问题fork()的优势与限制引入`wait`和`waitpid`(解决僵尸进程)wait函数waitpid函数:exit函数结论命为志存。——朱熹引言《Linux系统编程篇》——基础篇首页传送门本节我们正式进入Linux的进......
  • github fork 及 pull requests 流程
    1.从原始仓库fork到自己的仓库 2.clone自己仓库的代码到本地gitclonehttps://xxxx.git 3.添加upstream(git地址为原始仓库地址,添加后可对原始仓库进行拉取和推送)gitremoteaddupstreamhttps://xxx.git 4.本地拉取原始仓库最新代码gitfetchupstream......
  • HNU-操作系统实验lab6-2022级
    实验目的任务调度是操作系统的核心功能之一。UniProton实现的是一个单进程支持多线程的操作系统。在UniProton中,一个任务表示一个线程。UniProton中的任务为抢占式调度机制,而非时间片轮转调度方式。高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务挂起或......
  • 如何安全彻底地删除GitHub上的fork项目
    GitHub提供了一个功能,允许用户fork其他开发者的项目,但在某些情况下,我们可能需要删除fork的项目。本文将指导您如何安全、彻底地进行此操作:1.确认删除的必要性;2.备份重要数据;3.删除项目;4.检查与原项目的联系;5.清除本地仓库。删除GitHub上的fork项目是一个相对简单的过程,但在执行之......
  • Mit6.S081笔记Lab3: page tables 页表
    课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.htmlLab地址:https://pdos.csail.mit.edu/6.S081/2020/labs/pgtbl.html我的代码地址:https://github.com/Amroning/MIT6.S081/tree/pgtbl相关翻译:http://xv6.dgs.zone/labs/requirements/lab3.html参考博客:https://ww......
  • Mit6.S081笔记Lab2: system calls 系统调用
    课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.htmlLab地址:https://pdos.csail.mit.edu/6.S081/2020/labs/syscall.html我的代码地址:https://github.com/Amroning/MIT6.S081/tree/syscall相关翻译:http://xv6.dgs.zone/labs/requirements/lab2.html参考博客:https:......
  • 【问题排查】-bash: fork: retry: Resource temporarily unavailable 记录一下
    ●最初现象,ssh端口异常,登录机器出现如下,执行每一个命令都会有这个错,最终就是关闭终端后无法在连接,只能重启释放资源●查看kernel.threads-max(每个进程中最多创建的的线程数目)●top-H查看每个线程的资源使用情况,发现达到了当前系统限制30938●修改threads-max,sy......
  • Fork/Join框架
    Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架packageforkjoin;importjava.util.concurrent.ExecutionException;importjava.util.concurrent.ForkJoinPool;importjava.util.co......