Yra's blog Yra's blog
Homepage
  • LeetCode周赛
  • Acwing周赛
  • 刷题整理
  • 题解
  • CPP
  • Golang
  • System

    • MIT6.S081 Labs
  • Computer Networking

    • CS144: Computer Networking
  • DataBase

    • MySQL
    • Redis
  • Others

    • ......
  • Paper Reading

    • Petri Net
  • Static Analysis

    • NJU Course Notes
  • Deep Learning

    • Dive into Deep Learning
Casual Records
Archives

Yra

Only a vegetable dog.
Homepage
  • LeetCode周赛
  • Acwing周赛
  • 刷题整理
  • 题解
  • CPP
  • Golang
  • System

    • MIT6.S081 Labs
  • Computer Networking

    • CS144: Computer Networking
  • DataBase

    • MySQL
    • Redis
  • Others

    • ......
  • Paper Reading

    • Petri Net
  • Static Analysis

    • NJU Course Notes
  • Deep Learning

    • Dive into Deep Learning
Casual Records
Archives
  • System

    • MIT6.S081 | 21Fall

      • Lab1: Unix utilities
      • Lab2: System Calls
      • Lab3: Page Tables
      • Lab4: Traps
      • Lab5: Copy-on-Write Fork for xv6
        • Lab6: Multithreading
        • Lab7: Networking
        • Lab8: Locks
        • Lab9: File System
        • Lab10: Mmap
    • Computer Networking

    • DataBase

    • Software Engineering

    • Others

    • Learning Notes
    • System
    • MIT6.S081 | 21Fall
    Yra
    2023-02-17
    目录

    [MIT6.S081]Lab5: Copy-on-Write Fork for xv6

    # [MIT6.S081]Lab5: Copy-on-Write Fork for xv6

    Lab: Copy-on-Write Fork for xv6 (mit.edu) (opens new window)

    # 1、Implement copy-on write

    这个 lab 中我们要实现写时复制。

    我们知道当一个进程 fork 出一个子进程后,子进程会复制父进程的所有内容,那么此时如果子进程执行了 exec,则会将刚刚复制完的内容又全部重置。我们知道拷贝是非常耗时的,如果每次像这样复制必然会产生许多的浪费。

    于是便有了写时复制,在 fork 出一个子进程后,我们不进行拷贝,而是将父子进程的页表映射到同一块物理内存,并将 PTE_W 都置为 0,当父子进程需要写这个地址时,便会产生 page fault,在遇到这种情况后我们才进行实际的拷贝操作。

    接下来我们开始写代码 💻

    因为此时同一块物理可能被多个进程所共享,那么我们就不能随意释放内存,而是要引入一个新的变量,作为引用计数,类似于 C++ 里的智能指针,只要当一块内存的引用计数为 0 了,才会释放该内存。

    在 kalloc.c 中添加引用计数变量

    int ref_cnt[(PHYSTOP - KERNBASE) / PGSIZE];
    

    我们对应的在新分配一块内存时也要将引用计数初始化为 1

    void *kalloc(void) {
      struct run *r;
    
      acquire(&kmem.lock);
      r = kmem.freelist;
      if(r)
        kmem.freelist = r->next;
      release(&kmem.lock);
    
      if(r) {
        ref_cnt[pageid(r)] = 1; // 初始化引用计数为 1 
        memset((char*)r, 5, PGSIZE); // fill with junk
      }
      return (void*)r;
    }
    
    

    在释放内存的时候,如果引用计数 > 1 时,就将其减 1,否则再释放

    void kfree(void *pa) {
      struct run *r;
    
      if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
        panic("kfree");
    
      if (ref_cnt[pageid(pa)] > 1) { // 引用计数减 1
        ref_cnt[pageid(pa)]--;
        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);
    }
    

    之后我们要对 uvmcopy 函数进行修改,当 fork 出一个子进程后,我们要让父子进程共享同一块物理内存,并将 PTE_W 设置为 0。同时为了标记一个页面是写时复制页面,我们要给 PTE 打一个新的标记 PTE_COW,这里可以用到 RSW 位。

    在 riscv.h 中添加

    #define PTE_COW (1L << 8)
    

    回到 uvmcopy,我们遍历父进程的页表,对每个 PTE 设置其标记位和获取对应的物理地址 pa,接着我们给新进程的页表的每一个同样位置的 PTE,与 pa 也进行映射,并且引用计数 + 1

    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 &= ~PTE_W; // 将 PTE_W 位 置为 0
        *pte |= PTE_COW; // 设置 PTE_COW 位
        pa = PTE2PA(*pte);
        flags = PTE_FLAGS(*pte);
        if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
          panic("uvmcopy: mappages");
          goto err;
        }
        ref_cnt[pageid(pa)]++;
      }
    
      return 0;
      err:
      uvmunmap(new, 0, i / PGSIZE, 1);
      return -1;
    }
    

    然后我们进入 trap.c 来对这种 page fault 进行处理。

    根据文档,我们知道此时对应的异常码是 15

    image-20230218192843147

    另外我们还要检查虚拟地址是否合法,指向的 PTE 是否是一个 cow 页面...,如果都符合我们就根据实验文档所说的 copy the old page to the new page, and install the new page in the PTE with PTE_W set.

    void usertrap(void) {
    //......
    else if ((r_scause() == 15) && checkcow(r_stval())) { 
        if (cow_fault(r_stval()) != 0) {
          panic("usertrap: cow_fault");
          p->killed = 1;
        }
    //......
    }
    

    下面是检查虚拟地址的代码 checkcow:「具体看注释」

    int checkcow(uint64 va) {
      pte_t *pte;
      struct proc *p = myproc();
      if (va >= MAXVA) return 0; // 虚拟地址是否在合法范围内
      if ((pte = walk(p->pagetable, va, 0)) == 0) return 0; // 是否能找到对应的 PTE
      if (!(*pte & PTE_V) || !(*pte & PTE_COW)) return 0; // PTE_V 有效位是否被设置,PTE_COW 是否被设置
      return 1;
    }
    

    下面是实际拷贝的代码 cow_fault: 「具体看注释」

    int cow_fault(uint64 va) {
      va = PGROUNDDOWN(va); // 要 page-aligned
      pte_t *pte;
      struct proc *p = myproc();
      if ((pte = walk(p->pagetable, va, 0)) == 0) { // 获取 PTE
        panic("cow_fault: walk");
      }
      uint64 old = PTE2PA(*pte); // 获取 PTE 对应的物理地址
      uint64 new;
      uint64 flags = (PTE_FLAGS(*pte) | PTE_W) & ~PTE_COW; // 取消 PTE_COW 位,设置 PTE_W 位
      if ((new = (uint64)kalloc()) == 0) { // 分配新的物理内存
        panic("cow_fault: kalloc panic");
      }
      memmove((void *)new, (void *)old, PGSIZE); // 实际拷贝操作
      uvmunmap(p->pagetable, va, 1, 1); // 取消 PTE 与原来的物理内存的映射
      if (mappages(p->pagetable, va, PGSIZE, new, flags) != 0) { // 与 new 建立新的映射,并设置了新的标志位
        panic("cow_fault: mappages");
        kfree((void *)new);
        return -1;
      }
      return 0;
    }
    

    这样一来,copy-on-write 就基本完成了,另外根据实验文档,我们还要在 copyout 函数中运用写时复制。

    先对虚拟地址 va0 进行检查,如果符合则进行写时复制。

    int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len) {
      uint64 n, va0, pa0;
      while(len > 0){
        va0 = PGROUNDDOWN(dstva);
        if (checkcow(va0)) { // copy-on-write
          if (cow_fault(va0) != 0) {
            return -1;
          }
        }
        pa0 = walkaddr(pagetable, va0);
        if(pa0 == 0)
          return -1;
        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;
    }
    

    # 实验结果


    image-20230218194733488

    # TODO

    给页面上把读写锁,保证进程安全(不过好像测试用例没有这部分要求?)

    #Learning Notes#System#6.S081
    Last Updated: 3/6/2023, 8:57:26 PM
    Lab4: Traps
    Lab6: Multithreading

    ← Lab4: Traps Lab6: Multithreading→

    最近更新
    01
    408 计组笔记
    07-19
    02
    Dive into Deep Learning
    01-27
    03
    25 考研随记
    11-27
    更多文章>
    Theme by Vdoing | Copyright © 2022-2025
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式