目录
Eliminate allocation from sbrk()
这个实验在2021的版本中没有,需要去2020版本中找,具体实验如下:
https://pdos.csail.mit.edu/6.828/2020/labs/lazy.html
lazy page allocation主要是对堆区空间采取“延迟分配”的策略,即当采取sbrk申请堆区内存时不分配页面,仅当实际使用时发生也错误才实际分配内存。
Eliminate allocation from sbrk()
在sbrk中删除实际分配页面,只给进程的内存大小修改。修改代码如下:
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
myproc()->sz += n;
// if(growproc(n) < 0)
// return -1;
return addr;
}
Lazy allocation
上个步骤只在用户进程申请分配时只增加了用户进程的空间当并未实际分配,接下来就需要分配了,首先在trap.c中,根据页错误(错误码是13或15),拿到页错误失败的虚拟地址,之后给该地址分配物理页面。将分配页面相关代码写成函数如下:
//懒分配相关函数
int lazy_alloc(uint64 va)
{
// printf("页错误导致惰性分配!");
uint64 page_addr = PGROUNDDOWN(va); //得到要分配页的虚拟地址的对齐后的结果
char *mem = kalloc();
struct proc* p = myproc();
if(mem == 0){
return -1;
}
memset(mem, 0, sizeof(PGSIZE));
if(mappages(p->pagetable, page_addr, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
return -1;
}
return 0;
}
发现这样之后还会报错,panic报的是uvmunmap:not mapped的错误,该函数的功能便是对pagetable页表下,虚拟地址va及其之后的npages页,若do_free=1则进行销毁,否则只是单纯赋0。发现在函数中对应的是无效的PTE即未分配,具体应该是进程销毁时,需要销毁所有的页表,这时发现进程中原本应该有的页表里面是未分配的。
具体原因是懒分配,我们只是根据使用情况来分配页,那些尚未使用的页并未分配物理页,在销毁时便会报错,但这是正常的应该出现的情况,所以我们continue即可。
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;
}
}
Lazytests and Usertests
发现之前写的代码还是很粗糙的(漏洞百出),这个实验就是要求我们修补这些漏洞。
第一个问题提示修补sbrk参数为负数的问题。
为负数则直接释放内存即可。
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
if(n < 0)
{
if(n + myproc()->sz < 0)
return -1;
else
{
if(growproc(n) < 0)
return -1;
}
} else {
myproc()->sz += n;
}
// if(growproc(n) < 0)
// return -1;
return addr;
}
第二个问题需要我们正确判断页错误是否的确由懒分配的页面导致的。需要正确判断va的范围。
首先我们回顾用户进程的地址空间如下:

添加判断是否是懒分配页面的判断如下:
//判断页面是否是合法的懒分配页面
int is_lazy_addr(uint64 va)
{
struct proc* p = myproc();
if(va > MAXVA) return 0; //超出va上限
//判断是否属于堆区内存
if(va < PGROUNDDOWN(p->trapframe->sp)+PGSIZE || va >= p->sz)
return 0;
//判断是否未分配pte
pte_t * pte = walk(p->pagetable, va, 0);
if(pte && (*pte & PTE_V))
return 0;
return 1;
}
第三个提示是要正确处理fork()中的父子内存副本,观察fork()的代码后,发现其中有uvmcopy函数,作用是把父亲的内存copy给儿子,该函数在查找父亲的pa时可能会由于父亲的懒分配页面尚未分配导致找不到pte(walk出错)或者找到的pte无效,导致panic,所以这里直接continue。这样子进程出现页错误也会分配页面。修改如下:
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;
}
第四个要求我们处理例如read(或write)的系统调用读入或者输出用户未分配的虚拟地址时出现的问题。考虑若在用户态,进程主动使用虚拟地址,那么会进入traps,然后在缺页错误下妥善处理,但如果未分配的va作为参数传入系统调用,则在内核态引发的页错误我们并没有处理。观察read和write的系统调用,发现他们采用copyin()和copyout()负责将用户进程的va复制进和复制出,发现其中主要由walkaddr负责将va转换成pa,发现其主要通过调用walk来查找实际地址,由于懒分配页帧都没有,所以在walk中一定会返回0,进而在这里返回0.所以我们只需提前判断当前va是否是懒分配地址,是的话提前分配就没问题了。
// Look up a virtual address, return the physical address,
// or 0 if not mapped.
// Can only be used to look up user pages.
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
pte_t *pte;
uint64 pa;
if(va >= MAXVA)
return 0;
if(is_lazy_addr(va))
{
lazy_alloc(va);
}
pte = walk(pagetable, va, 0);
if(pte == 0)
return 0;
if((*pte & PTE_V) == 0)
return 0;
if((*pte & PTE_U) == 0)
return 0;
pa = PTE2PA(*pte);
return pa;
}
第五个是kalloc分配页面不足是,直接杀死进程。这个好写,由于我们已经写了lazy_alloc()函数代表分别过程,如果返回-1说明内存不足,直接杀进程即可。(话说在上一步是不是也应该这样写。)
else if ((r_scause() == 13 || r_scause() == 15) && is_lazy_addr(r_stval())) //这里加上是懒分配的页面
{
if(!lazy_alloc(r_stval()))
{
p->killed = 1;
}
}
最后一个要求处理用户栈下方的guard page的问题,考虑到我们当初判断懒分配页面是否属于时,就严格要求页面在堆区范围内,所以没有此问题。
总结:lazy allocation本质是在堆区申请空间时,不即时分配物理页面,而是仅修改用户进程堆区指针(空间),当用户进程实际使用该堆区空间时,由于未配置页表,则一定会进入traps且报错为页错误,这时才修改页表并且分配物理空间。
协同解决问题:1.sbrk传入参数为负数(缩小堆空间),即时释放内存即可。2.在traps中准确判断va为堆区lazy allocation的页,不影响正常的页错误流程。3.同时当进程最后销毁时面对未分配物理页的内存直接释放即可,取消报错。4.fork()时,面对父进程的未分配页,子进程采取同同样策略,也不分配。5.由于实际分配物理页仅在traps(由用户态到内核态)中,所以在内核态发生页错误(采用系统调用如read或write读写未分配物理页的va)时同时需要即时分配物理页。
最后完结撒花!!!

...


3440

被折叠的 条评论
为什么被折叠?



