lab6 cow

image-20230824203604487

task

cow的目标就是延迟分配,并且直到必须要复制的时候才会分配物理内存

  1. cow的fork只为child创造了一个页表,其中的PTE指向了父进程的物理页面cow的fork将父进程和孩子进程的用户态的PTE都标记为不可写
  2. 当某个进程想要去写一个cow的页面时,cpu会执行一个页错误
    1. 内核的页错误处理函数会检测到
    2. 然后为错误的进程分配一个物理页面
    3. 将原始的页面拷贝的新的页面,
    4. 修改错误进程的PTE指向新的页面,并在PTE中标记为可写
    5. 当页面处理函数返回的时候,用户进程能够正常地写了
  3. 一个物理页面只有在所有的引用都消失时,才会被free掉

如果能够通过cowtestusertest,则通过这个lab

推荐完成路线

  1. 修改uvmcopy

    1. 使其不是分配一个物理页面,而是将父进程的物理页面映射到子进程的页表中

    2. 将父进程和子进程的PTE中的PTE_W都清空

      补充:加入cow标识:#define PTE_C (1L<<8)

  2. 修改usertrap

    1. 使其能够识别出页错误

    2. 当一个页错误发生在cow的page上时

      1. 用kalloc分配一个新的页
      2. 将旧的页拷贝到新的页
      3. 将新的页的地址更新到PTE中,并且设置PTE_W标志
    3. 保证每个物理页面都是在完全没有进程引用的时候再被free,不可以提前

      一个好的实现方法是未每个物理界面都维护一个引用count

      1. 当kalloc时设置引用计数为1
      2. 当一个进程调用fork的时候,给引用计数+1
      3. 任何一个进程free掉某一页的时候都讲引用计数减1
      4. kfree应该只将引用计数为0的页面放到free链表上

      你可以将引用计数记录在一个固定大小的数组中

      1. 你需要想出一个映射的策略,以及决定它的size

        你可以将一个物理地址除以4096来得出索引的下标

        并且通过kinit能够给出的最大的物理地址得到最大的数组大小(size):12810241024/4096

        // the kernel expects there to be RAM
        // for use by the kernel and user pages
        // from physical address 0x80000000 to PHYSTOP.
        #define KERNBASE 0x80000000L
        #define PHYSTOP (KERNBASE + 128*1024*1024)
        
    4. 修改copyout,让它在遇到cow的page时,使用和页错误相同的策略

hints

  1. 用PTE中的RSWbits来标记这个页面是否是cow

    image-20230824164155465

  2. 一个对页表的标志有帮助的宏和定义在kernel/riscv.h

  3. 如果一个cow的页错误发生了,并且没有多余的内存,这个进程应该被杀死

思路

按照实验文档推荐的路线来即可,但是还是有一些小坑的

页引用计数

这一部分实验文档没有给出非常具体的指导没有直接把饭喂到我这种菜鸡嘴里,所以有许多具体实现的方式

我是将这一部分的代码全部放在了kalloc.c文件中。可以想一下,我们什么时候会用到这个页引用计数呢?

  1. 当我们free一个页面的时候,会使用
  2. 当我们kalloc一个页面的时候,我们需要给它初始化为1
  3. 当我们遇到某个cow的页面被写的时候,需要重新分配并更改引用计数
  4. 当我们fork的时候,需要增加这个引用计数

前2点已经足够让我们把相关的定义放到kalloc.c文件里,这里用到了一些宏,主要是为了后面使用其他方便

这里我们的count数组的大小,是由PHYSTOP和KERNBASE计算出来的,一个是可以分配的物理内存的最大值,一个是最小值。因此将它们相减,再除以页面的大小,就可以得到页数,也就是数组的大小。

通过PA2INDEX可以快速得出当前地址位于数组的哪个下标

下面的四个宏分别是求出这个地址对应的页面的引用计数值,以及初始化,减1和加1的操作

在对这个数组操作时,要用lock将其夹住

// KERNBASE 不是 end
#define PA2INDEX(pa) ((((uint64)pa) - KERNBASE) / PGSIZE)struct {struct spinlock lock;int count[PA2INDEX(PHYSTOP)];
} ref_count;#define PA2REFCOUNT(pa) (ref_count.count[PA2INDEX(pa)])
#define PAINITRC(pa) (ref_count.count[PA2INDEX(pa)] = 1)
#define PADEC(pa) (ref_count.count[PA2INDEX(pa)]--)
#define PAINC(pa) (ref_count.count[PA2INDEX(pa)]++)

接下来分别在kinitkfreekalloc时将引用计数的逻辑加入

kinit比较简单,初始化这个锁就行了

void kinit() {initlock(&kmem.lock, "kmem");// 初始化计数数组的锁initlock(&ref_count.lock, "ref_count");freerange(end, (void *)PHYSTOP);
}

kfree只在这个页面引用计数为0时才真的free它。按理说,应该是用==0去判断,可是这样的话xv6都启动不起来。找出问题了,因为最开始freerange的时候,引用计数没有值,你走来就给它减1,就是负数了,结果导致所有的页面都没有放到freelist中。

void kfree(void *pa) {struct run *r;if (((uint64)pa % PGSIZE) != 0 || (char *)pa < end || (uint64)pa >= PHYSTOP)panic("kfree");acquire(&ref_count.lock);PADEC(pa);if (PA2REFCOUNT(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(&ref_count.lock);
}

kalloc只需要一行,将对应的值初始化为1即可

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// 初始化这个物理地址的引用数PAINITRC((char *)r);}return (void *)r;
}

同时,我们还需要两个函数,一个是在cow被写时用来分配一个物理页面并将原来的页面拷贝过去,一个是在fork的时候增加引用计数,分别叫做kcopykinc

// 发生了对cow页面的写操作,必须要分配一个物理页面了
void *kcopy(void *pa) {acquire(&ref_count.lock);// 如果自己就是唯一的拥有者了,那么就不用申请页面,直接用就完事了if (PA2REFCOUNT(pa) == 1) {release(&ref_count.lock);return pa;}void *npa = kalloc();// 没有可用页面,返回0if (npa == 0) {release(&ref_count.lock);return NULL;}// 将当前页面的计数减1,并复制新的页面PADEC(pa);memmove(npa, pa, PGSIZE);release(&ref_count.lock);return npa;
}
// 给某个页面增加一个计数
void kinc(void *pa) {acquire(&ref_count.lock);PAINC(pa);release(&ref_count.lock);
}

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);flags = PTE_FLAGS(*pte);// 如果这个页面可写,才需要设置成cow,因为后面会直接给其write的权利,所以如果只读,那就不用cow了if (*pte & PTE_W) {*pte |= PTE_C;*pte &= ~PTE_W;}// 更新flags,下面mappages要用flags = PTE_FLAGS(*pte);// 将父进程的物理地址映射到子进程的页表上if (mappages(new, i, PGSIZE, (uint64)pa, flags) != 0) {goto err;}// 增加引用计数kinc((void *)pa);}return 0;err:uvmunmap(new, 0, i / PGSIZE, 1);return -1;
}

usertrap

对页错误的trap进行捕获,然后排除以下情况

  1. 地址本来就不合法,超出了最大范围,这可能引起页错误
  2. 如果这一页的W权限位为1,说明不是因为不能写导致的trap,那我们也处理不了
  3. 如果这一页的V标志位为0,那也不是我们能处理的
  4. 同样,如果这一页不是COW页,那我们也处理不了

第2,3,4在xv6里面其实是有点重复的判断,但是小心点反正不会出bug

还有一个细节就是在uvmunmap的时候,dofree必须是0,因为我们在kcopy的时候已经给这个页面的引用减1了,如果dofree=1,待会还得减1,就会出bug

    } else if ((which_dev = devintr()) != 0) {// ok} else if (r_scause() == 12 || r_scause() == 13 || r_scause() == 15) {// 地址越界if (r_stval() >= p->sz) {p->killed = 1;} else {// 分配新的一页pte_t *pte = walk(p->pagetable, r_stval(), 0);// 不存在,或者不是cow页if ((*pte & PTE_W) != 0 || ((*pte) & PTE_V) == 0 || ((*pte) & PTE_C) == 0) {p->killed = 1;} else {void *pa = (void *)PTE2PA(*pte);void *npa = kcopy(pa);// 申请内存失败if (npa == 0) {p->killed = 1;} else {// 已经获得了一块属于自己的物理内存,将地址和标志位更新到页表中int flag = PTE_FLAGS(*pte);flag |= PTE_W;flag &= ~PTE_C;uvmunmap(p->pagetable, PGROUNDDOWN(r_stval()), 1, 0);mappages(p->pagetable, PGROUNDDOWN(r_stval()), PGSIZE, (uint64)npa, flag);}}}}

copyout

整体逻辑和trap捕获差不多

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);if (pa0 == 0)return -1;// 到了这里,肯定是一个合法的值了,但是不一定可以写啊pte_t *pte = walk(pagetable, va0, 0);// 如果是cow并且不可以写if (*pte & PTE_C && !(*pte & PTE_W)) {// 请求获得一块物理内存void *npa = kcopy((void *)pa0);if (npa == 0) {return -1;}// 这个物理内存可用int flag = PTE_FLAGS(*pte);flag &= ~PTE_C;flag |= PTE_W;uvmunmap(pagetable, va0, 1, 0);mappages(pagetable, va0, PGSIZE, (uint64)npa, flag);pa0 = (uint64)npa;}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;
}

收获

  1. 如果需要维护一个全局性的变量,你不用让所有文件都能直接访问到它。可以在全局提供一个可以操作和查询它的函数即可。
  2. 能放在一个函数内部解决的事情就放在一个函数内部解决。特别是对已有的函数增加某些新的判断或者功能时,以全局改动最少的做法为标准。
  3. 对于临界变量,只要在可能访问或者修改它的前后加锁和解锁即可。如果提前return,记得解锁。
  4. 如果要修改一个函数或者某个属性,那要充分考虑到它可能被使用的场景,比如这里的kfree,除了正常的调用,还有freerange。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uniapp微信小程序点击右上角菜单分享功能权限配置

个人项目地址&#xff1a; SubTopH前端开发个人站 &#xff08;自己开发的前端功能和UI组件&#xff0c;一些有趣的小功能&#xff0c;感兴趣的伙伴可以访问&#xff0c;欢迎提出更好的想法&#xff0c;私信沟通&#xff0c;网站属于静态页面&#xff09; SubTopH前端开发个人站…

农业水价综合改革系统主要组成

一、系统概述 农业水价改革灌区信息化系统主要由感知采集层、网络传输层、系统应用层等部分组成。通过无线技术、感知层技术与新型应用的有效结合&#xff0c;可以用于各种业务的传送&#xff0c;充分满足灌区监测站间的物与物互联&#xff0c;农业生产的自动化和信息化相结合。…

设计模式之门面模式(Facade)的C++实现

1、门面模式提出 在组件的开发过程中&#xff0c;某些接口之间的依赖是比较紧密的&#xff0c;如果某个接口发生变化&#xff0c;其他的接口也会跟着发生变化&#xff0c;这样的代码违背了代码的设计原则。门面设计模式是在外部客户程序和系统程序之间添加了一层中间接口&…

使用Termux在安卓手机上搭建Hexo博客网站,并发布到公网访问

文章目录 1. 安装 Hexo2. 安装cpolar内网穿透3. 公网远程访问4. 固定公网地址 Hexo 是一个用 Nodejs 编写的快速、简洁且高效的博客框架。Hexo 使用 Markdown 解析文章&#xff0c;在几秒内&#xff0c;即可利用靓丽的主题生成静态网页。 下面介绍在Termux中安装个人hexo博客并…

nginx代理webSocket链接响应403

一、场景 使用nginx代理webSocket链接&#xff0c;nginx响应403 1、nginx访问日志响应403 [18/Aug/2023:09:56:36 0800] "GET /FS_WEB_ASS/webim_api/socket/message HTTP/1.1" 403 5 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit…

在Linux系统上安装和配置Redis数据库,无需公网IP即可实现远程连接的详细解析

文章目录 1. Linux(centos8)安装redis数据库2. 配置redis数据库3. 内网穿透3.1 安装cpolar内网穿透3.2 创建隧道映射本地端口 4. 配置固定TCP端口地址4.1 保留一个固定tcp地址4.2 配置固定TCP地址4.3 使用固定的tcp地址连接 Redis作为一款高速缓存的key value键值对的数据库,在…

从零开始的Hadoop学习(一) | 大数据概念、特点、应用场景、发展前景

1. 大数据概念 大数据(Big Data)&#xff1a;指 无法在一定时间范围 内用常规软件工具进行捕捉、管理和处理的数据集合&#xff0c;是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的 海量、高增长率和多样化 的 信息资产。 大数据主要解决&#xff0c;海量…

vue项目配置git提交规范

vue项目配置git提交规范 一、背景介绍二、husky、lint-staged、commitlint/cli1.husky2.lint-staged3.commitlint/cli 三、具体使用1.安装依赖2.运行初始化脚本3.在package.json中配置lint-staged4.根目录新增 commitlint.config.js 4.提交测试1.提示信息格式错误时2.eslint校验…

解决Oracle中XML插入数据时的空格问题

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

git协议实现管理(三个步骤)

GitHub官网访问&#xff1a; https://github.com/dashboard ssh-add /Users/haijunyan/.ssh/id_rsa ssh-add -K /Users/haijunyan/.ssh/id_rsa 初次使用git的用户要使用git协议大概需要三个步骤: 一、生成密钥对 二、设置远程仓库(本文以github为例)上的公钥 三、把git的re…

干货 | IC模拟版图设计学习笔记,一文教你快速入门

模拟版图设计处于IC设计流程的后端&#xff0c;属于模拟IC设计岗位的一种。而每个芯片最终能够付诸于生产都离不开集成电路版图设计师的功劳&#xff0c;所以IC模拟版图工程师在芯片产业的发展过程中至关重要。 模拟版图工程师介绍 模拟版图设计工程师为专业版图设计人员&…

网络互联与互联网 - TCP 协议详解

文章目录 1 概述2 TCP 传输控制协议2.1 报文格式2.2 三次握手&#xff0c;建立连接2.3 四次挥手&#xff0c;释放连接 3 扩展3.1 实验演示3.2 网工软考 1 概述 在 TCP/IP 协议簇 中有两个传输协议 TCP&#xff1a;Transmission Control Protocol&#xff0c;传输控制协议&…