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,这样后面会继续申请内存映射,导致最后释放进程会有部分页面没有取消映射,然后在freewalkpanic("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_allcableusertrap 中,会调用 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;
}

然后就是uvmunmapuvmcopy函数的修改,这里解释一下

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_V0,这样的也无需进行处理,否则do_free成立时,释放物理空间转换可能会出现垃圾数据,然后panic

forkcopy也是这样,不存在的,以及无效的都不需要复制。

//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,需要直接进行处理,因此需要改变copyincopyout。用户空间出现缺页进入trap分配页面,这里readwrite系统调用是不会进入处理的,因此在根据虚拟地址寻找物理地址没找到,判断虚拟地址是否合法,也就是是否在范围内,如果在的话,直接现场分配,不在再原路返回。

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算是完成,returnguard page花了很多时间。

posted @ 2025-07-18 00:27  BuerH  阅读(13)  评论(0)    收藏  举报