Lab5 惰性分配
预备知识
惰性分配
当一个用户进程申请内存时,OS并不会立即分配所需要的全部物理内存,而是仅仅记录已分配的用户地址并在用户页表中将这些PTE标记为无效。当进程首次尝试使用这些内存页时,CPU会触发页面错误异常,内核在此时才会进行物理内存分配。
页面故障异常
RISC-V中有三种页面故障异常:
- 加载页面错误(错误号13):当加载指令访问的虚拟地址找不到对应的物理地址;
- 存储页面错误(错误号15):当存储指令访问的虚拟地址找不到对应的物理地址;
- 指令页面错误(错误号12):当指令获取的虚拟地址找不到对应的物理地址。
对于这些异常的信息,SCAUSE寄存器中的值指示页面错误的类型,即具体是哪一种,STVAL寄存器保存了不能转换的虚拟地址。为了在完成页面分配后重新执行触发异常的指令,还需要其地址。页面故障异常也是trap的一部分,因此这个指令地址会保存在SEPC寄存器中,同时在usertrap()中保存在进程的trapframe->epc中。
Eliminate allocation from sbrk()
目前Xv6会立即进行内存分配,因此第一个任务是删除sbrk(n)系统调用。代码位于sysproc.c中的sys_sbrk()函数。sbrk(n)系统调用将进程的内存大小增加n字节,然后返回新分配区域的起始地址。新的 sbrk(n) 只需将进程的大小增加 n,而不分配内存,也即删除 growproc() 的调用,而只在myproc()->sz记录。
//kernel/sysproc.c
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
//注释掉growproc()
//if(growproc(n) < 0)
// return -1;
myproc()->sz += n;
return addr;
}
运行一下echo,可以看到SCAUESE中的错误号是15
$ echo hi
usertrap(): unexpected scause 0x000000000000000f pid=3
sepc=0x00000000000012ac stval=0x0000000000004008
panic: uvmunmap: not mapped
Lazy allocation
修改usertrap()函数,增加对错误号为13或15的处理。也就是为STVAL中的虚拟地址分配物理内存,实现惰性分配。
//kernel/trap.c:usertrap()
...
else if((which_dev = devintr()) != 0){
// ok
} else if(r_scause() == 13 || r_scause() == 15){
struct proc* p = myproc();
uint64 va = r_stval();//获取发生异常的虚拟地址
char* pa = kalloc(); //等待分配的物理地址
if( pa==0 ){
printf("超出内存!\n");
p->killed = 1;
} else{
memset(pa, 0 ,PGSIZE);
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
printf("分配内存过程失败!\n");
kfree(pa);
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;
}
除此之外,题目还说要修改uvmunmap()以避免在某些页面未映射时引发 panic。这是因为惰性分配最开始只修改了sz,比如说在利用sz/PGSIZE计算npages时就会得到很多未建立映射的虚拟地址,这时去解除映射就会触发panic。注释这一部分即可
//kernel/vm.c
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 & PTE_V) == 0)
// panic("uvmunmap: not mapped");
continue;
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
现在执行echo,即可正常处理
Lazytests and Usertests
完善惰性分配代码,使其能够处理各种情况。
- 处理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;
if( n > 0){
myproc()->sz += n;
} else if( mycpu() + n > 0){
myproc()->sz = uvmdealloc( myproc()->pagetable, myproc()->sz, myproc()->sz + n);
} else{
printf("缩减后进程内存小于0!\n");
return -1;
}
return addr;
}
- 确保虚拟地址是有效的:处理进程在高于sbrk()分配的虚拟地址发生故障;处理用户栈下方的保护页面故障
if(PGROUNDUP(p->trapframe->sp)-1 < va && va < p->sz){
char* pa = kalloc(); //等待分配的物理地址
if( pa == 0 ){
printf("超出内存!intrap\n");
p->killed = 1;
} else {
memset(pa, 0 ,PGSIZE);
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
printf("分配内存过程失败!\n");
kfree(pa);
p->killed = 1;
}
}
} else
p->killed = 1;
- fork()中的父子内存复制。fork()会调用uvmcopy()复制父进程的页表,而父进程的页表也有可能是未实际分配的。同样也是注释掉里面的panic
//kernel/vm.c uvmcopy()
if((pte = walk(old, i, 0)) == 0)
//panic("uvmcopy: pte should exist");
continue;;
if((*pte & PTE_V) == 0)
//panic("uvmcopy: page not present");
continue;
- 进程从sbrk()传递一个有效地址到系统调用(如read或write),但该地址对应的内存尚未分配。
这应该是在说现在的sbrk()虽然没有实际分配内存了,但是系统已经记录了内存的变化(sz),因此也有可能使用到未映射的虚拟地址。
根据write的调用流程,write系统调用后会通过copyin来将用户空间的虚拟地址通过转化为物理地址从而传递到内核空间。这时如果虚拟地址未分配内存就会导致调用失败,因此我们在这里面找到报错的地址改成分配内存即可,也就是walkaddr()函数。
//kernel/vm.c: walkaddr
...
pte = walk(pagetable, va, 0);
//新增
struct proc* p = myproc();
if(pte == 0 || (*pte & PTE_V) == 0){
if(PGROUNDUP(p->trapframe->sp)-1 < va && va < p->sz){
char* mem = kalloc();
if( mem == 0){
p->killed = 1;
return 0;
} else{
memset(mem, 0, PGSIZE);
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
printf("分配内存过程失败!\n");
kfree(mem);
p->killed = 1;
return 0;
}
return (uint64)mem;
}
}
}
// if(pte == 0)
// return 0;
// if((*pte & PTE_V) == 0)
// return 0;
此时执行lazytests 和uertests已经能够全部通过。
$ lazytests
lazytests starting
running test lazy alloc
test lazy alloc: OK
running test lazy unmap
test lazy unmap: OK
running test out of memory
test out of memory: OK
ALL TESTS PASSED
$ usertests
...
test iref: OK
test forktest: OK
test bigdir: OK
超出内存!
ALL TESTS PASSED

浙公网安备 33010602011771号