MIT 6.S08操作系统实验课 lab6 写时复制

提示:这是操作系统本人对 MIT 6.S081 的 lab6 实验课的笔记,仅供参考。


前言

参考资料:
资料1
资料2


提示:以下是本篇文章正文内容,下面案例可供参考

一、实验目标

实现一个写时复制操作,即在进程 fork 后,不立刻复制内存页,而是将子进程虚拟地址指向与父进程相同的物理地址。在父子任意一方尝试对内存页进行修改时,才对内存页进行复制。

二、实验

第一步
首先在文件kernel/riscv.h文件中设置一个标志位如下,用于区分该虚拟地址是否是写时复制页。页表项 flags 中,第 8、9、10 位可以由开发人员自定义用途。这里使用第8位来表示该地址是否是写时复制页。

#define PTE_COW (1L << 8) // 是否为写时复制页,使用页表项 flags 中保留的第 8 位表示

第二步
修改kernel/kalloc.c文件所有内存管理代码,修改后内容如下。该部分主要是开辟一个数组为 [KERNBASE, PHYSTOP) 区间的每一页物理内存建立一个引用计数,表示拥有该物理页的进程数量。

// Physical memory allocator, for user processes,
// kernel stacks, page-table pages,
// and pipe buffers. Allocates whole 4096-byte pages.

#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "riscv.h"
#include "defs.h"

// 用于访问物理页引用计数数组
#define PA2PGREF_ID(p) (((p)-KERNBASE)/PGSIZE)  // 这里是为[KERNBASE, PHYSTOP)之间的物理页建立索引
#define PGREF_MAX_ENTRIES PA2PGREF_ID(PHYSTOP)

struct spinlock pgreflock; // 用于 pageref 数组的锁,防止竞态条件引起内存泄漏
int pageref[PGREF_MAX_ENTRIES]; // 从 KERNBASE 开始到 PHYSTOP 之间的每个物理页的引用计数,这是一个很大的数组

// 通过物理地址获得引用计数
#define PA2PGREF(p) pageref[PA2PGREF_ID((uint64)(p))]

extern char end[]; // first address after kernel.
                   // defined by kernel.ld.

void freerange(void *pa_start, void *pa_end);

struct run {
  struct run *next;
};

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem;

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

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);
}

// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc().  (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");
  
  acquire(&pgreflock);  // 获取锁
  if(--PA2PGREF(pa) <= 0){  // 如果当前的引用计数小于等于 0,就释放该页
    memset(pa, 1, PGSIZE);

    r = (struct run*)pa;

    // 将释放的页加入到空闲链表中
    acquire(&kmem.lock);
    r->next = kmem.freelist;
    kmem.freelist = r;
    release(&kmem.lock);
  }
  release(&pgreflock);
}

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
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;
}

// Decrease reference to the page by one if it's more than one, then
// allocate a new physical page and copy the page into it.
// (Effectively turing one reference into one copy.)
// 
// Do nothing and simply return pa when reference count is already
// less than or equal to 1.
// 
// 当引用已经小于等于 1 时,不创建和复制到新的物理页,而是直接返回该页本身
void *kcopy_n_deref(void *pa) {
  acquire(&pgreflock);

  if(PA2PGREF(pa) <= 1) { // 只有 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);
}

第三步
修改kernel/vm.c文件uvmcopy函数:该函数主要在未修改前,主要用于在fork函数中子进程拷贝父进程的页表项。该函数在修改后,用与在复制父进程的内存到子进程的时候,不立刻复制数据,而是建立指向原物理页的映射,并将父子两端的页表项都设置为不可写。修改后内容如下:

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

    if(*pte & PTE_W){
      // 清除父进程的 PTE_W 标志位,设置 PTE_COW 标志位表示是一个懒复制页(多个进程引用同个物理页)
      *pte = (*pte & ~PTE_W) | PTE_COW;
    }

    flags = PTE_FLAGS(*pte);  // 获取当前页表的标志位
    // if((mem = kalloc()) == 0)
    //   goto err;
    // memmove(mem, (char*)pa, PGSIZE);
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      // kfree(mem);
      goto err;
    }

    krefpage((void*)pa); // 将物理页的应用数加1
  }
  return 0;

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

第四步
需要添加两个函数,用于在产生page fault错误时,检查当前虚拟地址是否符合写时复制的页以及进行写时复制操作。在kernel/vm.c添加如下函数,在kernel/defs.h添加函数声明。

// 检查一个地址指向的页是否是懒复制页
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); // 页是一个懒复制页
}

// 实复制一个懒复制页,并重新映射为可写
int uvmcowcopy(uint64 va) {
  pte_t *pte;
  struct proc *p = myproc();

  if((pte = walk(p->pagetable, va, 0)) == 0)
    panic("uvmcowcopy: walk");
  
  // 调用 kalloc.c 中的 kcopy_n_deref 方法,复制页
  // (如果懒复制页的引用已经为 1,则不需要重新分配和复制内存页,只需清除 PTE_COW 标记并标记 PTE_W 即可)
  uint64 pa = PTE2PA(*pte);
  uint64 new = (uint64)kcopy_n_deref((void*)pa); // 将一个懒复制的页引用变为一个实复制的页
  if(new == 0)
    return -1;
  
  // 重新映射为可写,并清除 PTE_COW 标记
  uint64 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;
}

第五步
修改kernel/trap.c文件中的usertrap函数,增加对page fault的检测,并将符合写时复制的内存进行内存拷贝操作。

void
usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");
    
 // ..........

    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else if((r_scause() == 13 || r_scause() == 15) && uvmcheckcowpage(r_stval())){  // 处理page fault
    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;
  }

  if(p->killed)
    exit(-1);

  // .........................

  usertrapret();
}

第六步
修改kernel/vm.c文件中的copyout函数,该函数主要用于从内核空间向用户空间传递数据,需要进行写操作,因此需要检测写入地址是否是写时复制页,如果是,就要进行内存拷贝。修改后代码如下:

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

至此,所有操作步骤完成,输入make命令进行编译,make qemu启动系统,输入make grade命令进行测试。


  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值