lazy page allocation
所谓的lazy allocation就是当用户进程通过sbrk()
申请内存时,内核不会立即为其分配内存,而只是简单的增加了用户进程内存地址范围(增加p->sz
),也没有为其分配页表项。这主要是因为用户程序申请的内存量可能会超过其实际使用的量,另一方面可以加快sbrk()
的执行速度。
当cpu访问到lazy allocation的虚拟地址时,由于没有对应的页表项会触发缺页中断,我们要做的就是在缺页中断处理函数中为其分配物理内存。发生中断时,scause
中存储的是中断类型,stval
存储的是发生中断时访问的虚拟地址。下表给出了RISC-V支持的中断类型,第二列就是发生中断时scause
的值,本实验中只涉及到两类缺页中断:Load page fault(scause=13)、Store page fault(scause=15)。
实验代码
本实验的三个部分实际上是实现lazy allocation的三步,为此这里不再分开讲述,而是直接讲述完整的lazy allocation的代码。
lazy allocation的第一步就是修改sys_sbrk()
,这里需要强调的是lazy allocation对于申请内存空间是延迟执行的,但是对于释放内存空间却是立即执行的。即如果sbrk(n)
的参数是负数,sys_sbrk()
应该调用uvmdealloc
缩减内存空间:
// sysproc.c
uint64
sys_sbrk(void)
{
int addr;
int n;
if (argint(0, &n) < 0)
return -1;
struct proc* p = myproc();
addr = p->sz;
// 如果是缩减内存,则立即执行
if (n < 0) {
uvmdealloc(p->pagetable, addr, addr + n);
}
// 如果是扩张内存,则采用lazy allocation的机制
p->sz += n;
return addr;
}
接下来就是在中断处理函数中为进程分配内存页面,如上所述我们只需要处理scause
为13、或15的中断;且发生缺页异常的虚拟地址必须是在进程的有效内存地址范围内,为此我们需要先实现一个shouldAlloc()
来判断缺页中断的虚拟地址是否合法:
// trap.c
// 检查虚拟地址是否处于lazy allocation的区域
// 要求虚拟地址不能在栈底以下,堆顶以上,并且还要是没有分配页表项的虚拟地址
int shouldAlloc(uint64 va) {
pte_t* pte;
struct proc* p = myproc();
return va < p->sz // within size of memory for the process
&& va < r_sp() // not accessing stack guard page (it shouldn't be mapped)
&& (((pte = walk(p->pagetable, va, 0)) == 0) || ((*pte & PTE_V) == 0)); // page table entry does not exist
}
接下来实现为进程申请页面的alloc()
,根据Hints如果内存分配失败映射杀死发生缺页中断的进程:
// trap.c
// 为虚拟地址分配页面
void alloc(uint64 va) {
struct proc* p = myproc();
char* mem = kalloc();
if (mem == 0) {
// failed to allocate physical memory
printf("lazy alloc: out of memory\n");
p->killed = 1;
}
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;
}
}
}
然后是在trap.c
中处理缺页中断:
// trap.c
void
usertrap(void)
{
...
else if ((which_dev = devintr()) != 0) {
// ok
}
else if (r_scause() == 13 || r_scause() == 15) {
uint64 va = r_stval();
// printf("page fault:%p\n", va);
if (shouldAlloc(va)) { // 由于lazy allocation引起的缺页异常
alloc(va);
}
else {
p->killed = 1;
}
}
...
}
到此还没有结束,我们还需要修改uvmunmap()
,它会在proc_freepagetable()
中被调用,用来释放进程的所有内存。因为lazy allocation没有为部分虚拟地址空间映射对应的页表项,所以要把uvmunmap()
中一些原本在遇到无映射地址时会 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) {
continue; // 如果页表项不存在,跳过当前地址
}
if((*pte & PTE_V) == 0){
continue; // 如果页表项不存在,跳过当前地址
}
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
另外一个相同点是在fork时,uvmcopy也会遍历进程的页表,也存在类似问题:
// kernel/vm.c
// 修改这个解决了 fork 时的 panic
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; // 忽略不存在的映射项
if((*pte & PTE_V) == 0)
continue; // 忽略不存在的映射项
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;
}
至此lazytests
要求的都完成了,但是还没发通过usertests
的sbrkarg()
。因为sbrkarg会涉及到内核与用户地址之间的内存拷贝,在拷贝之前需要完成内存的分配:
// kernel/vm.c
// 修改这个解决了 read/write 时的错误 (usertests 中的 sbrkarg 失败的问题)
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
if(shouldAlloc(dstva))
alloc(dstva);
// ......
}
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
uint64 n, va0, pa0;
if(shouldAlloc(srcva))
alloc(srcva);
// ......
}
至此usertests
的测试也能通过了。