6.s081/6.1810(Fall 2022)Lab3: page tables

文章目录

  • 前言
  • 其他篇章
  • 参考链接
  • 0. 前置环境
  • 1. Speed up system calls (easy)
    • 1.1 简单分析
    • 1.2 映射
    • 1.3 页分配
    • 1.4 页释放
    • 1.5 测试
  • 2. Print a page table (easy)
    • 2.1 简单分析
    • 2.2 实现
    • 2.3 测试
  • 3. Detect which pages have been accessed (hard)
    • 3.1 简单分析
    • 3.2 实现
      • 3.2.1 获取参数
      • 3.2.2 传出参数
      • 3.2.3 定义PTE_A
      • 3.2.4 实现主体逻辑
    • 3.3 测试
  • 测试

前言

这一个Lab是往年叫苦声最大的、最难的一个lab,不过今年显然简化了不少,换掉了Task,其间意义见仁见智吧。

其他篇章

环境搭建
Lab1: Utilities
Lab2: System calls
Lab3: Page tables

参考链接

官网链接
xv6手册链接,这个挺重要的,建议做lab之前最好读一读。
xv6手册中文版,这是几位先辈们的辛勤奉献来的呀!再习惯英文文档阅读我还是更喜欢中文一点,开源无敌!
OSTEP,对OS不熟悉的同学做之前可以看一下这本经典书籍,写得很好,也有中文版实体书。
官方文档

0. 前置环境

如果你和我操作步骤一直一样,那就可以在VS的远程仓库里找到分支base/pgtbl分支,选中
上一个lab里我用的命令行拉,这次就

打开分支管理器(Alt->G->M),右键pgtbl,取消设置上游分支,然后右键推送,显示成功推送到origin,这样就成功了
在这里插入图片描述
然后在wsl里的对应文件夹下,git pullgit checkout pgtbl,整体配置完成:
在这里插入图片描述

1. Speed up system calls (easy)

1.1 简单分析

上一个Lab我们实现了两个系统调用,从中可以认识到系统调用涉及到用户态与内核态的切换,自然也就涉及到了各种参数传来传去的问题。本Lab开篇就介绍了许多操作系统都通过维护一个read-only的共享内存区去实现内核态与用户态资源的共享,免去了某些资源交换的过程,从而提升系统调用的效率。
在这里插入图片描述
介绍完后,本Task要求我们在xv6中为getpid实现这种功能,我们知道操作系统通过页表去管理内存,而它告诉我们每个进程创建时都会映射到一个USYSCALL,这玩意是个VA,也就是Virtual Address,这应该就是我们的共享区域的起始地址,打开他提到的文件看一看:
在这里插入图片描述

可以看到,这个USYSCALL是由TRAPFRAME往前偏移一页算出来的,而TRAPFRAME又是由TRAMPOLINE偏移出来的,TRAMPOLINE页相当于在VA的最后一页上,里面映射了一些内核的指令,用于陷入内核,而TRAPFRAME页则负责保存进程相关的一些数据。此外,可以注意到这个地方有一个条件编译,这个是在Makefile里编译启用的,我们不用手动宏定义,或者看着不爽先宏定义一下后面撤掉也行。结构体里面目前就一个pid,后面看看用不用得上。

1.2 映射

然后看一看Hint:
在这里插入图片描述
这在提示我们怎么去做USYSCALL这个映射,我们首先看一下proc.c
在这里插入图片描述
扫一眼就可以看出,这里做的是进程向trampoline pagetrapframe page的映射,在申请资源后,每次map都需要检查一下是否成功,不成功就得释放之前申请过的资源以及映射过的页。因此我们可以往里面添加这样一些代码:

#ifdef LAB_PGTBL // 模仿着memlayout.h加上条件编译
  // 映射
  if (mappages(pagetable, USYSCALL, PGSIZE,
    // TODO: 还差后面两个参数
  ) < 0) {
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmunmap(pagetable, TRAPFRAME, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }
#endif

看一下倒数第二个参数,我们可以发现这个trapframe就是存在proc里的一个指针而已,因此我们也在proc.h加上usyscall指针的定义:
在这里插入图片描述

#ifdef LAB_PGTBL 
  struct usyscall* usyscall;   
#endif

然后看一下mappages函数的最后一个参数,最后一个参数代表了所谓的PTE的值,标记了分页的一些状态,打开定义位置,我们可以看到这里定义了五个宏:
在这里插入图片描述
关于这些标志位的解释xv6 book里有,我之前放那个中文的链接是基于x86的,和现在的RISC-V在这里有一点不一样,所以我这里就放原文了:
在这里插入图片描述
可以看到,这五个标志位分别标记了是否有效、可读、可写、可执行(将页标记为指令,像之前说的trampoline page,里面就放的一些内核的指令,因此我们看到它被标记上了PTE_X)、用户可用,我们的这个共享页需要可读且用户态与内核态都可以访问,因此我们需要将它设置为PTE_R | PTE_U

据此我们依葫芦画瓢照着映射我们的usyscall page就行:

...
#ifdef LAB_PGTBL // 模仿着memlayout.h加上条件编译
  // 映射到USYSCALL
  if (mappages(pagetable, USYSCALL, PGSIZE,
               (uint64)(p->usyscall), PTE_R | PTE_U) < 0) {
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmunmap(pagetable, TRAPFRAME, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }
#endif
...

在这里插入图片描述

1.3 页分配

没啥好说的,找到allocproc函数照猫画虎就行,只是别忘了给pid赋值
在这里插入图片描述

#ifdef LAB_PGTBL 
  if ((p->usyscall = (struct usyscall*)kalloc()) == 0) {
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  p->usyscall->pid = p->pid; // 别忘了给usyscall的pid赋值
#endif

1.4 页释放

freeproc里释放usyscall
在这里插入图片描述

#ifdef LAB_PGTBL
if (p->usyscall)
    kfree((void*)p->usyscall);
  p->usyscall = 0;
#endif

记得我们前面初始化的时候映射失败需要调用unmap去取消映射吗?正常运行完毕自然也要去做这个事情,做这个事情的函数就在下面那个proc_freepagetable里,F12打开,加上去:
在这里插入图片描述

#ifdef LAB_PGTBL
  uvmunmap(pagetable, USYSCALL, 1, 0);
#endif

就此就搞定了

1.5 测试

推送后make qemu,本来是很稀松平常的事情,结果我一直报这个错:

make: *** 没有规则可制作目标“kernel/sysinfo.h”,由“kernel/sysproc.o” 需求。 停止。

在这里插入图片描述

然后我又是回退还原又是各种各样的操作,都依然报这个错,网上也一直找不到别人吐槽这个事情,最后我make clean了一下,成功了,,,这玩意卡我一个多小时你敢信?
在这里插入图片描述
硬生生一个多小时
然后输入pgtbltest,看到ugetpid_test那里显示OK就行:
在这里插入图片描述
跑一下 ./grade-lab-pgtbl ugetpid
在这里插入图片描述

2. Print a page table (easy)

2.1 简单分析

这个task要我们写一个打印页表的函数,也比较简单:
在这里插入图片描述
初步阅读上文可以简单提炼出需求:我们需要在kernel/vm.c中定义一个名为vmprintf()的函数,接受并按格式打印一个pagetable_t 类型的参数,然后在exec.creturn argc插入if(p->pid==1) vmprint(p->pagetable)语句用来打印第一个进程的page table,读到这里,我们顺手给他塞进去:
在这里插入图片描述
然后看看打印格式:
在这里插入图片描述

The first line displays the argument to vmprint. After that there is a line for each PTE, including PTEs that refer to page-table pages deeper in the tree. Each PTE line is indented by a number of " …" that indicates its depth in the tree. Each PTE line shows the PTE index in its page-table page, the pte bits, and the physical address extracted from the PTE. Don’t print PTEs that are not valid. In the above example, the top-level page-table page has mappings for entries 0 and 255. The next level down for entry 0 has only index 0 mapped, and the bottom-level for that index 0 has entries 0, 1, and 2 mapped.

可以看到,第一行打印了vmprint的参数,后面各行展示了页表所属下方的条目,那么问题来了——我们怎么知道页表下面有哪些页面呢?参照The function freewalk may be inspirational. 因此我们可以看一下这个函数:
在这里插入图片描述
打开pagetable_t的定义发现这其实就是个指针型别,看注释这里是用了9位用来表示子页表,因此它遍历了512位,寻址后判定对期望的标志位的页面使用PTE2PA截断了低10位和高2位,然后继续递归进入执行逻辑,可以看出这是个DFS。值得注意的一点是,标志位限定了不可读、不可写、不可执行的页面才进入下一步递归,因为这意味着这是个间接层,不记载内容,只作为多级页表的一级。
在这里插入图片描述

2.2 实现

分析清楚后我们就可以写我们的函数了,由于我们要根据深度打印.,因此我们可以给参数传入一个深度的参数,我们可以为这个递归函数设立一个helper函数,对外接口就只暴露调用helper的vmprint本身,避免污染。

void
vmprint_dfs(pagetable_t pagetable, uint depth)
{
  static char* prefix[] = {
    [1] = "..",
          ".. ..",
          ".. .. .."
  };

  if (depth > 3) {
    panic("vmprint_dfs: depth > 3");
    return;
  }

  for (int i = 0; i < 512; i++) {
    pte_t pte = pagetable[i];
    if (pte & PTE_V) {
      pte_t child = PTE2PA(pte);
      printf("%s%d: pte %p pa %p\n", prefix[depth], i, pte, child);
      if (child & (PTE_R | PTE_W | PTE_X) == 0) {
        vmprint_dfs((pagetable_t)child, depth + 1);
      }
    }
  }
}

void
vmprint(pagetable_t pagetable)
{
  printf("page table %p\n", pagetable);
  vmprint_dfs(pagetable, 1);
}

然后在defs.h中暴露出接口:
在这里插入图片描述
到此就基本搞定了,看一看:
在这里插入图片描述

2.3 测试

运行一下测试脚本./grade-lab-pgtbl pte printout,通过:
在这里插入图片描述

3. Detect which pages have been accessed (hard)

3.1 简单分析

首先lab介绍了一下标记page是否被访问过(accessed)是比较有用的一个信息,比如对GC有用,这个位维护在一些位里,由RISC-V的硬件页遍历器(hardware page walker)去维护这些位。我们要做的就是检查这些页,并返回给用户态。
在这里插入图片描述
具体而言,我们需要实现一个名为pgaccess的系统调用,用于报告哪些页被访问过,它接受三个参数:

  1. 待检查的第一个用户页的起始VA
  2. 待检查页面的数量
  3. 存储结果(被访问了的页面号)用的bitmap
    在这里插入图片描述

第一个Hint还告诉我们可以从user/pgtlbtest.c中的pgaccess_test()看一看pgaccess是怎么用的:
在这里插入图片描述
可以看到,pgaccess应当在失败时返回一个-1,第1、2、30页被访问过了,因此最后结果abits的对应位就被置为了1。

3.2 实现

理清楚这些东西,实现起来就很简单了,上个lab告诉了我们syscall实现的步骤,不过这次我们只用写实现就行了,不用关注那些繁文缛节的事情。

3.2.1 获取参数

依赖之前的经验获取参数,不多说

  uint64 va;             // 待检测页表起始地址
  int num_pages;         // 待检测页表的页数
  uint64 access_mask;    // 记录检测结果的掩码

  // 从用户栈中获取参数
  argaddr(0, &va);  
  argint(1, &num_pages);
  argaddr(2, &access_mask);

3.2.2 传出参数

For the output bitmask, it’s easier to store a temporary buffer in the kernel and copy it to the user (via copyout()) after filling it with the right bits. 提示我们可以用一个中间变量把mask存起来由此可以完善我们的实现:

int
sys_pgaccess(void)
{
  uint64 va;             // 待检测页表起始地址
  int num_pages;         // 待检测页表的页数
  uint64 access_mask;    // 记录检测结果掩码的地址

  // 从用户栈中获取参数
  argaddr(0, &va);  
  argint(1, &num_pages);
  argaddr(2, &access_mask);

  if (num_pages <= 0 || num_pages > 512)
  {
    return -1;
  }

  uint mask = 0;

  // TODO

  copyout(myproc()->pagetable, access_mask, (char*)&mask, sizeof(mask));
  return 0;
}

3.2.3 定义PTE_A

刚才说了,我们实际上是用一个accessed位去记录信息的,这个位同样也保存在PTE中,题中要求我们去在riscv.h中定义一下这个位,那么问题来了,这个位定义成多少呢?
在这里插入图片描述
查阅risc-v手册可以看到,risc-v中将PTE_A放在了第六位,因此我们在riscv.h中加入:

#define PTE_A (1L << 6) // accessed

或者干脆全定义了算了()
在这里插入图片描述

3.2.4 实现主体逻辑

然后就比较简单了,我们遍历页表,利用walk获取pte,然后对PTE_A置位的页复位,并把页码放在mask里:

int
sys_pgaccess(void)
{
  struct proc* p = myproc();

  uint64 va;             // 待检测页表起始地址
  int num_pages;         // 待检测页表的页数
  uint64 access_mask;    // 记录检测结果掩码的地址

  // 从用户栈中获取参数
  argaddr(0, &va);  
  argint(1, &num_pages);
  argaddr(2, &access_mask);

  if (num_pages <= 0 || num_pages > 512)
  {
    return -1;
  }

  uint mask = 0;

  // 遍历页表
  for (int i = 0; i < num_pages; i++)
  {
    pte_t* pte = walk(p->pagetable, va + i * PGSIZE, 0);
    if (pte && (*pte & PTE_V) && (*pte & PTE_A))
    {
      *pte &= ~PTE_A;  // 清除访问位
      mask |= (1 << i);
    }
  }

  // 将检测结果写入用户栈
  copyout(p->pagetable, access_mask, (char*)&mask, sizeof(mask));
  return 0;
}

3.3 测试

make qemupgtbltest,测试成功:
在这里插入图片描述
./grade-lab-pgtbl pgaccess一下:
在这里插入图片描述

测试

最后添加time.txtanswers-pgtbl.txt,跑一下make grade,通过(话说不知道那个Test time为什么卡老半天):
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Ajax】笔记-设置CORS响应头实现跨域

CORS CORS CORS是什么&#xff1f; CORS(Cross-Origin Resource Sharing),跨域资源共享。CORS是官方的跨域解决方案&#xff0c;它的特点是不需要在客户端做任何特殊的操作&#xff0c;完全在服务器中进行处理&#xff0c;支持get和post请求。跨域资源共享标准新增了一组HTTP首…

【新版系统架构补充】-嵌入式技术

嵌入式微处理体系结构 冯诺依曼结构 传统计算机采用冯诺依曼结构&#xff0c;也称普林斯顿结构&#xff0c;是一种将程序指令存储器和数据存储器合并在一起的存储器结构 冯诺依曼的计算机程序和数据共用一个存储空间&#xff0c;程序指令存储地址和数据存储地址指向同一个存…

Nginx启动报错- Failed to start The nginx HTTP and reverse proxy server

根据日志&#xff0c;仍然出现 “bind() to 0.0.0.0:8888 failed (13: Permission denied)” 错误。这意味着 Nginx 仍然无法绑定到 8888 端口&#xff0c;即使使用 root 权限。 请执行以下操作来进一步排查问题&#xff1a; 确保没有其他进程占用 8888 端口&#xff1a;使用以…

【雕爷学编程】MicroPython动手做(27)——物联网之掌控板小程序2

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

使用 Docker Compose 部署 Redis Cluster 集群,轻松搭建高可用分布式缓存

Redis Cluster&#xff08;Redis 集群&#xff09;是 Redis 分布式解决方案的一部分&#xff0c;它旨在提供高可用性、高性能和横向扩展的功能。Redis Cluster 能够将多个 Redis 节点组合成一个分布式集群&#xff0c;实现数据分片和负载均衡&#xff0c;从而确保在大规模应用场…

Java源码规则引擎:jvs-rules 8月新增功能介绍

JVS-rules是JAVA语言下开发的规则引擎&#xff0c;是jvs企业级数字化解决方案中的重要配置化工具&#xff0c;核心解决业务判断的配置化&#xff0c;常见的使用场景&#xff1a;金融信贷风控判断、商品优惠折扣计算、对员工考核评分等各种变化的规则判断情景。 8月是收获的季节…

antDv table组件滚动截图方法的实现

在开发中经常遇到table内容过多产生滚动的场景&#xff0c;正常情况下不产生滚动进行截图就很好实现&#xff0c;一旦产生滚动就会变得有点棘手。 下面分两种场景阐述解决的方法过程 场景一&#xff1a;右侧不固定列的情况 场景二&#xff1a;右侧固定列的情况 场景一 打开…

三 动手学深度学习v2 —— Softmax回归+损失函数+图片分类数据集

三 动手学深度学习v2 —— Softmax回归损失函数图片分类数据集 目录: softmax回归损失函数 1. softmax回归 回归vs分类: 回归估计一个连续值分类预测一个离散类别 从回归到多类分类 回归 单连续数值输出自然区间R跟真实值的误差作为损失 分类 通常多个输出输出i是预测为第…

硬核!10分钟教你搭建一个本地版GPT4.0!

今天10分钟手把手教会你在自己电脑上搭建一个官方原版的GPT4.0。 不用ChatGPT账号&#xff0c;不用API&#xff0c;直接免费使用上官方原版的GPT4.0&#xff01; 对&#xff01;你没看错&#xff01;不仅是正版GPT4.0&#xff0c;还完全免费&#xff01; 而且整个部署流程极其…

供水管网漏损监测,24小时保障城市供水安全

供水管网作为城市生命线重要组成部分&#xff0c;其安全运行是城市建设和人民生活的基本保障。随着我国社会经济的快速发展和城市化进程的加快&#xff0c;城市供水管网的建设规模日益增长。然而&#xff0c;由于管网老化、外力破坏和不当维护等因素导致的供水管网漏损&#xf…

数据结构--单链表

前言 上一章&#xff0c;我们讲了数据结构--动态顺序表&#xff0c;我们会发现有以下问题&#xff1a; 1.当我们要头部或者插入或删除时&#xff0c;都需要进行位置挪动&#xff0c;腾出某一个位置&#xff0c;时间复杂度为0(N)&#xff1b; 2.增容需要申请新空间&#xff0c;…

个人可搭建在线商城系统,支持docker一键部署

Hmart 给大家推荐一个简约自适应电子商城系统&#xff0c;针对虚拟商品在线发货&#xff0c;支持企业微信通知&#xff0c;支持docker一键部署&#xff0c;个人资质也可搭建。 前端 后端 H2 console 运行命令 docker run -d --name mall --restartalways -p 8080:8080 -e co…

全志F1C200S嵌入式驱动开发(从DDR中截取内存)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 linux内核起来的时候,不一定所有的内存都是分配给linux使用的。有的时候,我们是希望能够截留一部分内存的。为什么保留这部分内存呢?这里面可以有很多的用途。比如说,第一,如果…

Spring MVC应用的开发步骤

Spring MVC应用的开发步骤 Spring MVC应用的开发步骤如果以异步方式提交请求利用XML配置文件配置控制器类 Spring MVC应用的开发步骤 下面简单介绍Spring MVC应用的开发步骤。 ① 在web.xml文件中配置核心控制器DispatcherServlet处理所有的HTTP请求。 由于Web应用是基于请求/…

3个命令定位CPU飙高

top 指令找出消耗CPU最厉害的那个进程的pid top -H -p 进程pid 找出耗用CPU资源最多的线程pid printf ‘0x%x\n’ 线程pid 将线程pid转换为16进制 结合jstack 找出哪个代码有问题 jstack 进程pid | grep 16进制的线程pid -A 多少行日志 jstack 进程pid | grep 16进制的线程…

第一章-JavaScript基础进阶part2:事件

文章目录 概念一、注册事件&#xff08;绑定事件&#xff09;1.1 addEventListener事件监听 二、删除事件&#xff08;解绑&#xff09;三、DOM事件流四、事件对象event4.1 e.target与this与e.currentTarget的区别4.2 事件对象的常见属性 五、阻止事件默认行为及冒泡六、事件委…

Apache Kafka Learning

目录 一、Kafka 1、Message Queue是什么&#xff1f; 2、Kafka 基础架构 3、Kafka安装 4、Offset自动控制 5、Acks & Retries 6、幂等性 7、事务控制 8、数据同步机制 9、Kafka-Eagle 二、Maven项目测试 1、Topic API 2、生产者&消费者 一、Kafka Kafka是…

express学习笔记5 - 自定义路由异常处理中间件

修改router/index.js&#xff0c;添加异常处理中间件 *** 自定义路由异常处理中间件* 注意两点&#xff1a;* 第一&#xff0c;方法的参数不能减少* 第二&#xff0c;方法的必须放在路由最后*/ router.use((err, req, res, next) > {console.log(err);const msg (err &…

GD32F103VE睡眠与唤醒

GD32F103VE睡眠与唤醒&#xff0c;兆易官网给的程序没有测试。等测试后&#xff0c;才发现有问题。 现修改&#xff0c;测试如下&#xff1a; #include "SleepMode.h" #include "delay.h"u8 WFE_CMD_EnterSleepModeFlag;void Enter_DeepSleepMode(void);…

vue2-diff算法

1、diff算法是什么&#xff1f; diff算法是一种通过同层的树节点进行比较的高效算法。 其有两个特点&#xff1a; 比较只会在同层级进行&#xff0c;不会跨层级进行。 在diff比较的过程中&#xff0c;循环从两边向中间比较。 diff算法在很多场景中都有应用&#xff0c;在vue中&…