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

前言

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

其他篇章

环境搭建
Lab1: Utilities
Lab2: System calls
Lab3: Page tables
Lab4: Traps
Lab5: Copy-on-Write Fork for xv6

参考链接

官网链接
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为什么卡老半天):
在这里插入图片描述

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值