MIT 6.S081 2020Lab5 lazy page allocation 个人全流程

文章目录

    • 零、写在前面
    • [一、Eliminate allocation from sbrk()](#一、Eliminate allocation from sbrk())
      • [1.1 说明](#1.1 说明)
      • [1.2 实现](#1.2 实现)
    • [二、Lazy allocation](#二、Lazy allocation)
      • [2.1 说明](#2.1 说明)
      • [2.2 实现](#2.2 实现)
    • [三、Lazytests and Usertests](#三、Lazytests and Usertests)
      • [3.1 说明](#3.1 说明)
      • [3.2 实现](#3.2 实现)
        • [3.2.1 lazytests](#3.2.1 lazytests)
        • [3.2.2 usertests](#3.2.2 usertests)

零、写在前面

可以阅读下4.6页面错误异常

像应用程序申请内存,内核分配和映射这些内存其实是很耗费时间的。比如,一个 GB 的内存包含 262,144 个 4096 字节的页;即便每次分配的开销很小,这么多次操作累积起来仍然非常耗时。

此外,一些程序会分配比实际使用更多的内存(例如,为了实现稀疏数组),或者会提前分配内存但迟迟不使用。为加快 sbrk() 的执行速度,现代内核采用了一种称为懒惰分配的技术:sbrk() 不再立即分配物理内存,而是仅记录下哪些用户地址被分配了,并在用户页表中将这些地址标记为无效。

当进程首次尝试访问这些**"懒惰分配"**的页面时,CPU 会触发一次缺页异常(page fault)。内核在处理该异常时,会分配一页物理内存、将其清零,并将其映射到进程的地址空间中。

总的来说,懒惰分配是一种常见的降低均摊成本的操作。

在本实验中,你将为 xv6 添加这个懒惰分配的功能。

记得先切换分支到lazy

一、Eliminate allocation from sbrk()

1.1 说明

你的第一个任务是从 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 中用户异常处理函数(user trap handler)的信息;它捕获到了一个它不知道如何处理的异常 。你需要弄清楚为什么会发生这个缺页异常(page fault)。

信息中的 stval=0x0000000000004008 表示导致缺页异常的虚拟地址是 0x4008。

1.2 实现

我们先来看看初始的sys_sbrk:

c 复制代码
uint64 sys_sbrk(void) {
    int addr;
    int n;

    if(argint(0, &n) < 0)
        return -1;
    addr = myproc()->sz;
    if(growproc(n) < 0)
        return -1;
    return addr;
}
  • 从寄存器a0拿出n
  • growproc(n) 来分配n byte 的物理内存
  • 返回addr

修改后:

  • 删除分配物理内存的逻辑
  • 如果n 大于0,我们增加sz
  • 否则,我们dealloc 掉n个字节
c 复制代码
uint64 sys_sbrk(void) {
    int addr;
    int n;

    if(argint(0, &n) < 0)
      return -1;

    struct proc* p = myproc();
    addr = p->sz;

    //  if(growproc(n) < 0)
    //    return -1;

    // alloc
    if (n > 0){
        (p->sz) += n;
    } else {
        // shrink
        uint sz = p->sz;
        p->sz = uvmdealloc(p->pagetable, sz, sz + n);
    }

    return addr;
}

我们启动xv6来测试下:

和预期一致,我们接着做后面的作业。

二、Lazy allocation

2.1 说明

修改 trap.c 中的代码,使其能在用户空间发生缺页异常 时,为发生异常的地址映射一页新分配的物理内存 ,然后返回用户空间,让进程继续执行。你应当在打印 "usertrap(): ..." 的那条 printf 语句之前 加入你的处理代码。此外,你还需要根据需要修改 xv6 内核中的其他代码,使 echo hi 能够正常运行。

官网的一些提示:

  • 你可以在 usertrap() 中通过检查 r_scause() 是否为 13 或 15 来判断是否是页面异常(page fault):
    • 13 表示加载时的页面异常(load page fault)
    • 15 表示存储时的页面异常(store page fault)
  • r_stval() 返回 RISC-V 的 stval 寄存器的值,它表示触发异常的虚拟地址。
  • 你可以参考 vm.c 中的 uvmalloc() 函数的代码,这是 sbrk() 通过 growproc() 最终调用的函数。你会用到以下两个函数:
    • kalloc():用于分配一页物理内存。
    • mappages():用于将虚拟地址映射到物理页。
  • 使用 PGROUNDDOWN(va) 宏将发生异常的虚拟地址向下对齐到页边界。
  • uvmunmap() 默认会触发 panic;你需要修改它的行为,使其在取消映射时不会因为某些页未映射就 panic。
  • 如果内核崩溃了,你可以查找 kernel/kernel.asm 中的 sepc 来定位异常发生的位置。
  • 使用你在页表实验(pgtbl lab)中写的 vmprint 函数来打印页表内容,辅助调试。
  • 如果你遇到 incomplete type proc 的错误,记得先 #include "spinlock.h",再 #include "proc.h"。

如果一切顺利,你的懒惰分配(lazy allocation)代码应该能使 echo hi 正常工作。在执行过程中,系统应该至少会发生一次页面异常(触发懒惰分配),也可能会触发两次。

2.2 实现

  • 按照官网提示,在trap.c 中的usertrap 的 printf else分支前添加处理代码
  • 如果 是 13(load page fault)或 15(store page fault)我们就调用 uvmalloc() 函数分配物理内存,并映射用户页表。
c 复制代码
// ...
} else if((which_dev = devintr()) != 0){
    // ok
} else if(r_scause() == 13 || r_scause() == 15) {
    uint64 va = r_stval();
    if (uvmalloc(p->pagetable, PGROUNDDOWN(va), PGROUNDDOWN(va) + PGSIZE) == 0)
        p->killed = 1;
}
else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
}

值得注意的是,因为我们是利用缺页异常实现延迟分配,所以 uvmunmap 中的无法根据虚拟地址找到实际物理页面的情况需要取消panic,改为continue,否则就会触发panic:

然后我们运行一下:

三、Lazytests and Usertests

3.1 说明

我们为你提供了一个名为 lazytests 的 xv6 用户程序,它会测试一些特定情况,这些情况可能会对你的懒惰内存分配器造成压力。请修改你的内核代码,使 lazytests 和 usertests 中的所有测试都能通过。

你需要处理以下几种情况:

  • 处理 sbrk() 的负数参数:即当进程释放内存时,要正确缩小进程的地址空间。
  • 如果进程在访问一个高于 sbrk() 分配范围的虚拟地址时发生缺页异常,应终止该进程。
  • 正确处理 fork() 中父进程到子进程的内存拷贝,包括懒惰分配页的复制。
  • 当进程将一个来自 sbrk() 的合法地址传递给系统调用(如 read 或 write),但该地址尚未实际分配物理内存时,也应正确处理并触发分配。
  • 正确处理内存耗尽的情况 :如果在页面异常处理函数中 kalloc() 失败,表示系统已无可用内存,应该终止当前进程。
  • 处理位于用户栈下方的非法页面上的访问异常。

你的实现是合格的,如果你的内核能够通过 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

$

3.2 实现

先跑一下看看哪里报错,结合官网提示去调:

3.2.1 lazytests

~~然后它就死了。~~但是给了很多信息。

我们先看uvmcopy,原代码:

  • 这个函数会把给定的父进程页表拷贝内存到子进程页表,页表和物理内存都进行拷贝
  • 官网提示我们正确完成拷贝包括懒惰分配页的复制
c 复制代码
// Given a parent process's page table, copy
// its memory into a child's page table.
// Copies both the page table and the
// physical memory.
// returns 0 on success, -1 on failure.
// frees any allocated pages on failure.
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);
    if((mem = kalloc()) == 0)
      goto err;
    memmove(mem, (char*)pa, PGSIZE);
    if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
      kfree(mem);
      goto err;
    }
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}

然后注意到 panic("uvmcopy: page not present");被触发显然是因为遇到了我们假分配的页面

那就很简单了,注释掉panic,改为continue即可。

再跑一遍:

它又死了,这次报错在 freewalk

我们查看一下源码:

  • 它会递归释放页表页
  • 所以叶子映射都必须已经释放
c 复制代码
// Recursively free page-table pages.
// All leaf mappings must already have been removed.
void freewalk(pagetable_t pagetable)
{
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
      // this PTE points to a lower-level page table.
      uint64 child = PTE2PA(pte);
      freewalk((pagetable_t)child);
      pagetable[i] = 0;
    } else if(pte & PTE_V){
      panic("freewalk: leaf");
    }
  }
  kfree((void*)pagetable);
}

钠根据该函数的描述,我们知道说明有叶子节点没有被释放。

这其实是比较奇怪的,然后看到官网这一条提示:

  • 如果进程在访问一个高于 sbrk() 分配范围的虚拟地址时发生缺页异常,应终止该进程。

于是合理怀疑是因为测试点里面有对于非法地址的访问,触发缺页异常,然后让我们误以为是懒惰分配,从而分配了物理内存。

于是在最早的usertrap中的逻辑中加一个地址界限的判断:

这次就没问题了:

3.2.2 usertests

同样的,我们根据错误找问题:

通过vscode找到这个sbrkarg函数是在 usertest.c 下

  • 用来测试对分配内存的读写
  • 我们报错是在write的地方报错了
c 复制代码
// test reads/writes from/to allocated memory
void sbrkarg(char *s)
{
  char *a;
  int fd, n;

  a = sbrk(PGSIZE);
  fd = open("sbrk", O_CREATE|O_WRONLY);
  unlink("sbrk");
  if(fd < 0)  {
    printf("%s: open sbrk failed\n", s);
    exit(1);
  }
  if ((n = write(fd, a, PGSIZE)) < 0) {
    printf("%s: write sbrk failed\n", s);
    exit(1);
  }
  close(fd);

  // test writes to allocated memory
  a = sbrk(PGSIZE);
  if(pipe((int *) a) != 0){
    printf("%s: pipe() failed\n", s);
    exit(1);
  } 
}

官网有着这样一条提示:

  • 当进程将一个来自 sbrk() 的合法地址传递给系统调用(如 read 或 write),但该地址尚未实际分配物理内存时,也应正确处理并触发分配。

那其实很好理解了,我们没有对 write 访问假分配时进行分配物理内存。

这个就需要我们去查看write 系统调用的实现,以及写逻辑。

先找到 sys_write

c 复制代码
uint64 sys_write(void)
{
  struct file *f;
  int n;
  uint64 p;

  if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argaddr(1, &p) < 0)
    return -1;

  return filewrite(f, p, n);
}

发现调用了 filewrite

然后查看 filewrite 发现它又调用了 writei 来进行写逻辑

writei 又调用了either_copyin ,而either_copyin又调用了copyin

最终在 copyin 中发现了对于pagetable 的访问

总而言之是这么个逻辑:

c 复制代码
sys_write() -> filewrite() -> writei() -> either_copyin() -> copyin() -> walkaddr()

查看 walkaddr:

  • 果然有问题,如果访问到空页或者无效页,它直接返回0了(0代表未映射)
c 复制代码
uint64 walkaddr(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;
  uint64 pa;

  if(va >= MAXVA)
    return 0;

  pte = walk(pagetable, va, 0);
  if(pte == 0)
    return 0;
  if((*pte & PTE_V) == 0)
    return 0;
  if((*pte & PTE_U) == 0)
    return 0;
  pa = PTE2PA(*pte);
  return pa;
}

我们特判尝试物理分配即可:

值得注意的是两个头文件的引用顺序:

测试一下:

相关推荐
流浪00112 小时前
Linux系统篇39——线程(四) pthread库的由来和线程的创建与等待
linux·操作系统·线程·pthread库
流浪00120 小时前
Linux系统篇42——线程(七) pthread库管理线程的工作流,从创建到回收
linux·操作系统·线程·线程创建到销毁
云计算练习生1 天前
什么是 Linux?从 Unix 到开源生态的完整脉络
linux·开源·操作系统·unix
云计算练习生2 天前
什么是上下文切换?进程切换时 CPU 到底在保存什么
linux·服务器·操作系统·进程切换
CS创新实验室4 天前
为什么三台机器就能选出一个“带头大哥“?——Raft 一致性算法,一次讲透
分布式·操作系统
sweet丶4 天前
iOS 为什么没有内存交换分区(swap)
操作系统
我爱cope10 天前
【操作系统 | 计算机硬件:CPU、内存与 I/O 如何支撑操作系统?】
学习·操作系统
NineData10 天前
NineData 与统信服务器操作系统完成兼容认证
数据库·oracle·操作系统·软件·数据库管理工具·ninedata·统信
DataScience_201911 天前
系统|dup2:文件描述符复制的艺术
操作系统
Tairitsu_H11 天前
[Linux系统] 自动化构建make / Makefile | 进度条
linux·操作系统·makefile·make