lab6 cow

news2025/1/27 12:20:07

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();
    // 没有可用页面,返回0
    if (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.coloradmin.cn/o/924262.html

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

相关文章

MyBatis 框架的搭建及使用

目录 什么是 MyBatisMyBatis 环境的搭建添加 MyBatis 依赖设置 MyBatis 配置 根据 MyBatis 写法完成数据库操作查询操作(无参)定义接口使用 XML 实现接口类中的属性名与数据库表中字段名不一致怎么办 ? 查询操作(有参)${} 和 #{} 有啥区别 ?使用 ${} 会出现的问题like 查询 删…

CypherRAT、CraxsRATAndroid系列恶意软件“始作俑者”曝光!

近日&#xff0c;恶意软件系列 CypherRAT 和 CraxsRAT 的创建者曝光&#xff0c;是一位名为 EVLF 的叙利亚威胁行为者。 网络安全公司Cyfirma在上周发布的一份报告中提到&#xff1a;这些RAT旨在允许攻击者远程执行实时操作&#xff0c;并控制受害者设备的摄像头、位置和麦克风…

职场的心灵疗愈:如何战胜压力与燃尽?

职场压力的根源 当代职场的变革与挑战 在数字化时代,职场的变革速度前所未有。新的技术和工具不断涌现,要求员工持续学习和适应。例如,云计算、大数据和人工智能等技术正在改变许多行业的工作方式。这些技术为我们带来了便利,但同时也带来了学习和适应的压力。 此外,全球…

keepalived+lvs(DR)(四十六)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、作用 二、调度器配置 三、web节点配置 一、作用 使用keepalived解决lvs的单点故障 高可用集群 二、调度器配置 安装keepalived yum install -y k…

leetcode:电话号码的字母组合(详解)

题目 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 示例 1&#xff1a; 输入&#xff1a;digits "23" 输出…

基于javaweb的新生报到系统

摘 要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&…

用Idea把SpringBoot项目打包镜像上传至docker

1、设置docker把2375端口开起来 命令查看docker装在哪里 vim docker.service 新增 -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock 2、配置Dockerfile 我在跟pom同一层 3、配置docker-maven-plugin <plugin><groupId>com.spotify</groupId><arti…

LAMP架构介绍配置命令讲解

LAMP架构介绍配置命令讲解 一、LAMP架构介绍1.1概述1.2LAMP各组件的主要作用1.3各组件的安装顺序 二、编译安装Apache httpd服务---命令讲解1、关闭防火墙&#xff0c;将安装Apache所需的软件包传到/opt/目录下2、安装环境依赖包3、配置软件模块4、编译安装5、优化配置文件路径…

API接口管理平台:提高开发效率和节省时间的解决方案

随着互联网的迅速发展&#xff0c;很多业务已经可以通过API接口实现&#xff0c;使得开发者可以更快速地开发出高质量的软件。然而&#xff0c;随着API数量的增加&#xff0c;管理这些API也变得尤为困难。因此&#xff0c;API接口管理平台应运而生。本文将探讨如何使用API接口管…

中国人民大学与加拿大女王大学合办金融硕士毕业证书是双证么?

中国人民大学与加拿大女王大学合办金融硕士毕业证书是双证么&#xff1f; 无需参加全国联考获国家承认双证中国教育部留学服务中心可认证 是的 双证的 学位授予 1、完成本项目十门课程&#xff0c;通过考核符合毕业要求的学生将获得由女王大学颁发的金融硕士学位证书&#x…

论文笔记: MOGRIFIER LSTM

2020 ICLR 修改传统LSTM 当前输入和隐藏状态充分交互&#xff0c;从而获得更佳的上下文相关表达 1 Mogrifier LSTM LSTM的输入X和隐藏状态H是完全独立的 机器学习笔记&#xff1a;GRU_gruc_UQI-LIUWJ的博客-CSDN博客这篇论文想探索&#xff0c;如果在输入LSTM之前&#xf…

亿级月活的社交 APP,陌陌如何做到 3 分钟定位故障?

一分钟精华速览 本文概述了挚文集团&#xff08;陌陌和探探母公司&#xff09;在微服务架构下解决故障定位问题中遇到的痛点、解决方案以及取得的效果。通过构建统一可观测平台&#xff0c;实现了故障快速定位&#xff0c;大幅提升了问题定位的效率。文中还讨论了存储优化、数…

SpringBoot案例-配置文件-@ConfigurationProperties

问题分析 在往期的配置参数的文章中&#xff0c;对于阿里云OSS的参数时设置在yml配置文件中&#xff0c;然后使用Value&#xff08;”${}“&#xff09;对参数进行赋值&#xff0c;具体如下&#xff1a; 此种方法比较繁琐 问题解决 使用注解 Data 为变量自动生成get/set方…

CrystalNet .Net VCL for Delphi Crack

CrystalNet .Net VCL for Delphi Crack VCL或更为人所知的可视化组件库是基于一个面向对象的框架&#xff0c;什么是用户对开发人员和事件的Microsoft Windows应用程序的接口。可视化组件库是用对象Pascal编写的。它主要是为使用Borland而开发的&#xff0c;它具有与Delphi以及…

vue关闭弹窗刷新父页面 this.$refs

代码截图 主页面 弹出框页面 接这一篇文章后续 参考链接

专题-【B树的构建与删除】

构建&#xff1a; 删除&#xff1a; 叶子结点直接删&#xff1b; 非叶结点 找前驱&#xff08;左子树最右边&#xff09;/后继&#xff08;右子树最左边&#xff09;元素覆盖即可&#xff08;满足关键字个数取值范围&#xff09;&#xff1b; 这个更详细 『数据结构与算法』…

JAVA-编程基础-09-常用工具类

Lison <dreamlison163.com>, v1.0.0, 2023.04.16 JAVA-编程基础-09-常用工具类 文章目录 JAVA-编程基础-09-常用工具类Arrays工具类创建数组copyOfcopyOfRangefill 数组比较数组排序数组检索数组转流打印数组数组转 ListsetAllparallelPrefix StringUtils工具类字符串判…

PHP 安装Composer,vue前端依赖包

电脑安装Composer 官网下载&#xff1a;https://getcomposer.org/Composer-Setup.exe 后端安装&#xff1a; 检查是否安装依赖&#xff1a; 安装Composer install 或 Composer i 前端安装&#xff1a; yarn install 安装依赖

数组和指针练习(3)

题目&#xff1a; int main() { int a[5][5]; int(*p)[4]; p a; printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]); return 0; } 思路分析&#xff1a; int(*p)[4]; 定义了指针变量p是一个数组指针&#xff0c;且该数组指…

Spring MVC详解

文章目录 一、SpringMVC1.1 引言1.2 MVC架构1.2.1 概念1.2.2 好处 二、开发流程2.1 导入依赖2.2 配置核心(前端)控制器2.3 后端控制器2.4 配置文件2.5 访问 三、接收请求参数3.1 基本类型参数3.2 实体收参【重点】3.3 数组收参3.4 集合收参 【了解】3.5 路径参数3.6 中文乱码 四…