当前位置: 首页 > news >正文

MIT6.828 Lab5 Copy-on-Write Fork for xv6

TOC

实验内容

  • 背景:
    • 在 xv6 中,fork()系统调用会将父进程的所有用户空间内存复制到子进程中。如果父进程很大,复制操作可能会花费很长时间。
    • 在子进程中,fork()通常会紧接着 exec(),这会丢弃复制的内存,通常不会使用大部分内存。
  • 解决方案:
    • 延迟分配
    • 写时复制
    • COW fork() 为子进程创建一个页表,用户内存的 PTE 指向父进程的物理页。COW fork() 将父进程和子进程中的所有用户 PTE 标记为只读。当任一进程尝试写入这些 COW 页面时,CPU 将引发页面故障。内核页面故障处理程序检测到这种情况,为故障进程分配一页物理内存,将原始页面复制到新页面,并在故障进程的相关 PTE 中修改为指向新页面,这次 PTE 标记为可写。当页面故障处理程序返回时,用户进程将能够写入其页面的副本。
  • 具体任务
    • 在 xv6 内核中实现copy-on-write fork。如果你修改后的内核能够成功执行 cowtest和usertests -q程序,那么你就完成了任务。

实现逻辑

在这里插入图片描述

修改内容

  • kernel/riscv.h中添加PTE_COW标志位
#define PTE_COW (1L << 8) //自定义COW标志位 lab5
  • kernel/kalloc.c添加页映射计数数组、相关函数(修改时使用锁)
// lab5
struct {
   struct spinlock lock;    // 引用计数专用锁
   int count[(PHYSTOP - KERNBASE) / PGSIZE];  // 引用计数数组
}kref;
// 增加引用计数
void kref_incr(uint64 pa) {
  acquire(&kref.lock);
  int idx = (pa - KERNBASE) / PGSIZE;
  kref.count[idx]++;
  release(&kref.lock);
}
// 减少引用计数
void kref_decr(uint64 pa) {
  int idx = (pa - KERNBASE) / PGSIZE;
  acquire(&kref.lock);
  if (kref.count[idx] <= 0) panic("kref_decr: invalid refcount");
  kref.count[idx]--;
  release(&kref.lock);
}
// 查询引用次数
int kref_count(uint64 pa) {
  acquire(&kref.lock);
  int idx = (pa - KERNBASE) / PGSIZE;
  release(&kref.lock);
  return kref.count[idx];
}
  • kernel/kalloc.c中修改初始化、分配页和释放页函数
void
kinit()
{
  // lab5 初始化计数数组
  for (int i = 0; i < (PHYSTOP - KERNBASE) / PGSIZE; i++) 
     kref.count[i] = 0;
  initlock(&kref.lock, "kref");
  initlock(&kmem.lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}

void
kfree(void *pa)
{
  struct run *r;
  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  // lab5
  acquire(&kmem.lock);
  if ( kref_count((uint64)pa) > 1) {
    kref_decr((uint64)pa);  // 引用计数减 1
    release(&kmem.lock);
    return;
  }
  acquire(&kref.lock);
  kref.count[((uint64)pa - KERNBASE) / PGSIZE] = 0;  // 清零后释放  
  release(&kref.lock);

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

void *
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r){
    kmem.freelist = r->next;
    // lab5
    acquire(&kref.lock);
    kref.count[((uint64)r - KERNBASE) / PGSIZE] = 1;  // 初始化为 1    
    release(&kref.lock);
  }
  release(&kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}
 - 只有当页的映射数为1时才能释放
  - 分配页时增加数组计数
  • kernel/vm.c中添加写时分配的实现形式
int cow_alloc(pagetable_t pagetable, uint64 va) {
  va = PGROUNDDOWN(va);  // 对齐到页边界
  // 检查va大小,在walk之前,否则无法应对恶意输入地址
  if(va >= MAXVA)
    return -1;
  pte_t *pte = walk(pagetable, va, 0);
  if (!pte || !(*pte & PTE_V) || !(*pte & PTE_COW)) {
    return -1;  // 非法地址或非 COW 页
  }

  uint64 pa = PTE2PA(*pte);  // 原物理地址
  // 如果引用计数为 1,直接恢复写权限
  if (kref_count(pa) == 1) {  
    *pte |= PTE_W;
    *pte &= ~PTE_COW;
    sfence_vma(); // 刷新 TLB
    return 0;
  }

  // 分配新页并复制数据
  char *new_pa = kalloc();
  if (!new_pa) return -1;  // 内存不足

  memmove(new_pa, (char*)pa, PGSIZE);  // 复制原页内容
  // 更新页表项:新页可写,清除 COW 标志
  uint64 flags = (PTE_FLAGS(*pte) | PTE_W ) & ~PTE_COW;
  *pte = PA2PTE(new_pa) | flags;

  kfree((void*)pa);  // 减少原页的引用计数
  sfence_vma(); // 刷新 TLB
  return 0;
}
  • kernel/vm.c中的修改
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");

    // lab5 只考虑可写页面标记cow,否则会使不可写的页在cow处理后变为可写!
    if(*pte & ~PTE_W){
      *pte = (*pte & ~PTE_W) | PTE_COW; // 清除父进程的写权限,标记为 COW 页
    }
    pa = PTE2PA(*pte); // 获取物理地址
    flags = PTE_FLAGS(*pte);
    kref_incr(pa); //增加引用计数
    // 子进程页表继承只读 + COW 标志,映射到同一物理页
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      kref_decr(pa);
      goto err;
    }    
  }
  return 0;

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

int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;
  pte_t *pte;

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    if(va0 >= MAXVA)
      return -1;
    pte = walk(pagetable, va0, 0);
    // lab5去掉pte_w检查,进行常规检查
    if(pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0 ) 
      return -1;
    // 禁止写入代码页(PTE_X)
    if (*pte & PTE_X) {
      return -1;
    }
    // lab5 
    if(*pte & PTE_COW){
      // cow页
      uint64 pa = PTE2PA(*pte); // 对应的物理页地址
      if(kref_count(pa) > 1){
        // 该物理页被多次映射,需重新分配
        if( cow_alloc(pagetable, va0) < 0 ){
          return -1;
        }
        // 获取新的pte
        pte = walk(pagetable, va0, 0);
        if( pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0 || (*pte & PTE_W) == 0){
          return -1;
        }
        sfence_vma(); // 刷新TLB
      }
      else{
        // 该物理页只有一次映射
        *pte |= PTE_W;
        *pte &= ~PTE_COW;
        sfence_vma(); // 刷新TLB
      }
    } else {
      // 非 COW 页:检查是否可写
      if ((*pte & PTE_W) == 0) {
        return -1; // 目标页不可写,返回错误
      }
    }

    
    pa0 = PTE2PA(*pte);
    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;
}
  • kernel/trap.c中添加陷入处理
void
usertrap(void)
{
  int which_dev = 0;

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

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();
  
  // save user program counter.
  p->trapframe->epc = r_sepc();
  
  if(r_scause() == 8){
    // system call

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

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sepc, scause, and sstatus,
    // so enable only now that we're done with those registers.
    intr_on();

    syscall();
  } else if( r_scause() == 15 ){ //lab5 页写入错误 // 写时错误
    uint64 va = r_stval();         // 获取触发错误的虚拟地址
    // 检查虚拟地址是否合法(在用户地址空间内)
    if (va >= p->sz || va < PGSIZE) {
      printf("usertrap: invalid va %p\n", va);
      setkilled(p);
    }
    // 处理 COW 错误(分配新页并更新页表)
    if (cow_alloc(p->pagetable, va) < 0) {   // 调用 COW 处理函数
      printf("usertrap: cow_alloc failed\n");
      setkilled(p);
    }

  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    setkilled(p);
  }

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

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

测试结果

$cowtest
simple: ok
simple: ok
three: ok
three: ok
three: ok
file: ok
ALL COW TESTS PASSED
$usertests -q
...
test sbrklast: OK
test sbrk8000: OK
test badarg: OK
ALL TESTS PASSED
http://www.dtcms.com/a/123185.html

相关文章:

  • BUUCTF-web刷题篇(19)
  • Vulhub-DC-3靶机通关攻略
  • 【MySQL】——理解事务的隔离性、MVCC、当前读、快照读
  • 力扣 922. 按奇偶排序数组 II :双指针原地交换
  • 2025年第十八届“认证杯”数学中国数学建模网络挑战赛【ABCD题】思路分析
  • Java 实现 List<String> 与 String 互转
  • CentOS Stream release 9安装 MySQL(二)
  • 在Halcon的语义分割中,过度拟合解决方法
  • 什么是中性线、零线、地线,三相四线制如何入户用电
  • JVM底层详解
  • ANTLR v4 中文文档1:语法词典Grammar Lexicon部分
  • NginxWebUI:可视化 Nginx 配置管理工具,告别繁琐命令行!
  • 高并发内存池(三):PageCache(页缓存)的实现
  • Window代码向Linux平台移植问题汇总
  • pytorch TensorDataset与DataLoader类
  • GRBL运动控制算法(五)脉冲生成Bresenham算法
  • 基于 Spring Boot 瑞吉外卖系统开发(三)
  • HackMyVM - todd记录
  • 编译器bug ?
  • (十二)安卓开发中FrameLayout的使用方法详解
  • 在计算极限的时候泰勒公式到底展开到第几项
  • 阿里云CDN与DCDN主动推送静态资源至边缘服务器的ASP.NET WEB实例
  • KaiwuDB 数据库基本概念解析
  • 山东大学软件学院创新项目实训开发日志(9)之测试前后端连接
  • 计算机组成原理 原码乘法 补码乘法
  • 体验OceanBase的 并行导入功能
  • 631SJBH中小型企业的网络管理模式的方案设计
  • AI日报 - 2025年4月11日
  • 44、Spring Boot 详细讲义(一)
  • 共享内存(与消息队列相似)