MIT6.s081_Lab5 lazy: Lazy allocation
MIT6.s081 Lab5:xv6 lazy page allocation
这个实验老师在课上已经讲了一些内容,主要就是实现一个lazy allocation,大概意思就是不主动,你需要了再给你。先告诉进程你有那么大空间了,然后等进程真需要的时候通过trap的方式进行分配,优点是省空间,缺点是费时间。
1. Eliminate allocation from sbrk()
删除growproc()的调用,然后添加myproc()->size += n,这里比较容易,就不过多阐述。
2. Lazy allocation
我在commit中将后面两个放在一起提交了。echo hi老师在课中已经讲过了,这里也不单独拿出来分析了,需要记住几个寄存器,r_scause()记录trap产生的原因,r_stval()记录trap产生页错误的虚拟地址。
这里单独说一下,因为后面需要用到。sp是栈指针,一般先移动sp,再根据sp进行访问。
为了能够先正常的执行echo hi,先增加如下代码,
} else if(r_scause() == 15 || r_scause() == 13){
uint64 fault_addr = r_stval();
lazy_allocation(fault_addr);
lazy_allocation是分配页面的,先通过lazy_allcable判断是否可以分配,主要依据是进程申请内存的大小和发生错误地址的比较,以及访问的是否是guard page。然后就是分配,映射。
这里我卡了半天,原因是
if(lazy_allcable(va) == 0)没有return,这样后面会继续申请内存映射,导致最后释放进程会有部分页面没有取消映射,然后在freewalk中panic("freewalk: leaf")。
void
lazy_allocation(uint64 va)
{
struct proc *p;
p = myproc();
char *mem;
uint64 a;
if(lazy_allcable(va) == 0){
p->killed = 1;
return ;
}
a = PGROUNDDOWN(va);
if((mem = kalloc()) == 0){
p->killed = 1;
return ;
}
memset(mem, 0, PGSIZE);
if(mappages(p->pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_U) != 0){
kfree(mem);
p->killed = 1;
}
}
lazy_allcable是我最后写出来的,判断是否访问的是哨兵页很烦,最后也没想出来一个完美的方法,实验上说,能通过测试就是可以接受的,我也只能接受了。判断访问的地址和进程大小容易va >= size即可。下面重点说一下用户栈访问到哨兵页怎么办。
在 xv6 的实现中,当栈向下增长到哨兵页并触发错误时,
trapframe->sp记录的通常是哨兵页上一个合法页面的最低地址,或者哨兵页内部的某个地址。(这个我还没有自己看到,后面证实了回来补充)(基于这种,其实还有一种写法。因为保存的是合法最低地址,没有实现。保存想法)
根据上述的说法,可以很容易的看出来,sp判断是否在哨兵页的界面中。
如果不是保存的上一个合法最低地址,那就出问题了。
当发生缺页错误时,
trapframe->sp记录的是发生错误时的sp值。模拟这个流程:
- 初始状态: 进程的栈是合法的,
sp位于一个已映射的页面内。- 函数调用: 假设
sp位于页面 A 的底部。函数调用发生,sp减小,进入了页面 B(未映射)。- 访问
0(sp): 此时,程序尝试访问sp指向的地址(在页面 B 中)。由于页面 B 未映射,会触发一个缺页错误。- 进入
usertrap: 处理器捕获缺页错误,保存上下文到trapframe,其中trapframe->sp记录的是发生缺页错误时的sp值,即页面 B 中的某个地址。- 调用
lazy_allcable: 在usertrap中,会调用lazy_allcable来处理这个缺页错误。现在我们来看
lazy_allcable中的判断:
PGROUNDDOWN(myproc()->trapframe->sp):这会得到trapframe->sp所在的页面的起始地址。由于trapframe->sp已经位于页面 B 中,所以这个值就是页面 B 的起始地址。
PGROUNDDOWN(myproc()->trapframe->sp) - PGSIZE:这会得到页面 B 下方一个页面的起始地址(即页面 C 的起始地址)。所以,
va < PGROUNDDOWN(myproc()->trapframe->sp) && va > PGROUNDDOWN(myproc()->trapframe->sp) - PGSIZE实际上是在检查va是否在trapframe->sp所在的页面(页面 B)下方的一个页面(页面 C)这样的话,下面的代码就不对了。
int
lazy_allcable(uint64 va)
{
uint64 size = myproc()->sz;
if(va >= size ||
(va < PGROUNDDOWN(myproc()->trapframe->sp)
&& va > PGROUNDDOWN(myproc()->trapframe->sp) - PGSIZE)) {
return 0;
}
return 1;
}
然后就是uvmunmap和uvmcopy函数的修改,这里解释一下
walk(pagetable, a, 0) 的作用:
walk函数的目的是在给定的页表pagetable中查找虚拟地址a对应的页表项 (PTE)。- 第三个参数
0表示如果中间的页目录项 (PDE) 或页表项 (PTE) 不存在,不要创建它们。它只进行查找。- 如果
walk成功找到a对应的 PTE,它会返回该 PTE 的地址。- 如果
walk无法找到a对应的 PTE(例如,某个中间级别的页目录项不存在,或者最终的 PTE 不存在),它会返回0(NULL)。
如果返回 0 意味着 a 对应的虚拟地址没有被映射。可能因为:
- 该虚拟地址从未被分配过(例如,访问了进程地址空间外的地址)。
- 该虚拟地址所在的页表层级尚未建立。
- 该虚拟地址对应的页面已被取消映射。
继续就行
第二个修改是为了预防即使检查到了PTE,即返回了非0 的pte地址,但是可能无效PTE_V 为 0,这样的也无需进行处理,否则do_free成立时,释放物理空间转换可能会出现垃圾数据,然后panic。
fork的copy也是这样,不存在的,以及无效的都不需要复制。
//map
if((pte = walk(pagetable, a, 0)) == 0)
continue;
if((*pte & PTE_V) == 0)
continue;
//copy
if((pte = walk(old, i, 0)) == 0)
continue;
if((*pte & PTE_V) == 0)
continue;
最后,就是sbrk之后对这个地址进行写入或者读取了,为什么需要改呢,因为在这里出现页面错误无法进入trap,需要直接进行处理,因此需要改变copyin和copyout。用户空间出现缺页进入trap分配页面,这里read和write系统调用是不会进入处理的,因此在根据虚拟地址寻找物理地址没找到,判断虚拟地址是否合法,也就是是否在范围内,如果在的话,直接现场分配,不在再原路返回。
copyout
if(pa0 == 0) {
if (lazy_allcable(dstva) == 0) {
return -1;
}
lazy_allocation(dstva);
pa0 = walkaddr(pagetable, va0);
}
copyin
if(pa0 == 0) {
if (lazy_allcable(srcva) == 0) {
return -1;
}
lazy_allocation(srcva);
pa0 = walkaddr(pagetable, va0);
}
我去找guard page相关问题的时候,发现有一位作者的写法不同,我本来向参考优化一下,发现不是改一下就行,就放弃了。
至此Lab5算是完成,return和guard page花了很多时间。

浙公网安备 33010602011771号