目录
- 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 的最大地址:
代码(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
:
这里会发生 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 来处理:
添加并实现 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); // 为这块地址填充 0va = 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):
完成以上修改后,make qemu 之后就可以正常运行 echo hi
了:
Task 3: Lazytests and Usertests
前一个 task 实现了一个简单的 lazy allocation,执行 echo hi
是没问题了,但在更复杂的场景下,仍然有许多需要考虑的事情,本 task 要求完善 lazy allocation 并能够通过 lazytests
和 usertests
两个测试。
首先为了实现的方便,这里将实际分配物理内存的代码逻辑封装到 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); // 为这块地址填充 0va = 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 未映射时,直接跳过:
我们还需要正确处理 fork 时父进程向子进程 copy 内存的逻辑,这里需要修改 uvmcopy()
函数,也是在页表不存在或 PTE 未映射时直接跳过:
还有一种情况是,当用户程序把通过 sbrk() 申请的内存(但还未实际分配)的内存地址传递给系统调用时,kernel 可能会在 copyin
和 copyout
这两个函数中访问这个内存地址,而 kernel 内是无法像用户程序那样走 page fault handler 来 lazy allocation 的,所以我们必须在 copyin
和 copyout
函数内也实现“访问用户程序传来的内存地址时做 lazy allocation”的逻辑:
这里 copyout 通过 walkaddr
来将 va 借助 user page table 来翻译得到 pa,但由于我们采用了 lazy allocation 机制,所以这里可能无法找到 PTE 映射,所以,当没有找到 PTE 映射时,我们需要立刻为其分配物理内存并修改 user page table,这也是上图红方框内代码的逻辑。对于 copyin 函数,所做的修改类似:
至此,我们完成了 lazy allocation,测试如下:
运行 lazytests:
运行 usertests: