代码在github上。git
这一个实验是要利用缺页异常来实现懒分配(lazy allocation)。用户态程序经过sbrk
系统调用来在堆上分配内存,而sbrk
则会经过kalloc
函数来申请内存页面,以后将页面映射到页表当中。github
当申请小的空间时,上述过程是没有问题的。可是若是当进程一次申请很大的空间,如数GB的空间,再使用上述策略来一页页地申请映射的话就会很是的慢(1GB/4KB=262,144)。这时候就引入了lazy allocation技术,当调用sbrk
时不进行页面的申请映射,而是仅仅增大堆的大小,当实际访问页面时,就会触发缺页异常,此时再申请一个页面并映射到页表中,这是再次执行触发缺页异常的代码就能够正常读写内存了。app
经过lazy allocation技术,就能够将申请页面的开销平摊到读写内存当中去,在sbrk
中进行大量内存页面申请的开销是不能够接受的,可是将代价平摊到读写操做当中去就能够接受了。函数
整体来讲这一个实验的难度并不大,理解了上一个trap的实验以及缺页异常就能比较轻松地完成了。ui
这一个就是要修改sbrk
函数,使其不调用growproc
函数进行页面分配,关键就是p->sz += n
将堆大小增大,而后注释掉growproc
。if(n < 0)
是后面部分的内容。this
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:当系统发生缺页异常时,就会进入到usertrap
函数中,此时scause
寄存器保存的是异常缘由(13为page load fault,15为page write fault),stval
是引起缺页异常的地址。code
在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; }
这一部分就是要强化上面写的的lazy allocation,使其能在一些特殊状况下工做。get
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
判断,当地址小于栈顶地址时就说明发生了非法访问。