一 实验原理
当进程调用sbrk()分配地址空间时,会从用户态进入到内核态,通过trap handler程序完成系统调用,并通过sys_sbrk()为用户分配虚拟地址和物理地址。分配物理地址,会加大操作系统的开销,而且为进程分配的物理地址未必会被使用到。一种提高地址分配效率的办法是在一开始并不为进程分配物理地址,当进程需要访问虚拟地址时,触发缺页中断,内核在处理缺页中断的时候为进程分配物理地址。这种结合缺页中断分配地址的策略,虽然可以减少分配物理地址所带来的开销,但是另一方面会增加用户态与内核态之间切换带来的开销。
二 实验部分
这个lab实现lazy page allocation的策略。
1 在kernel/trap.c的usertrap函数中,当scause寄存器的值为13或者15时表明此时是缺页中断,通过stval寄存器获得造成缺页中断的虚拟地址,在保证缺页中断的地址合理的情况下,为其分配物理地址。
else if(r_scause()==13||r_scause()==15){
uint64 va=r_stval();
if(va>p->sz||PGROUNDUP(va)==PGROUNDDOWN(p->trapframe->sp)) exit(-1);
char *mem=kalloc();
if(mem==0)
exit(-1);
if(mappages(p->pagetable,PGROUNDDOWN(va),PGSIZE,(uint64)mem,PTE_R|PTE_W|PTE_X|PTE_U)!=0){
kfree(mem);
exit(-1);
}
}
2 在kernel/sysproc.c的sys_sbrk函数中,当n为正值时,不分配物理地址,只是update p->sz的值,当n为负值时,释放地址,最终返回进程的old size。
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
if(n>0)
myproc()->sz+=n;
else if(growproc(n)<0)
return -1;
return addr;
}
3 在kernel/vm.c的uvmunmap函数中,进程需要释放物理页时,可能会释放还没有分配的虚拟地址。同理fork会调用uvmcopy为子进程复制父进程的内存,而父进程可能存在还未被分配的地址。
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)
continue;//panic("uvmunmap: walk");
if((*pte & PTE_V) == 0)
continue;//panic("uvmunmap: not mapped");
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
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)
continue;//panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
continue;//panic("uvmcopy: page not present");
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;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
4 当进程调用write pipe 和read去解引用user space的地址时,可能会解引用还未被分配的地址。而此时因为系统调用,进程已经从用户态进入到了内核态,发生缺页中断时会执行kerneltrap而并不是usertrap,因此需要为这三种情况分配地址。allocmem函数传入的参数是页表、虚拟地址以及需要分配的地址的大小,若虚拟地址合理且没有分配相应的物理地址,则为其分配地址。然后在sys_write sys_read 和 sys_pipe的合适地方调用该函数即可。
kernel/sysfile.c
uint64
allocmem(pagetable_t pgtbl,uint64 addr,int n){
uint64 va=PGROUNDDOWN(addr),pa;
if(addr>myproc()->sz||addr+n>myproc()->sz)
return -1;
int nop=(PGROUNDDOWN(addr+n)-va)/PGSIZE;
if(nop==0) nop=1;
for(int i=0;i<nop;i++,va+=PGSIZE){
if((pa=walkaddr(pgtbl,va))==0){
char *mem=kalloc();
if(mem==0){
uvmunmap(pgtbl,PGROUNDDOWN(addr),i,1);
return -1;
}
memset(mem,0,PGSIZE);
pa=(uint64)mem;
if(mappages(pgtbl,va,PGSIZE,pa,PTE_R|PTE_W|PTE_X|PTE_U)!=0){
kfree(mem);
uvmunmap(pgtbl,PGROUNDDOWN(addr),i,1);
return -1;
}
}
}
return 0;
}
//sys_read
uint64
sys_read(void)
{
struct file *f;
int n;
uint64 p;
if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argaddr(1, &p) < 0)
return -1;
if(allocmem(myproc()->pagetable,p,n)<0)
return -1;
return fileread(f, p, n);
}
//sys_write
uint64
sys_write(void)
{
struct file *f;
int n;
uint64 p;
if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argaddr(1, &p) < 0)
return -1;
if(allocmem(myproc()->pagetable,p,n)<0)
return -1;
return filewrite(f, p, n);
}
//sys_pipe
uint64
sys_pipe(void)
{
uint64 fdarray; // user pointer to array of two integers
struct file *rf, *wf;
int fd0, fd1;
struct proc *p = myproc();
if(argaddr(0, &fdarray) < 0)
return -1;
if(pipealloc(&rf, &wf) < 0)
return -1;
fd0 = -1;
if((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0){
if(fd0 >= 0)
p->ofile[fd0] = 0;
fileclose(rf);
fileclose(wf);
return -1;
}
allocmem(p->pagetable,fdarray,sizeof(fd0)+sizeof(fd1));
if(copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) < 0 ||
copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1)) < 0){
p->ofile[fd0] = 0;
p->ofile[fd1] = 0;
fileclose(rf);
fileclose(wf);
return -1;
}
return 0;
}