6.S081的Lab学习——Lab5: xv6 lazy page allocation

news2024/11/15 12:28:59

文章目录

  • 前言
  • 一、Eliminate allocation from sbrk() (easy)
    • 解析:
  • 二、Lazy allocation (moderate)
    • 解析:
  • 三、Lazytests and Usertests (moderate)
    • 解析:
  • 总结


前言

一个本硕双非的小菜鸡,备战24年秋招。打算尝试6.S081,将它的Lab逐一实现,并记录期间心酸历程。
代码下载

官方网站:6.S081官方网站

安装方式:
通过 APT 安装 (Debian/Ubuntu)
确保你的 debian 版本运行的是 “bullseye” 或 “sid”(在 ubuntu 上,这可以通过运行 cat /etc/debian_version 来检查),然后运行:

sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu 

(“buster”上的 QEMU 版本太旧了,所以你必须单独获取。

qemu-system-misc 修复
此时此刻,似乎软件包 qemu-system-misc 收到了一个更新,该更新破坏了它与我们内核的兼容性。如果运行 make qemu 并且脚本在 qemu-system-riscv64 -machine virt -bios none -kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 之后出现挂起

则需要卸载该软件包并安装旧版本:

  $ sudo apt-get remove qemu-system-misc
  $ sudo apt-get install qemu-system-misc=1:4.2-3ubuntu6

在 Arch 上安装

sudo pacman -S riscv64-linux-gnu-binutils riscv64-linux-gnu-gcc riscv64-linux-gnu-gdb qemu-arch-extra

测试您的安装
若要测试安装,应能够检查以下内容:

$ riscv64-unknown-elf-gcc --version
riscv64-unknown-elf-gcc (GCC) 10.1.0
...

$ qemu-system-riscv64 --version
QEMU emulator version 5.1.0

您还应该能够编译并运行 xv6: 要退出 qemu,请键入:Ctrl-a x。

# in the xv6 directory
$ make qemu
# ... lots of output ...
init: starting sh
$

一、Eliminate allocation from sbrk() (easy)

你的首项任务是删除sbrk(n)系统调用中的页面分配代码(位于sysproc.c中的函数sys_sbrk())。sbrk(n)系统调用将进程的内存大小增加n个字节,然后返回新分配区域的开始部分(即旧的大小)。新的sbrk(n)应该只将进程的大小(myproc()->sz)增加n,然后返回旧的大小。它不应该分配内存——因此您应该删除对growproc()的调用(但是您仍然需要增加进程的大小!)。

试着猜猜这个修改的结果是什么:将会破坏什么?

进行此修改,启动xv6,并在shell中键入echo hi。你应该看到这样的输出:

init: starting sh
$ echo hi
usertrap(): unexpected scause 0x000000000000000f pid=3
            sepc=0x0000000000001258 stval=0x0000000000004008
va=0x0000000000004000 pte=0x0000000000000000
panic: uvmunmap: not mapped

“usertrap(): …”这条消息来自trap.c中的用户陷阱处理程序;它捕获了一个不知道如何处理的异常。请确保您了解发生此页面错误的原因。“stval=0x0…04008”表示导致页面错误的虚拟地址是0x4008。

切换分支执行操作

git stash
git fetch
git checkout lazy
make clean

解析:

非常简单,删除sbrk(n)系统调用中的页面分配代码,并且新的sbrk(n)应该只将进程的大小(myproc()->sz)增加n,然后返回旧的大小。按照说得来就行。


uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;
  /*if(growproc(n) < 0)
    return -1;*/
  myproc()->sz += n;
  return addr;
}

输出结果正常:
在这里插入图片描述
对照下面的图,显示是usertrap里面的缺页中断
在这里插入图片描述

二、Lazy allocation (moderate)

修改trap.c中的代码以响应来自用户空间的页面错误,方法是新分配一个物理页面并映射到发生错误的地址,然后返回到用户空间,让进程继续执行。您应该在生成“usertrap(): …”消息的printf调用之前添加代码。你可以修改任何其他xv6内核代码,以使echo hi正常工作。

提示:

  1. 你可以在usertrap()中查看r_scause()的返回值是否为13或15来判断该错误是否为页面错误
  2. stval寄存器中保存了造成页面错误的虚拟地址,你可以通过r_stval()读取
  3. 参考vm.c中的uvmalloc()中的代码,那是一个sbrk()通过growproc()调用的函数。你将需要对kalloc()和mappages()进行调用
  4. 使用PGROUNDDOWN(va)将出错的虚拟地址向下舍入到页面边界
  5. 当前uvmunmap()会导致系统panic崩溃;请修改程序保证正常运行
  6. 如果内核崩溃,请在kernel/kernel.asm中查看sepc
  7. 使用pgtbl lab的vmprint函数打印页表的内容
  8. 如果您看到错误“incomplete type proc”,请include“spinlock.h”然后是“proc.h”。

如果一切正常,你的lazy allocation应该使echo hi正常运行。您应该至少有一个页面错误(因为延迟分配),也许有两个。

解析:

首先按照提示,要修改usertrap()(kernel/trap.c)函数,查看r_scause()的返回值是否为13或15来判断该错误是否为页面错误。

根据提示,先查看uvmalloc()中的代码。

// Allocate PTEs and physical memory to grow process from oldsz to
// newsz, which need not be page aligned.  Returns new size or 0 on error.
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
  char *mem;
  uint64 a;

  if(newsz < oldsz)
    return oldsz;

  //它将oldsz向上舍入到最近的页面边界。这是因为内存通常按页面大小进行分配
  oldsz = PGROUNDUP(oldsz);
  for(a = oldsz; a < newsz; a += PGSIZE){
    //使用kalloc函数分配一个页面大小的物理内存,并返回其地址
    mem = kalloc();
    if(mem == 0){
      uvmdealloc(pagetable, a, oldsz);
      return 0;
    }
    memset(mem, 0, PGSIZE);
    //使用mappages函数将物理内存页面映射到虚拟地址空间中
    if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
      kfree(mem);
      uvmdealloc(pagetable, a, oldsz);
      return 0;
    }
  }
  return newsz;
}

先判断发生错误的虚拟地址(r_stval()读取)是否位于栈空间之上,进程大小(虚拟地址从0开始,进程大小表征了进程的最高虚拟地址)之下,然后分配物理内存并添加映射

......
else if (r_scause() == 13 || r_scause() == 15) {
    uint64 virtual_address = r_stval();
    char *mem;
    if(PGROUNDUP(p->trapframe->sp) > virtual_address && virtual_address >= p->sz)
    	return;
    mem = kalloc();
    virtual_address = PGROUNDDOWN(virtual_address);
    if(mem == 0){
      return;
    }
    memset(mem, 0, PGSIZE);
    if(mappages(p->pagetable, virtual_address, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) {
      kfree(mem);
      p->killed = 1;
    }
  } 
......

修改uvmunmap()(kernel/vm.c),之所以修改这部分代码是因为lazy allocation中首先并未实际分配内存,所以当解除映射关系的时候对于这部分内存要略过,而不是使系统崩溃,这部分在课程视频中已经解答。

......
    if((*pte & PTE_V) == 0)
      //panic("uvmunmap: not mapped");
      continue;
......

最后也是成功输出
在这里插入图片描述

三、Lazytests and Usertests (moderate)

我们为您提供了lazytests,这是一个xv6用户程序,它测试一些可能会给您的惰性内存分配器带来压力的特定情况。修改内核代码,使所有lazytests和usertests都通过。

  1. 处理sbrk()参数为负的情况。
  2. 如果某个进程在高于sbrk()分配的任何虚拟内存地址上出现页错误,则终止该进程。
  3. 在fork()中正确处理父到子内存拷贝。
  4. 处理这种情形:进程从sbrk()向系统调用(如read或write)传递有效地址,但尚未分配该地址的内存。
  5. 正确处理内存不足:如果在页面错误处理程序中执行kalloc()失败,则终止当前进程。
  6. 处理用户栈下面的无效页面上发生的错误。

如果内核通过lazytests和usertests,那么您的解决方案是可以接受的:

$ lazytests
lazytests starting
running test lazy alloc
test lazy alloc: OK
running test lazy unmap...
usertrap(): ...
test lazy unmap: OK
running test out of memory
usertrap(): ...
test out of memory: OK
ALL TESTS PASSED
$ usertests
...
ALL TESTS PASSED
$

解析:

处理sbrk()参数为负数的情况,参考之前sbrk()调用的growproc()程序,如果为负数,就调用uvmdealloc()函数,但需要限制缩减后的内存空间不能小于0

// Grow or shrink user memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
  //定义了一个无符号整数 sz 来存储进程的当前大小
  uint sz;
  //并声明了一个指向 proc 结构体的指针 p用于获取当前进程的指针
  struct proc *p = myproc();
  //从 p 指向的 proc 结构体中获取当前进程的大小,并将其存储在 sz 中
  sz = p->sz;
  //如果 n 大于 0,则尝试增加内存。使用 uvmalloc 函数来增加从 sz 到 sz + n 的内存。如果 uvmalloc 返回 0,表示内存分配失败,函数返回 -1
  if(n > 0){
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
  } else if(n < 0){
    //如果 n 小于 0,则尝试减少内存。使用 uvmdealloc 函数来释放从 sz 到 sz + n 的内存。注意这里即使 uvmdealloc 可能失败(尽管函数签名没有显示返回值或错误处理),该函数也不会检查返回值或返回错误
    sz = uvmdealloc(p->pagetable, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}

修改之后的sys_sbrk:

uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;
  
  uint sz;
  struct proc *p = myproc();

  sz = p->sz;
  if(n > 0){
    p->sz += n;
  } else if(sz + (n > 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
    p->sz = sz;
  } else {
    return -1;
  }
  
  return addr;
}

正确处理fork的内存拷贝:fork调用了uvmcopy进行内存拷贝,现在让其直接进入下一页,而不是报错,所以修改uvmcopy如下

    if((pte = walk(old, i, 0)) == 0)
      //panic("uvmcopy: pte should exist");
      continue;
    if((*pte & PTE_V) == 0)
      //panic("uvmcopy: page not present");
      continue;

还需要继续修改uvmunmap,同样的问题,同样的方式

    if((pte = walk(pagetable, a, 0)) == 0)
      //panic("uvmunmap: walk");
      continue;
    if((*pte & PTE_V) == 0)
      //panic("uvmunmap: not mapped");
      continue;

以上部分搞完之后我就去测试了,然后就g了。。。
看了大佬们的讲解,应该是write错了。
因为我们现在采用的是懒分配,写的时候由于此时传入的地址还未实际分配(因为还没映射,只分配了虚拟地址),就不能走到上文usertrap中判断scause是13或15后进行内存分配的代码。
解决办法我看到了两种:
一种是在walkaddr寻找物理地址时,如果发现虚拟地址没有映射,就给它实际分配一下。
另外一种是系统通过argaddr函数从寄存器中读取地址时添加物理内存分配的代码。
都可以解决,也没啥区别。

我采用在walkaddr函数中更改。代码总体跟之前的uvmalloc没差太多,毕竟是一个功能的。

uint64
sys_sigalarm(void) {
  struct proc *p;
  p = myproc();
  argint(0, &p->alarm_interval);
  argaddr(1, &p->handler);
  p->ticks_count = 0;
  return 0;
}

uint64
sys_sigreturn(void) {
    return 0;
}

最后在trap.c中的usertrap()中处理.
增加用户报警函数的地址可能是0的判断。当进程的报警间隔期满时(也就是两次报警间的滴答计数达到了报警间隔),用户进程执行处理程序函数。

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if (p->alarm_interval != 0) {
      p->ticks_count++;
      if (p->ticks_count == p->alarm_interval) {
        p->ticks_count = 0;
        p->trapframe->epc = p->handler;
      }
    }
  }
  yield();

没有什么问题
在这里插入图片描述

题目二中要解决的主要问题是寄存器保存恢复和防止重复执行的问题。
要在usertrap中再次保存用户寄存器,当handler调用sigreturn时将其恢复,并且要防止在handler执行过程中重复调用。
再在struct proc中新增两个字段

int is_alarming;                    // 是否正在执行告警处理函数
struct trapframe* alarm_trapframe;  // 告警陷阱帧

同时记得在allocproc中将它们初始化为0,并在freeproc中也设为0

  // 增加不是修改
  // 进程初始化,这里主要是防止申请不成功,那就学着已有的代码对进程进行销毁
  if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  p->pid = allocpid();
  p->ticks_count = 0;
  p->alarm_interval = 0;
  p->handler = 0;
  p->is_alarming = 0;

  //增加不是修改
  if(p->alarm_trapframe)
    kfree((void*)p->alarm_trapframe);
  p->trapframe = 0;
  p->is_alarming = 0;

更改usertrap函数,保存进程陷阱帧p->trapframe到p->alarm_trapframe

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if (p->alarm_interval != 0) {
      p->ticks_count++;
      if (p->ticks_count >= p->alarm_interval && p->is_alarming == 0) {
        memmove(p->alarm_trapframe, p->trapframe, sizeof(struct trapframe));
        p->is_alarming == 1;
        p->ticks_count = 0;
        p->trapframe->epc = (uint64)p->handler;
      }
    }
  }
  yield();

  usertrapret();
}

最后修改sys_sigreturn函数,恢复陷阱帧。

uint64
sys_sigreturn(void) {
  struct proc *p;
  p = myproc();
  memmove(p->trapframe, p->alarm_trapframe, sizeof(struct trapframe));
  p->is_alarming = 0;
  return 0;
}

在这里插入图片描述

总结

这个实验成功的带着我们实现了回溯,中断的处理过程并如何恢复初始状态。从系统调用中梳理中断的全流程。收获颇丰。

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

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

相关文章

C++STL容器系列(三)list的详细用法和底层实现

目录 一&#xff1a;介绍二&#xff1a;list的创建和方法创建list方法 三&#xff1a;list的具体用法3.1 push_back、pop_back、push_front、pop_front3.2 insert() 和 erase()3.3 splice 函数 四&#xff1a;list容器底层实现4.1 list 容器节点结构5.2 list容器迭代器的底层实…

磁带存储:“不老的传说”依然在继续

现在是一个数据指数增长的时代&#xff0c;根据IDC数据预测&#xff0c;2025年全世界将产生175ZB的数据。 这里面大部分数据是不需要存储的&#xff0c;在2025预计每年需要存储11ZB的数据。换算个容易理解的说法&#xff0c;1ZB是10^18Bytes, 相当于要写5556万块容量18TB的硬盘…

markdown语法保存

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

PaddleOCR2.7+Qt5

章节一&#xff1a;Windows 下的 PIP 安装 官网安装教程地址 按照里面的教程去安装 如果使用cuda版本的还要安装tensorrt&#xff0c;不然后面运行demo程序的程序会报如下错。 下载TensorRT 8版本&#xff0c;tensorrt下载地址 章节二&#xff1a;编译源码 进入官网源码地址 下…

深入解析Web前端三大主流框架:Angular、React和Vue

Web前端三大主流框架分别是Angular、React和Vue。下面我将为您详细介绍这三大框架的特点和使用指南。 Angular 核心概念: 组件(Components): 组件是Angular应用的构建块,每个组件由一个带有装饰器的类、一个HTML模板、一个CSS样式表组成。组件通过输入(@Input)和输出(…

旧手机翻身成为办公利器——PalmDock的介绍也使用

旧手机有吧&#xff01;&#xff01;&#xff01; 破电脑有吧&#xff01;&#xff01;&#xff01; 那恭喜你&#xff0c;这篇文章可能对你有点用了。 介绍 这是一个旧手机废物利用变成工作利器的软件。可以在 Android 手机上快捷打开 windows 上的文件夹、文件、程序、命…

解决文件传输难题:如何绕过Gitee的100MB上传限制

引言 在版本控制和代码托管领域&#xff0c;Gitee作为一个流行的平台&#xff0c;为用户提供了便捷的服务。然而&#xff0c;其对单个文件大小设定的100MB限制有时会造成一些不便。 使用云存储服务 推荐理由&#xff1a; 便捷性&#xff1a;多数云存储服务如&#xff1a; Dro…

构造+模拟,CF1148C. Crazy Diamond

一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 Problem - 1148C - Codeforces 二、解题报告 1、思路分析 题目提示O(5n)的解法了&#xff0c;事实上我们O(3n)就能解决&#xff0c;关键在于1&#xff0c;n的处理 我们读入数据a[]&#xff0c;代表初始数组…

(函数)求一元二次方程的根(C语言)

一、运行结果&#xff1b; 二、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h> # include <math.h>//声明函数&#xff1b; //判断条件等于0时&#xff1b; void zeor(double a, double b);//判断条件大于0时&#xff1b; void bigzeo…

【Linux】Socket中的心跳机制(心跳包)

Socket中的心跳机制(心跳包) 1. 什么是心跳机制&#xff1f;(心跳包) 在客户端和服务端长时间没有相互发送数据的情况下&#xff0c;我们需要一种机制来判断连接是否依然存在。直接发送任何数据包可以实现这一点&#xff0c;但为了效率和简洁&#xff0c;通常发送一个空包&am…

java高级——Collection集合之List探索(包含ArrayList、LinkedList、Vector底层实现及区别,非常详细哦)

java高级——Collection集合之List探索 前情提要文章介绍提前了解的知识点1. 数组2. 单向链表3. 双向链表4. 为什么单向链表使用的较多5. 线程安全和线程不安全的概念 ArrayList介绍1. 继承结构解析1.1 三个标志性接口1.2 AbstractList和AbstractCollection 2. ArrayList底层代…

⌈ 传知代码 ⌋ YOLOv9最新最全代码复现

&#x1f49b;前情提要&#x1f49b; 本文是传知代码平台中的相关前沿知识与技术的分享~ 接下来我们即将进入一个全新的空间&#xff0c;对技术有一个全新的视角~ 本文所涉及所有资源均在传知代码平台可获取 以下的内容一定会让你对AI 赋能时代有一个颠覆性的认识哦&#x…

洗地机什么品牌质量好?家用洗地机排行榜

一年一度的大促节又到了&#xff0c;各大电商平台和实体店纷纷推出力度不小的折扣活动&#xff0c;吸引着消费者的关注和购买欲望。很多家庭也趁着这个机会&#xff0c;购置一些智能家居产品来提升生活品质。其中&#xff0c;洗地机作为近年来发展迅速的明星产品&#xff0c;受…

【UML用户指南】-02-UML的14种图

1、结构图 1、类图&#xff08;class diagram&#xff09; 展现了一组类、接口、协作和它们之间的关系。 在面向对象系统的建模中所建立的最常见的图就是类图。类图给出系统的静态设计视图。 包含主动类的类图给出系统的静态进程视图。构件图是类图的变体。 2、对象图&a…

什么是 ISP 代理?

代理是路由互联网流量的中间服务器&#xff0c;通常分为三类&#xff1a;数据中心、住宅和 ISP。根据定义&#xff0c;ISP 代理隶属于互联网服务提供商&#xff0c;但实际上&#xff0c;更容易将它们视为数据中心和住宅代理的组合。 让我们仔细研究一下 ISP 代理&#xff0c;看…

计算机网络学习笔记——应用层

一、应用层概述 二、客户/服务器方式(C/S方式)和对等方式(P2P方式) 客户/服务器(Client/Server&#xff0c;C/S)方式 服务器总是处于运行状态&#xff0c;并等待客户的服务请求。服务器具有固定端口号(例如HTTP服务器的默认端口号为80)&#xff0c;而运行服务器的主机也具有固…

Vue.js2+Cesium1.103.0 十六、多模型轨迹运动

Vue.js2Cesium1.103.0 十六、多模型轨迹运动 Demo <template><div id"cesium-container" style"width: 100%; height: 100%;"><ul class"ul"><li v-for"(item, index) of deviceInfo" :key"index" cl…

E. Binary Deque[双指针好思维题]

Binary Deque 题面翻译 有多组数据。 每组数据给出 n n n 个数&#xff0c;每个数为 0 0 0 或 1 1 1 。你可以选择从两边删数&#xff0c;求至少删几个数才可以使剩下的数总和为 s s s 。 如果不能达到 s s s &#xff0c;则输出 − 1 -1 −1 。 题目描述 Slavic h…

NAS使用小妙招丨系统域名配件

NAS&#xff08;网络附加存储&#xff09;使用主要涉及到系统安装与设置、域名绑定、以及配件选择与配置。以下将分别针对这三个方面进行详细阐述&#xff1a; 一、系统安装与设置 安装群晖NAS系统&#xff1a; 将NAS设备连接到网络和电源。 通过计算机浏览器输入设备的IP地址…