XV6学习(7)Lab lazy
代码在github上。
这一个实验是要利用缺页异常来实现懒分配(lazy allocation)。用户态程序通过sbrk
系统调用来在堆上分配内存,而sbrk
则会通过kalloc
函数来申请内存页面,之后将页面映射到页表当中。
当申请小的空间时,上述过程是没有问题的。但是如果当进程一次申请很大的空间,如数GB的空间,再使用上述策略来一页页地申请映射的话就会非常的慢(1GB/4KB=262,144)。这时候就引入了lazy allocation技术,当调用sbrk
时不进行页面的申请映射,而是仅仅增大堆的大小,当实际访问页面时,就会触发缺页异常,此时再申请一个页面并映射到页表中,这是再次执行触发缺页异常的代码就可以正常读写内存了。
通过lazy allocation技术,就可以将申请页面的开销平摊到读写内存当中去,在sbrk
中进行大量内存页面申请的开销是不可以接受的,但是将代价平摊到读写操作当中去就可以接受了。
总体来说这一个实验的难度并不大,理解了上一个trap的实验以及缺页异常就能比较轻松地完成了。
Eliminate allocation from sbrk() (easy)
这一个就是要修改sbrk
函数,使其不调用growproc
函数进行页面分配,关键就是p->sz += n
将堆大小增大,然后注释掉growproc
。if(n < 0)
是后面部分的内容。
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
struct proc *p = myproc();
addr = p->sz;
p->sz += n;
if(n < 0) {
p->sz = uvmdealloc(p->pagetable, addr, addr + n);
}
// if(growproc(n) < 0)
// return -1;
return addr;
}
Lazy allocation (moderate)
接下来就是真正实现Lazy allocation:当系统发生缺页异常时,就会进入到usertrap
函数中,此时scause
寄存器保存的是异常原因(13为page load fault,15为page write fault),stval
是引发缺页异常的地址。
在usertrap
判断scause
为13或15后,就可以读取stval
获取引发异常的地址,之后调用lazy_alloc
对该地址的页面进行分配即可。在这里不需要进行p->trapframe->epc += 4
操作,因为我们要返回发生异常的那条指令并重新执行。
void
usertrap(void)
{
...
} else if((which_dev = devintr()) != 0){
// ok
} else if (r_scause() == 13 || r_scause() == 15) {
// 13: page load fault; 15: page write fault
// printf("page fault\n");
uint64 addr = r_stval();
if (lazy_alloc(addr) < 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;
}
...
}
在lazy_alloc
函数中,首先判断地址是否合法,之后通过PGROUNDDOWN
宏获取对应页面的起始地址,然后调用kalloc
分配页面,memset
将页面内容置0,最后调用mappages
将页面映射到页表中去。
int
lazy_alloc(uint64 addr) {
struct proc *p = myproc();
// page-faults on a virtual memory address higher than any allocated with sbrk()
// this should be >= not > !!!
if (addr >= p->sz) {
// printf("lazy_alloc: access invalid address");
return -1;
}
if (addr < p->trapframe->sp) {
// printf("lazy_alloc: access address below stack");
return -2;
}
uint64 pa = PGROUNDDOWN(addr);
char* mem = kalloc();
if (mem == 0) {
// printf("lazy_alloc: kalloc failed");
return -3;
}
memset(mem, 0, PGSIZE);
if(mappages(p->pagetable, pa, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
return -4;
}
return 0;
}
Lazytests and Usertests (moderate)
这一部分就是要强化上面写的的lazy allocation,使其能在一些特殊情况下工作。
Handle negative sbrk() arguments.
这一个就是在上面的sys_sbrk
函数中的if(n < 0)
部分,当参数为负数时,调用uvmdealloc
取消分配。
Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().
这一个即lazy_alloc
函数中的addr >= p->sz
部分,当访问的地址大于堆的大小时就说明访问了非法地址,注意这里是>=
而不是>
。
Handle the parent-to-child memory copy in fork() correctly.
在fork
函数中通过uvmcopy
进行地址空间的拷贝,我们只要将其中panic
的部分改为continue
就行了,当页表项不存在时并不是说明出了问题,直接跳过就可以了。
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.
当进程通过read
或write
等系统调用访问未分配页面的地址时,并不会通过页表硬件来访问,也就是说不会发生缺页异常;在内核态时是通过walkaddr
来访问用户页表的,因此在这里也要对缺页的情况进行处理。
当出现pte == 0 || (*pte & PTE_V) == 0
时,就说明发生了缺页,这时只要调用lazy_alloc
进行分配,之后再次使用walk
就能正确得到页表项了。
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 || (*pte & PTE_V) == 0) {
if (lazy_alloc(va) == 0) {
pte = walk(pagetable, va, 0);
} else {
return 0;
}
}
if((*pte & PTE_U) == 0)
return 0;
pa = PTE2PA(*pte);
return pa;
}
Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.
当kalloc
失败时,lazy_alloc
就会返回负值,此时判断返回值然后p->killed = 1
就行了。
Handle faults on the invalid page below the user stack.
这一个可以通过addr < p->trapframe->sp
判断,当地址小于栈顶地址时就说明发生了非法访问。