MIT 6.S081 Lab Six

news2024/11/16 5:30:04

MIT 6.S081 Lab Six

  • 引言
  • Copy-on-Write Fork for xv6
    • 问题
    • 解决方案
    • Implement copy-on write (hard)
      • 代码解析
  • 可选的挑战练习


引言

本文为 MIT 6.S081 2020 操作系统 实验六解析。

MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列


Copy-on-Write Fork for xv6

虚拟内存提供了一定程度的间接寻址:

  • 内核可以通过将PTE标记为无效或只读来拦截内存引用,从而导致页面错误,还可以通过修改PTE来更改地址的含义。
  • 在计算机系统中有一种说法,任何系统问题都可以用某种程度的抽象方法来解决。
  • Lazy allocation实验中提供了一个例子。
  • 这个实验探索了另一个例子:写时复制分支(copy-on write fork)。

在开始本实验前,将仓库切换到cow分支

$ git fetch
$ git checkout cow
$ make clean

问题

xv6中的fork()系统调用将父进程的所有用户空间内存复制到子进程中。如果父进程较大,则复制可能需要很长时间。更糟糕的是,这项工作经常造成大量浪费;

  • 例如,子进程中的fork()后跟exec()将导致子进程丢弃复制的内存,而其中的大部分可能都从未使用过。
  • 另一方面,如果父子进程都使用一个页面,并且其中一个或两个对该页面有写操作,则确实需要复制。

解决方案

copy-on-write (COW) fork()的目标是推迟到子进程实际需要物理内存拷贝时再进行分配和复制物理内存页面。

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

COW fork()将使得释放用户内存的物理页面变得更加棘手。给定的物理页可能会被多个进程的页表引用,并且只有在最后一个引用消失时才应该被释放。


Implement copy-on write (hard)

YOUR JOB

  • 您的任务是在xv6内核中实现copy-on-write fork。如果修改后的内核同时成功执行cowtestusertests程序就完成了。
  • 为了帮助测试你的实现方案,我们提供了一个名为cowtest的xv6程序(源代码位于user/cowtest.c)。
  • cowtest运行各种测试,但在未修改的xv6上,即使是第一个测试也会失败。因此,最初您将看到:
$ cowtest
simple: fork() failed
$

“simple”测试分配超过一半的可用物理内存,然后执行一系列的fork()fork失败的原因是没有足够的可用物理内存来为子进程提供父进程内存的完整副本。

完成本实验后,内核应该通过cowtestusertests中的所有测试。即:

$ cowtest
simple: ok
simple: ok
three: zombie!
ok
three: zombie!
ok
three: zombie!
ok
file: ok
ALL COW TESTS PASSED
$ usertests
...
ALL TESTS PASSED
$

这是一个合理的攻克计划:

  1. 修改uvmcopy()将父进程的物理页映射到子进程,而不是分配新页。在子进程和父进程的PTE中清除PTE_W标志。
  2. 修改usertrap()以识别页面错误。当COW页面出现页面错误时,使用kalloc()分配一个新页面,并将旧页面复制到新页面,然后将新页面添加到PTE中并设置PTE_W
  3. 确保每个物理页在最后一个PTE对它的引用撤销时被释放——而不是在此之前。这样做的一个好方法是为每个物理页保留引用该页面的用户页表数的“引用计数”。当kalloc()分配页时,将页的引用计数设置为1。当fork导致子进程共享页面时,增加页的引用计数;每当任何进程从其页表中删除页面时,减少页的引用计数。kfree()只应在引用计数为零时将页面放回空闲列表。可以将这些计数保存在一个固定大小的整型数组中。你必须制定一个如何索引数组以及如何选择数组大小的方案。例如,您可以用页的物理地址除以4096对数组进行索引,并为数组提供等同于kalloc.ckinit()在空闲列表中放置的所有页面的最高物理地址的元素数。
  4. 修改copyout()在遇到COW页面时使用与页面错误相同的方案。

提示:

  • lazy page allocation实验可能已经让您熟悉了许多与copy-on-write相关的xv6内核代码。但是,您不应该将这个实验室建立在您的lazy allocation解决方案的基础上;相反,请按照上面的说明从一个新的xv6开始。
  • 有一种可能很有用的方法来记录每个PTE是否是COW映射。您可以使用RISC-V PTE中的RSW(reserved for software,即为软件保留的)位来实现此目的。
  • usertests检查cowtest不测试的场景,所以别忘两个测试都需要完全通过。
  • kernel/riscv.h的末尾有一些有用的宏和页表标志位的定义。
  • 如果出现COW页面错误并且没有可用内存,则应终止进程。

代码解析

跟着提示一步一步来

(1).kernel/riscv.h中选取PTE中的保留位定义标记一个页面是否为COW Fork页面的标志位

在这里插入图片描述

// 记录应用了COW策略后fork的页面
#define PTE_F (1L << 8)

(2).kalloc.c中进行如下修改

  • 定义引用计数的全局变量ref,其中包含了一个自旋锁和一个引用计数数组,由于ref是全局变量,会被自动初始化为全0。

这里使用自旋锁是考虑到这种情况:

  • 进程P1和P2共用内存M,M引用计数为2,此时CPU1要执行fork产生P1的子进程,CPU2要终止P2,那么假设两个CPU同时读取引用计数为2,执行完成后CPU1中保存的引用计数为3,CPU2保存的计数为1,那么后赋值的语句会覆盖掉先赋值的语句,从而产生错误
struct ref_stru {
  struct spinlock lock;
  int cnt[PHYSTOP / PGSIZE];  // 引用计数
} ref;
  • kinit中初始化ref的自旋锁
void
kinit()
{
  initlock(&kmem.lock, "kmem");
  initlock(&ref.lock, "ref");
  freerange(end, (void*)PHYSTOP);
}
  • 修改kallockfree函数,在kalloc中初始化内存引用计数为1,在kfree函数中对内存引用计数减1,如果引用计数为0时才真正删除
void *
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r) {
    kmem.freelist = r->next;
    acquire(&ref.lock);
    ref.cnt[(uint64)r / PGSIZE] = 1;  // 将引用计数初始化为1
    release(&ref.lock);
  }
  release(&kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}
void
kfree(void *pa)
{
  struct run *r;

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

  // 只有当引用计数为0了才回收空间
  // 否则只是将引用计数减1
  acquire(&ref.lock);
  if(--ref.cnt[(uint64)pa / PGSIZE] == 0) {
    release(&ref.lock);

    r = (struct run*)pa;

    // Fill with junk to catch dangling refs.
    memset(pa, 1, PGSIZE);

    acquire(&kmem.lock);
    r->next = kmem.freelist;
    kmem.freelist = r;
    release(&kmem.lock);
  } else {
    release(&ref.lock);
  }
}
  • 添加如下四个函数,详细说明已在注释中,这些函数中用到了walk,记得在defs.h中添加声明,最后也需要将这些函数的声明添加到defs.h
  • 在cowalloc中,读取内存引用计数,如果为1,说明只有当前进程引用了该物理内存(其他进程此前已经被分配到了其他物理页面),就只需要改变PTE使能PTE_W;否则就分配物理页面,并将原来的内存引用计数减1。该函数需要返回物理地址,这将在copyout中使用到。
/**
 * @brief cowpage 判断一个页面是否为COW页面
 * @param pagetable 指定查询的页表
 * @param va 虚拟地址
 * @return 0 是 -1 不是
 */
int cowpage(pagetable_t pagetable, uint64 va) {
  if(va >= MAXVA)
    return -1;
  pte_t* pte = walk(pagetable, va, 0);
  if(pte == 0)
    return -1;
  if((*pte & PTE_V) == 0)
    return -1;
  return (*pte & PTE_F ? 0 : -1);
}

/**
 * @brief cowalloc copy-on-write分配器
 * @param pagetable 指定页表
 * @param va 指定的虚拟地址,必须页面对齐
 * @return 分配后va对应的物理地址,如果返回0则分配失败
 */
void* cowalloc(pagetable_t pagetable, uint64 va) {
  if(va % PGSIZE != 0)
    return 0;

  uint64 pa = walkaddr(pagetable, va);  // 获取对应的物理地址
  if(pa == 0)
    return 0;

  pte_t* pte = walk(pagetable, va, 0);  // 获取对应的PTE

  if(krefcnt((char*)pa) == 1) {
    // 只剩一个进程对此物理地址存在引用
    // 则直接修改对应的PTE即可
    *pte |= PTE_W;
    *pte &= ~PTE_F;
    return (void*)pa;
  } else {
    // 多个进程对物理内存存在引用
    // 需要分配新的页面,并拷贝旧页面的内容
    char* mem = kalloc();
    if(mem == 0)
      return 0;

    // 复制旧页面内容到新页
    memmove(mem, (char*)pa, PGSIZE);

    // 清除PTE_V,否则在mappagges中会判定为remap
    *pte &= ~PTE_V;

    // 为新页面添加映射
    if(mappages(pagetable, va, PGSIZE, (uint64)mem, (PTE_FLAGS(*pte) | PTE_W) & ~PTE_F) != 0) {
      kfree(mem);
      *pte |= PTE_V;
      return 0;
    }

    // 将原来的物理内存引用计数减1
    kfree((char*)PGROUNDDOWN(pa));
    return mem;
  }
}

/**
 * @brief krefcnt 获取内存的引用计数
 * @param pa 指定的内存地址
 * @return 引用计数
 */
int krefcnt(void* pa) {
  return ref.cnt[(uint64)pa / PGSIZE];
}

/**
 * @brief kaddrefcnt 增加内存的引用计数
 * @param pa 指定的内存地址
 * @return 0:成功 -1:失败
 */
int kaddrefcnt(void* pa) {
  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    return -1;
  acquire(&ref.lock);
  ++ref.cnt[(uint64)pa / PGSIZE];
  release(&ref.lock);
  return 0;
}
  • 修改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) {
    // 在kfree中将会对cnt[]减1,这里要先设为1,否则就会减成负数
    ref.cnt[(uint64)p / PGSIZE] = 1;
    kfree(p);
  }
}

(3). 修改uvmcopy,不为子进程分配内存,而是使父子进程共享内存,但禁用PTE_W,同时标记PTE_F,记得调用kaddrefcnt增加引用计数

//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");
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);

    // 仅对可写页面设置COW标记
    if(flags & PTE_W) {
      // 禁用写并设置COW Fork标记
      flags = (flags | PTE_F) & ~PTE_W;
      *pte = PA2PTE(pa) | flags;
    }
    
    // 子进程页表中的pte的权限也是flags --> COW
    if(mappages(new, i, PGSIZE, pa, flags) != 0) {
      uvmunmap(new, 0, i / PGSIZE, 1);
      return -1;
    }
    // 增加内存的引用计数
    kaddrefcnt((char*)pa);
  }
  return 0;
}

本lab不依赖于之前的lab,此时使用的不是lazy allocation,而是预先分配策略。


(4). 修改usertrap,处理页面错误

//trap.c

uint64 cause = r_scause();
if(cause == 8) {
  ...
} else if((which_dev = devintr()) != 0){
  // ok
} else if(cause == 13 || cause == 15) {
  uint64 fault_va = r_stval();  // 获取出错的虚拟地址
  if(fault_va >= p->sz
    // 如果第二个条件不满足,说明当前发生的是COW写入错误
    || cowpage(p->pagetable, fault_va) != 0
    // 当前发生COW写入错误时,继续进入到第三个判断
    // 该判断负责处理COW写入错误,即分配一个新页面
    || cowalloc(p->pagetable, PGROUNDDOWN(fault_va)) == 0)
    p->killed = 1;
} else {
  ...
}

本lab不依赖于之前的lab,此时使用的不是lazy allocation,而是预先分配策略。

由于采用的是预先分配策略,所以产生page fault的原因只能有两个:

  • 访问了p->sz之上的虚拟地址,即非法空间
  • 权限问题导致的错误 --> 用户态下访问没有设置pte_U的虚拟地址 ;向只读的虚地址范围尝试写入,并且对应的虚拟式非COW页面;
  • 尝试向COW页面写入数据 --> 这是我们唯一能够挽救的错误,其他错误发生时,直接Kill当前进程即可。

(5).copyout中处理相同的情况,如果是COW页面,需要更换pa0指向的物理地址

//vm.c

// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;

  while(len > 0){
    // 将用户态虚拟地址转换为物理地址
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);

    // 处理COW页面的情况
    if(cowpage(pagetable, va0) == 0) {
      // 更换目标物理地址
      pa0 = (uint64)cowalloc(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;
}

copyout负责从内核态空间拷贝数据到用户态虚地址空间某处位置,因为涉及对用户态虚地址空间的写入操作,所以需要处理COW。


可选的挑战练习

  • 修改xv6以同时支持lazy allocation和COW。
  • 测量您的COW实现减少了多少xv6拷贝的字节数以及分配的物理页数。寻找并利用机会进一步减少这些数字。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/677070.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

开发之路,穷且益坚,不坠青云之志

引言 2023毕业季&#xff0c;距离笔者毕业已过2年有余。 互联网从业环境由盛转衰&#xff0c;互联网从业者数量剧增&#xff0c;市场竞争异常激烈&#xff0c;原本的利润空间被不断挤压&#xff0c;以至于很多开发者对互联网已经失去了信心与激情。 互联网的市场份额依旧是占…

vulntarget-j内网靶机write-up

文章目录 第一部分 获取边界服务器0x01 漏洞扫描0x02 漏洞利用第二部分 信息收集+代理0x01 连接工具0x02 进行信息收集0x03 sock代理设置第三部分 内网漫游0x01 通过代理获取服务器0x02 信息收集-获取账号信息0x03 上线CS0x04 远程访问免责声明摘抄第一部分 获取边界服务器 0x0…

JSP 在线药品管理系统用myeclipse定制开发sqlserver数据库网页模式java编程jdbc

一、源码特点 JAVA 在线药品管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 JSP 在线药品管理系统用myeclipse定制开发sqls 二、功能介绍 此次系统主要在JSP…

go context详解

文章目录 摘要1. context接口2. 实现context接口的类型2.1 emptyCtx2.2 valueCtx2.3 cancelCtx2.4 timerCtx 摘要 Context是go语言用于上下文管理的包&#xff0c;主要用于携程间的上下文管理&#xff0c;控制携程按时或者按时间取消执行。多个Context按树形或者链表的结果向前…

深圳阿里云代理商:阿里云服务器的可用区和地域选择有哪些考虑因素?如何优化性能?

阿里云服务器的可用区和地域选择有哪些考虑因素&#xff1f;如何优化性能&#xff1f;   选择阿里云服务器时&#xff0c;可用区和地域选择是一个非常关键的环节。本文将为您详细解析在这个过程中需要考虑的因素以及如何优化性能。   一、阿里云服务器的可用区和地域选择的…

Linux进程编程、fork函数范例详解 ( 5 ) -【Linux通信架构系列 】

系列文章目录 C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the…

Spring高手之路6——Bean生命周期的扩展点:BeanPostProcessor

文章目录 1. 探索Spring的后置处理器&#xff08;BeanPostProcessor&#xff09;1.1 BeanPostProcessor的设计理念1.2 BeanPostProcessor的文档说明 2. BeanPostProcessor的使用2.1 BeanPostProcessor的基础使用示例2.2 利用BeanPostProcessor修改Bean的初始化结果的返回值2.3 …

【Linux工具】yum指令、vim的使用和修改信任白名单

【Linux工具】yum指令、vim的使用和修改信任白名单 目录 【Linux工具】yum指令、vim的使用和修改信任白名单软件包rzsz查看软件包 安装软件卸载软件vim的使用vim的三种模式vim的基本操作vim命令模式命令集vim末行模式命令集vim操作总结 简单vim配置修改信任白名单 作者&#xf…

第四章 进程同步

目录 一、进程同步、进程互斥 1.1 进程同步 1.2 进程互斥 二、信号量机制 2.1 整型信号量 2.2 记录型信号量 三、用信号量实现进程互斥、同步、前驱关系 3.1 信号量机制实现进程互斥 3.2 信号量机制实现进程同步 3.3 信号量机制实现前驱关系 四、生产者-消费者问题…

【MySQL】· 一文了解四大子查询

前言 ✨欢迎来到小K的MySQL专栏&#xff0c;本节将为大家带来MySQL标量/单行子查询、列子/表子查询的讲解✨ 目录 前言一、子查询概念二、标量/单行子查询、列子/表子查询三、总结 一、子查询概念 子查询指一个查询语句嵌套在另一个查询语句内部的查询&#xff0c;这个特性从My…

MySQL - 第7节 - MySQL内置函数

1.日期函数 1.1.常用的日期函数 常用的日期函数如下&#xff1a; 1.2.current_date函数 current_date函数用于获取当前的日期。如下&#xff1a; 1.3.current_time函数 current_time函数用于获取当前的时间。如下&#xff1a; 1.4.current_timestamp函数 current_timestamp函数…

计组:各硬件工作原理

目录 ​编辑程序指令执行流程 程序执行指令前 执行各指令的顺序 程序&#xff08;每一条&#xff09;指令执行具体步骤 以第一步为例&#xff1a; 取指令&#xff08;#1~#4&#xff09; 初&#xff1a;&#xff08;PC)0&#xff0c;指向第一条指令的存储地址 #1&#x…

计算机组成原理学习笔记(学习中)

计算机系统概论 1.1计算机基本组成 冯诺依曼计算机特点&#xff1a; 计算机由五大部件组成&#xff1a;控制器&#xff0c;运算器&#xff0c;存储器&#xff0c;输入设备&#xff0c;输出设备 指令和数据以同等地位存于存储器&#xff0c;可按地址寻访 指令和数据用二进制…

一文读懂CAN总线及通信协议

CAN总线的汽车 CAN概念 CAN是控制器域网 (Controller Area Network, CAN) 的简称&#xff0c;是由研发和生产汽车电子产品著称的德国BOSCH公司开发了的&#xff0c;并最终成为国际标准&#xff08;ISO11898&#xff09;&#xff0c;是ISO国际标准化的串行通信协议。是国际上应…

Anchor-free应用一览:目标检测、实例分割、多目标跟踪

作者&#xff5c;杨阳知乎 来源&#xff5c;https://zhuanlan.zhihu.com/p/163266388 本文整理了与Anchor free相关的一些工作。一方面是分享近期在目标检测领域中一些工作&#xff0c;另一方面&#xff0c;和大家一起梳理一下非常火热的网络模型CenterNet、FCOS&#xff0c;当…

Linux网络-数据链路层,MAC帧解析

目录 数据链路层VS网络层 以太网概念 以太网的帧格式&#xff08;报文格式&#xff09;&#xff08;也可以称之为MAC帧&#xff09; MAC地址的概念 MAC帧格式 局域网通信原理 MTU MTU说明 MTU对IP协议的影响 MTU对UDP协议的影响 MTU对TCP协议的影响 数据链路层VS网…

【Linux】应用层协议:HTTP和HTTPS

每个人都可以很喜欢每个人&#xff0c;但喜欢治不了病&#xff0c;喜欢买不了东西&#xff0c;喜欢不能当饭吃&#xff0c;喜欢很廉价… 文章目录 一、HTTP协议1.URL1.1 URL的组成1.2 urlencode && urldecode 2.HTTP协议格式2.1 http请求和响应的格式2.2 通过代码来进行…

[Eigen中文文档] 求解稀疏线性系统

文档总目录 本文目录 稀疏求解器列表内置直接求解器内置迭代求解器外部求解器的包装器 稀疏求解器概念计算步骤基准测试例程 英文原文(Solving Sparse Linear Systems) 在Eigen中&#xff0c;有多种方法可用于求解稀疏系数矩阵的线性系统。由于此类矩阵的特殊表示&#xff0c…

零基础速成simulink代码生成——结合CANOE的DBC文件CAN报文代码生成 移植到硬件4

零基础速成simulink代码生成——结合CANOE的DBC文件CAN报文代码生成 移植到硬件4 本次我们将讲解如何将代码放到嵌入式硬件上运行,本次例子将dbc文件导入simulink中,生成代码,不需要我们自己实现数据库的内容。 导入DBC文件 新建一个simulink模型 MCU_CAN.slx simulink具…

开源计算机视觉库OpenCV详解

目录 1、概述 2、OpenCV详细介绍 2.1、OpenCV的起源 2.2、OpenCV开发语言 2.3、OpenCV的应用领域 3、OpenCV模块划分 4、OpenCV源码文件结构 4.1、根目录介绍 4.2、常用模块介绍 4.3、CUDA加速模块 5、OpenCV配置以及Visual Studio使用OpenCV 6、OpenCV和OpenGL的区…