实验目的
实现一个内存页懒分配机制,在调用 sbrk() 的时候,不立即分配内存,而是只作记录。在访问到这一部分内存的时候才进行实际的物理内存分配。
本次 lab 分为三个部分,但其实都是属于同一个实验的不同步骤,所以本文将三点集合到一起:
Eliminate allocation from sbrk()[easy]
在sbrk()的具体实现函数sys_sbrk()中只增加(或者减少)进程内存大小,但不实际分配内存(不调用growproc()),下面的代码考虑到了sbrk参数为负的情况
//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;
*/
//+ lazy allocation
uint64 sz = myproc()->sz;
if(n > 0) {
// lazy allocation
myproc()->sz += n;
} else if(sz + n > 0) {
sz = uvmdealloc(myproc()->pagetable, sz, sz + n);
myproc()->sz = sz;
} else {
return -1;
}
return addr;
}
Lazy allocation[moderate] & Tests[moderate]
- Modify the code in trap.c to respond to a page fault from user space by mapping a newly-allocated page of physical memory at the faulting address
- Then returning back to user space to let the process continue executing.
- You should add your code just before the printf call that produced the “usertrap(): …” message.
提示 - You can check whether a fault is a page fault by seeing if r_scause() is 13 or 15 in usertrap().
- r_stval() returns the RISC-V stval register, which contains the virtual address that caused the page fault.
- Steal code from uvmalloc() in vm.c, which is what sbrk() calls (via growproc()). You’ll need to call kalloc() and mappages().(参照uvmalloc()代码,kalloc()和mappages()结合使用来分配空间和建立映射)
- Use PGROUNDDOWN(va) to round the faulting virtual address down to a page boundary.
- uvmunmap() will panic; modify it to not panic if some pages aren’t mapped.(不要直接抛出panic)
- If the kernel crashes, look up sepc in kernel/kernel.asm
- Use your vmprint function from pgtbl lab to print the content of a page table.
- If you see the error “incomplete type proc”, include “spinlock.h” then “proc.h”.(proc.h之前要先包含spinlock.h)
- Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().(造成page fault的虚拟地址不能超过进程已经分配的虚拟地址大小p->sz)
- Handle the case in which a process passes a valid address from sbrk() to a system call such as read or write,but the memory for that address has not yet been allocated.(对于有效地址(PTE_V)但物理空间还没有分配的情况应该予以处理,不能像之前那样抛出panic)
- Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.(即如果kalloc返回0,修改p->killed为非零值)
- Handle faults on the invalid page below the user stack.
实验过程
1.修改 usertrap 用户态 trap 处理函数,为缺页异常添加检测,如果为缺页异常((r_scause() == 13 || r_scause() == 15)),且发生异常的地址是由于懒分配而没有映射的话,就为其分配物理内存,并在页表建立映射:
// kernel/trap.c
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
void
usertrap(void)
{
// ......
syscall();
} else if((which_dev = devintr()) != 0){
// ok
} else {
uint64 va = r_stval();
if((r_scause() == 13 || r_scause() == 15) && uvmshouldtouch(va)){ // 缺页异常,并且发生异常的地址进行过懒分配
uvmlazytouch(va); // 分配物理内存,并在页表创建映射
} 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;
}
}
// ......
}
2.uvmlazytouch()和uvmshouldtouch()函数的实现
uvmlazytouch()函数负责分配实际的物理内存并建立映射。懒分配的内存页在被 touch 后就可以被使用了。
uvmshouldtouch()用来判断虚拟地址是否是待懒分配的地址,下面三种情况都满足表明它是这样的地址:
- 该虚拟地址没有超过进程已经分配的地址
- 该虚拟地址不是守护页地址(具体见 xv6 book,栈页的低一页故意留成不映射,作为哨兵用于捕捉 stack overflow 错误。懒分配不应该给这个地址分配物理页和建立映射,而应该直接抛出异常)
- 页表项不存在
//kernel/vm.c
//+
// touch a lazy-allocated page so it's mapped to an actual physical page
void uvmlazytouch(uint64 va) {
struct proc *p = myproc();
char *mem = kalloc();
if(mem == 0) {
printf("lazy alloc : out of memory");
p->killed = 1; //kill because kalloc() failed
}
else {
memset(mem, 0, PGSIZE);
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) {
printf("lazy alloc : failed to map page\n");
kfree(mem);
p->killed = 1;
}
}
}
// whether a page is previously lazy-allocated and needed to be touched before use
int uvmshouldtouch(uint64 va) {
pte_t *pte;
struct proc *p = myproc();
return va < p->sz //within size of memory for process
&& PGROUNDDOWN(va) != r_sp() //not acessing stack guard page(it shouldn't mapped)
&& ((pte = walk(p->pagetable, va, 0)) == 0 || (*pte & PTE_V) == 0);//pagetable entry does not exist
}
3.由于懒分配的页,在刚分配的时候是没有对应的映射的,所以要把一些原本在遇到无映射地址时会 panic 的函数的行为改为直接忽略这样的地址。
//kernel/vm.c
//修改这里解决了proc_freepagetable()中调用uvmunmap()出现的panic
// Remove npages of mappings starting from va. va must be
// page-aligned. The mappings must exist.
// Optionally free the physical memory.
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
if((pte = walk(pagetable, a, 0)) == 0)
//panic("uvmunmap: walk");
continue; //if pte does not exist,
if((*pte & PTE_V) == 0)
//panic("uvmunmap: not mapped");
continue; //if pte does not valid
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
//kernel/vm.c
//修改这个解决了fork时调用uvmcopy()产生的panic
// 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");
continue; //a pte does not exist,we consider it might be a lazy page
if((*pte & PTE_V) == 0)
//panic("uvmcopy: page not present");
continue; a pte does not valid,we consider it might be a lazy page
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;
4.copyin() 和 copyout():内核/用户态之间互相拷贝数据
由于这里可能会访问到懒分配但是还没实际分配的页,所以要加一个检测,确保 copy 之前,用户态地址对应的页都有被实际分配和映射。
// kernel/vm.c
// 修改这个解决了 read/write 时的错误 (usertests 中的 sbrkarg 失败的问题)
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
if(uvmshouldtouch(dstva))
uvmlazytouch(dstva);
// ......
}
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
uint64 n, va0, pa0;
if(uvmshouldtouch(srcva))
uvmlazytouch(srcva);
// ......
}
Lazytests and Usertests[moderate]
提示
- Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().(造成page fault的虚拟地址不能超过进程已经分配的虚拟地址大小p->sz)
- Handle the case in which a process passes a valid address from sbrk() to a system call such as read or write,but the memory for that address has not yet been allocated.(对于有效地址(PTE_V)但物理空间还没有分配的情况应该予以处理,不能像之前那样抛出panic)
- Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.(即如果kalloc返回0,修改p->killed为非零值)
- Handle faults on the invalid page below the user stack.