【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation

目录

    • Task 1: Eliminate allocation from sbrk()
    • Task 2: Lazy allocation
    • Task 3: Lazytests and Usertests

在学习了 page fault 这一节课后,了解了操作系统是如何结合 page table 和 trap 利用 page fault 来实现一系列的神奇的功能。这个 lab 就是在 XV6 中实现 lazy allocation 机制

xv6 默认是 eager allocation,也就是用户程序一旦调用 sbrk,内核会立刻分配应用程序所需要的物理内存。这个实验就是将其修改为 lazy allocation,用户在调用 sbrk 时不会立刻分配物理内存,只是做一个记录,等到程序真正读写这个内存 page 时,才会因触发 page fault 而让内核分配一个实际的物理内存并修改到 page table 中。

Task 1: Eliminate allocation from sbrk()

这个 task 让我们删除 sbrk 的系统调用实现函数 sys_sbrk() 中分配物理内存的代码,内核只需要增大 myproc()->sz 这个值即可。

myproc()->sz 这个值记录的当前用户进程已申请的 heap 的最大地址:
【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第1张图片

代码(kernel/sysproc.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;
  myproc()->sz += n;
  return addr;
}

make qemu 后执行 echo hi

【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第2张图片
这里会发生 page fault,在下面的 task 中,我们将处理发生的 page fault,并为其分配物理内存 page 且修改对应的 page table。

Task 2: Lazy allocation

这个 task 需要修改代码来处理 page fault 并为用户程序分配物理内存。

当 page fault 发生时,程序会通过 trap 机制进入 usertrap() 函数,我们可以在这里实现相关的而逻辑。SCAUSE 寄存器记录了本次 trap 发生的原因,当寄存器的值为 13 或 15 时,表示因 load 或 store 指令访问一个地址但没找到相应 PTE 而发生 page fault,所以我们需要在 usertrap() 函数中判断 SCAUSE 寄存器的值,并实现相应的 page fault 处理逻辑。

首先在 usertrap() 函数(kernel/trap.c)中添加对 page fault 的识别并调用 page fault handler 来处理:

【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第3张图片
添加并实现 page_fault_handler() 函数(代码紧跟在 usertrap 函数后面就可以):

//
// handle page fault
//
void 
page_fault_handler(struct proc * const p)
{
  uint64 va = r_stval();  // 触发 page fault 的虚拟地址
  if (p->sz <= va || va < p->trapframe->sp) {  // 如果 va 高于 sbrk 申请的地址或者低于栈顶地址
    p->killed = 1;
  } else {
    uint64 ka = (uint64) kalloc();
    if (ka == 0) {  // 如果物理内存不足
      p->killed = 1;
    } else {
      memset((void*) ka, 0, PGSIZE);  // 为这块地址填充 0
      va = PGROUNDDOWN(va);  // round the faulting virtual address down to a page boundary.
      // 将 va -> ka 的 mapping 添加到 user page table 中
      if (mappages(p->pagetable, va, PGSIZE, ka, PTE_W | PTE_X | PTE_U | PTE_R) != 0) {
        kfree((void*) ka);
        p->killed = 1;
      }
    }
  }
}

还存在一个问题,因为用户申请的内存并没有一定分配实际的物理内存,所以在对申请但未分配的内存做 unmap 时会产生错误,因此需要对 unmap 的代码进行修改,当想要释放一个未分配的 page 时,代码中只需要直接忽视就可以了(vm.c):

【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第4张图片
完成以上修改后,make qemu 之后就可以正常运行 echo hi 了:

【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第5张图片

Task 3: Lazytests and Usertests

前一个 task 实现了一个简单的 lazy allocation,执行 echo hi 是没问题了,但在更复杂的场景下,仍然有许多需要考虑的事情,本 task 要求完善 lazy allocation 并能够通过 lazytestsusertests 两个测试。

首先为了实现的方便,这里将实际分配物理内存的代码逻辑封装到 alloc_memory_page() 函数(kernel/vm.c)中:

// 分配一个实际物理内存,并映射到 va 中,将这个 mapping 添加到 page table 中
uint64
alloc_memory_page(uint64 va, pagetable_t pagetable)
{
  uint64 ka = (uint64) kalloc();
  if (ka == 0) {  // 如果物理内存不足
    return 0;
  }
  memset((void*) ka, 0, PGSIZE);  // 为这块地址填充 0
  va = PGROUNDDOWN(va);  // round the faulting virtual address down to a page boundary.
  if (mappages(pagetable, va, PGSIZE, ka,  PTE_W | PTE_X | PTE_R | PTE_U) != 0) {
    kfree((void*) ka);
    return 0;
  }
  return ka;
}

这个函数通过 kalloc() 来分配一个物理内存 page,并将其映射到 va 中,然后将这个 mapping 添加到 page table 中。当成功时,函数返回分配的物理内存地址,当失败时(内存不足或添加 mapping 失败),函数返回 0。

我们将这个 alloc_memory_page() 函数的声明放到 defs.h 头文件中。

有了这个 alloc_memory_page 函数,我们在上一个实验写的 page fault handler 就可以简化一下了,分配物理内存的逻辑改为调用 alloc_memory_page 即可:

//
// handle page fault
//
void 
page_fault_handler(struct proc * const p)
{
  uint64 va = r_stval();
  if (p->sz <= va || va < p->trapframe->sp) {  // 如果 va 高于 sbrk 申请的地址或者低于栈顶地址
    p->killed = 1;
  } else {
    if (alloc_memory_page(va, p->pagetable) == 0) {  // 当分配内存或添加 mapping 失败,就直接杀死该进程
      p->killed = 1;
    }
  }
}

修改 uvmummap() 函数,当无法从 page table 中找到 va 的 PTE 时或者 PTE 未映射时,直接跳过:

【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第6张图片
我们还需要正确处理 fork 时父进程向子进程 copy 内存的逻辑,这里需要修改 uvmcopy() 函数,也是在页表不存在或 PTE 未映射时直接跳过:

【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第7张图片
还有一种情况是,当用户程序把通过 sbrk() 申请的内存(但还未实际分配)的内存地址传递给系统调用时,kernel 可能会在 copyincopyout 这两个函数中访问这个内存地址,而 kernel 内是无法像用户程序那样走 page fault handler 来 lazy allocation 的,所以我们必须在 copyincopyout 函数内也实现“访问用户程序传来的内存地址时做 lazy allocation”的逻辑:

【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第8张图片
这里 copyout 通过 walkaddr 来将 va 借助 user page table 来翻译得到 pa,但由于我们采用了 lazy allocation 机制,所以这里可能无法找到 PTE 映射,所以,当没有找到 PTE 映射时,我们需要立刻为其分配物理内存并修改 user page table,这也是上图红方框内代码的逻辑。对于 copyin 函数,所做的修改类似:

【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第9张图片
至此,我们完成了 lazy allocation,测试如下:

运行 lazytests:
【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第10张图片
运行 usertests:
【MIT 6.S081】2020, 实验记录(5),Lab: lazy allocation_第11张图片

你可能感兴趣的:(MIT6.S081,c语言,操作系统)