「实验记录」MIT 6.S081 Lab5 lazy page allocation

I. Source

  1. MIT-6.S081 2020 课程官网
  2. Lab5: lazy page allocation 实验主页
  3. MIT-6.S081 2020 xv6 book
  4. B站 - MIT-6.S081 Lec8 Page Faults

II. My Code

  1. Lab5: lazy page allocation 的 Gitee
  2. xv6-labs-2020 的 Gitee 总目录

III. Motivation

Lab5: lazy page allocation 主要是想让我们了解 xv6 在内存管理方面还可以有另一种玩法:内存空间延迟分配机制(与空间立即分配机制相反)

在开始实验之前,一定要阅读 xv6-6.S081 的第四章节 Traps and device drivers 的第六小节及 kernel/trap.ckernel/vm.ckernel/sysproc.c

IV. Eliminate allocation from sbrk() (easy)

i. Motivation

Lab: Eliminate allocation from sbrk()Lab5: lazy page allocation 的第一步,主要是想让我们简化 sbrk() 的具体业务流程,将原先的立即分配改成只做标注,这么一件简单的事情

ii. Solution

首先需要了解 xv6 的内存结构,如下图,

sbrk() 的扩容操作其实就是从 stack 的顶端出发,最大可以扩展到 trapframe 的底端,如此之大的空间(整个 heap )

原先的 sbrk() 会调用 growproc()(见 kernel/proc.c),实现内存空间立即分配;现在我们需要将立即分配改成延迟分配(好处和缺点需要自己细品),延迟分配的第一步,就是在 sbrk() 被调用时仅仅做个标记,如此简单。kernel/sysproc.c:sys_sbrk() 代码如下,

sys_sbrk(void)
{
  int addr;
  int n;
  struct proc* p;

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

  p = myproc();
  addr = p->sz;

  /** sbrk越界处理 */
  if(addr+n>=TRAPFRAME || addr+n<=0)
    return addr;

  p->sz += n;

  /** 堆空间负增长 */
  if(n < 0)
    uvmdealloc(p->pagetable, addr, p->sz);
  
  return addr;
}

其中,需要说明的是 sbrk() 扩容 or 缩小都不要越界(下界可以为 0 ,也可以为 stack 之后的首地址,见 kernel/kalloc.c:end[] );如果 sbrk() 参数为负,在将内存释放之前需要解除其映射关系

iii. Result

手动进入 qemu

make qemu
$echo hi

此时是不能正常运行的,因为 xv6 并未分配实际的内存空间给 echo 应用程序,只是做了简单的内存扩容标注而已(类似于指针变量声明,实际上并未 malloc 内存交给该指针)

V. Lazy allocation (moderate)

i. Motivation

上一个实验,我们仅仅完成了内存空间需要分配的标注工作,实际上 xv6 并未给进程分配空间。所以在 Lab: Lazy allocation 中我们需要补上标注之后的空缺,即进程需要使用内存时(一般为第一次使用内存,缺页中断 page fault ),xv6 将在标注处为其分配物理内存

ii. Solution

主要是修改添加 kernel/trap.c:usertrap() 部分。仔细想想 xv6 的工作流程:当应用进程第一次使用到标注的内存时会发生怎么的操作?xv6 会拿着进程所需使用到的那块内存的虚拟地址,将其翻译成物理地址。如果此时物理内存中存在虚拟地址对应的页块,那么一切正常;但是,事与愿违,因为是 “第一次” ,所以物理内存中一定没有虚拟地址对应的 page ,故不可能存在上述假设,xv6 一定会发生异常(缺页中断),随后陷入中断处理

此时就需要在 usertrap() 中添加为进程在标注处分配新的内存空间的业务逻辑,Lab5: lazy page allocation 实验主页 中的 Lazy allocation 部分的提示很清晰明了,大致如下,

scause 值为13 or 15的是为缺页中断;stval 保存引起缺页中断的虚拟地址,等等细节,需要自己研读

下面,大刀阔斧来改革,代码如下,

usertrap(void)
{
 	...
  uint64 scause = r_scause();
  if(scause == 8){
    ...
  } else if(scause==13 || scause==15) { 
    /** 缺页中断 */
    uint64 va = r_stval();

    /** 虚拟地址是否合法(在堆区,见xv6 book Figure3.4) */
    if(va>=p->sz || va<=p->trapframe->sp)
      goto killing;
      
    /** 尝试分配空间(缺页中断handler) */
    char* mem = kalloc();
    if(mem == 0)
      goto killing;

    /** 做好新页的空间映射工作 */
    memset(mem, 0, PGSIZE);
    va = PGROUNDDOWN(va);
    if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) 
      goto freeing;

    /** 顺利handle缺页中断 */
    goto rest;

  freeing:
    kfree(mem);

  killing:
    p->killed = 1;

  rest:
    ;
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
   	...
  }
  ...
}

需要添加 if-else 分支,只是代码流程中不可避免的,请勿滥用 if-else ,否则代码将极其丑陋( goto 在跳出 if-else 分支中是个很好的选择,如果是跳出 for 循环,continue/break 是最好的选择)!

该段代码主要工作就是为进程在标注处分配新的内存空间,并建立映射关系。根据 Lab5: lazy page allocation 实验主页 提示,分配空间和建立映射关系的代码可以借鉴 kernel/vm.c:uvmalloc()

有些细节需要注意,kernel/vm.c:uvmunmap() 会提示我们虚拟地址对应的物理页块不存在 or 未建立映射关系,实验要求 xv6 即使遇到这两种情况也能够顺利运行,所以我们在这里选择忽略 panic 。具体缘由,我将在第三个补漏实验中详细解释,代码如下,

uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
	 ...
   for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
    if((pte = walk(pagetable, a, 0)) == 0)
      continue;
      // panic("uvmunmap: walk");
    if((*pte & PTE_V) == 0)
      continue;
      // panic("uvmunmap: not mapped");
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
   	...
  }
}

iii. Result

手动进入 qemu,

make qemu
$lazytests

进入 xv6 来查看输出结果,此时能通过 alloc 和 unmap 的测试

VI. Lazytests and Usertests (moderate)

i. Motivation

Lab: Lazytests and Usertests 主要的目的,就是想让我们处理一些在缺页中断机制中关键的临界 or 异常问题

ii. Solution

首先,根据 Lab5: lazy page allocation 实验主页 提示,在 kernel/syspro.c:sys_sbrk() 中添加缩小空间的业务处理函数,很简单,代码见 Lab: Eliminate allocation from sbrk() ,其中要点是需要释放物理空间并解除映射关系,

...
/** 堆空间负增长 */
if(n < 0)
  uvmdealloc(p->pagetable, addr, p->sz);
...

然后,还需要注意的是,在缺页中断过程中,如果虚拟地址越界 or 分配空间失败,则直接 kill 该进程。在 kernel/trap.c:usertrap() 中添加相应的细节,

usertrap(void)
{
 	...
  uint64 scause = r_scause();
  if(scause == 8){
    ...
  } else if(scause==13 || scause==15) { 
    /** 缺页中断 */
    uint64 va = r_stval();

    /** 虚拟地址是否合法(在堆区,见xv6 book Figure3.4) */
    if(va>=p->sz || va<=p->trapframe->sp)
      goto killing; 
    
    /** 尝试分配空间(缺页中断handler) */
    char* mem = kalloc();
    if(mem == 0)
      goto killing;
   	...

  killing:
    p->killed = 1;

	 ...
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
   	...
  }
  ...
}

此外,在调用 fork() 的时候要确保能够避开缺页等错误,顺利将父进程的页表拷贝(可 “深” 可 “浅” ,xv6 选择的是 “深” )至子进程中。kernel/proc.c:fork() 的修改代码如下,

uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
	...
  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0)
      continue;
      // panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      continue;
      // panic("uvmcopy: page not present");
   ...
  }
  return 0;
	...
}

父子进程页表的拷贝工作主要由 kernel/vm.c:uvmcopy() 完成,忽视掉该页块不存在 or 未建立映射关系的情况。这至关重要,因为采用 lazy allocation 机制,这就必然会出现踩空的情况(标注了,但还未分配真实内存空间);以及分配了空间,但还没有建立映射关系(父进程中存在空间已分配,但还未映射的情况?)

uvmcopy() 中,如果分配空间 or 建立映射失败,xv6 最终会去调用 uvmunmap() ,解除映射关系。这也就解释了 uvmunmap() 为什么要忽视空间未分配 or 空间未映射两种情况

试想,如果 uvmcopy() 未能成功分配空间,那么就会触发 uvmunmap() 。我们希望 uvmunmap() 很识相地跳过空间未分配问题,继续运行;未能成功建立映射也是同样的情形

最后,要能够应对读 or 写等系统调用可能出现的问题,比如向某个正确的虚拟地址中写数据,但此时在内存中并没有虚拟地址对应的物理空间

此时,我们或许会有疑问,不是 usertrap() 能够 handle 此类缺页中断嘛?等等,在此之前我们要理顺一件事:usertrap() 中的缺页中断处理函数,是应对从用户态切到内核的手段。但读 or 写等系统调用发生之后,xv6 已经进入了内核,根本不会再走一遍 usertrap() 中的缺页中断流程。此时的 xv6 只会想着如何将写系统调用传来的虚拟地址翻译成物理地址

在这期间(无论读 or 写)最终都会调用 walkaddr() (从读 or 写的 kernel/vm.c:copyin()kernel/vm.c:copyout 入手),所以只需在 kernel/vm.c:walkaddr() 中动点手脚即可,代码如下,

uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
  ...
  if(pte==0 || (*pte & PTE_V)==0) {
    /** 针对read或write,内存还未分配 */
    struct proc* p = myproc();

    /** 虚拟地址是否合法(在堆区,见xv6 book Figure3.4) */
    if(va>=p->sz || va<=p->trapframe->sp)
      return 0;

    char* mem = kalloc();
    if(mem == 0)
      return 0;

    memset(mem, 0, PGSIZE);
    va = PGROUNDDOWN(va);
    if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) {
      kfree(mem);
      return 0;
    }

    return (uint64)mem;
  }
	...
}

usertrap() 中的缺页中断机制如出一辙,越界检查,分配内存,一气呵成

iii. Result

手动进入 qemu,

make qemu
$lazytests
$usertests

VII. Reference

  1. CSDN - 操作系统实验Mit6.S081笔记 Lab5: Lazy allocation
  2. 知乎 - MIT 6.S081 Lab5: xv6 lazy page allocation
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值