Lab5 惰性分配

预备知识

惰性分配

当一个用户进程申请内存时,OS并不会立即分配所需要的全部物理内存,而是仅仅记录已分配的用户地址并在用户页表中将这些PTE标记为无效。当进程首次尝试使用这些内存页时,CPU会触发页面错误异常,内核在此时才会进行物理内存分配。

页面故障异常

RISC-V中有三种页面故障异常:

  • 加载页面错误(错误号13):当加载指令访问的虚拟地址找不到对应的物理地址;
  • 存储页面错误(错误号15):当存储指令访问的虚拟地址找不到对应的物理地址;
  • 指令页面错误(错误号12):当指令获取的虚拟地址找不到对应的物理地址。

对于这些异常的信息,SCAUSE寄存器中的值指示页面错误的类型,即具体是哪一种,STVAL寄存器保存了不能转换的虚拟地址。为了在完成页面分配后重新执行触发异常的指令,还需要其地址。页面故障异常也是trap的一部分,因此这个指令地址会保存在SEPC寄存器中,同时在usertrap()中保存在进程的trapframe->epc中。

Eliminate allocation from sbrk()

目前Xv6会立即进行内存分配,因此第一个任务是删除sbrk(n)系统调用。代码位于sysproc.c中的sys_sbrk()函数。sbrk(n)系统调用将进程的内存大小增加n字节,然后返回新分配区域的起始地址。新的 sbrk(n) 只需将进程的大小增加 n,而不分配内存,也即删除 growproc() 的调用,而只在myproc()->sz记录。

//kernel/sysproc.c
uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;

  //注释掉growproc()
  //if(growproc(n) < 0)
  //  return -1;

   myproc()->sz += n;

  return addr;
}

运行一下echo,可以看到SCAUESE中的错误号是15

$ echo hi
usertrap(): unexpected scause 0x000000000000000f pid=3
            sepc=0x00000000000012ac stval=0x0000000000004008
panic: uvmunmap: not mapped

Lazy allocation

修改usertrap()函数,增加对错误号为13或15的处理。也就是为STVAL中的虚拟地址分配物理内存,实现惰性分配。

//kernel/trap.c:usertrap()
...
else if((which_dev = devintr()) != 0){
    // ok
  } else if(r_scause() == 13 || r_scause() == 15){
    
    struct proc* p = myproc();
    uint64 va = r_stval();//获取发生异常的虚拟地址
    char* pa = kalloc();  //等待分配的物理地址

    if( pa==0 ){
      printf("超出内存!\n");
      p->killed = 1;
    } else{
      memset(pa, 0 ,PGSIZE);
      if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
        printf("分配内存过程失败!\n");
        kfree(pa);
        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;
  }

除此之外,题目还说要修改uvmunmap()以避免在某些页面未映射时引发 panic。这是因为惰性分配最开始只修改了sz,比如说在利用sz/PGSIZE计算npages时就会得到很多未建立映射的虚拟地址,这时去解除映射就会触发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)
    //  panic("uvmunmap: walk");
	continue;
    if((*pte & PTE_V) == 0)
    //  panic("uvmunmap: not mapped");
	continue;
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    *pte = 0;
  }
}

现在执行echo,即可正常处理

Lazytests and Usertests

完善惰性分配代码,使其能够处理各种情况。

  1. 处理sbrk()的负数参数
//kernel/sysproc.c
uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;

  //if(growproc(n) < 0)
  //  return -1;

  if( n > 0){
    myproc()->sz += n;
  } else if( mycpu() + n > 0){
    myproc()->sz = uvmdealloc( myproc()->pagetable, myproc()->sz, myproc()->sz + n);
  } else{
    printf("缩减后进程内存小于0!\n");
    return -1;
  }
   
  return addr;
}
  1. 确保虚拟地址是有效的:处理进程在高于sbrk()分配的虚拟地址发生故障;处理用户栈下方的保护页面故障
    if(PGROUNDUP(p->trapframe->sp)-1 < va && va < p->sz){
      char* pa = kalloc();  //等待分配的物理地址
      if( pa == 0 ){
        printf("超出内存!intrap\n");
        p->killed = 1;
      } else {
        memset(pa, 0 ,PGSIZE);
          if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
            printf("分配内存过程失败!\n");
            kfree(pa);
            p->killed = 1;
          }
      }
    } else
      p->killed = 1;
  1. fork()中的父子内存复制。fork()会调用uvmcopy()复制父进程的页表,而父进程的页表也有可能是未实际分配的。同样也是注释掉里面的panic
//kernel/vm.c uvmcopy()
    if((pte = walk(old, i, 0)) == 0)
      //panic("uvmcopy: pte should exist");
      continue;;
    if((*pte & PTE_V) == 0)
      //panic("uvmcopy: page not present");
      continue;
  1. 进程从sbrk()传递一个有效地址到系统调用(如read或write),但该地址对应的内存尚未分配。
    这应该是在说现在的sbrk()虽然没有实际分配内存了,但是系统已经记录了内存的变化(sz),因此也有可能使用到未映射的虚拟地址。
    根据write的调用流程,write系统调用后会通过copyin来将用户空间的虚拟地址通过转化为物理地址从而传递到内核空间。这时如果虚拟地址未分配内存就会导致调用失败,因此我们在这里面找到报错的地址改成分配内存即可,也就是walkaddr()函数。
//kernel/vm.c: walkaddr
...
 pte = walk(pagetable, va, 0);
//新增
  struct proc* p = myproc();
  if(pte == 0 || (*pte & PTE_V) == 0){
    if(PGROUNDUP(p->trapframe->sp)-1 < va && va < p->sz){
      char* mem = kalloc();
      if( mem == 0){
        p->killed = 1;
        return 0;
      } else{
        memset(mem, 0, PGSIZE);
        if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
          printf("分配内存过程失败!\n");
          kfree(mem);
          p->killed = 1;
          return 0;
        }
        return (uint64)mem;
      }
    }
  }
  // if(pte == 0)
  //   return 0;
  // if((*pte & PTE_V) == 0)
  //   return 0;

此时执行lazytests 和uertests已经能够全部通过。

$ lazytests
lazytests starting
running test lazy alloc
test lazy alloc: OK
running test lazy unmap
test lazy unmap: OK
running test out of memory
test out of memory: OK
ALL TESTS PASSED
$ usertests
...
test iref: OK
test forktest: OK
test bigdir: OK
超出内存!
ALL TESTS PASSED
posted @ 2025-07-17 17:39  名字好难想zzz  阅读(20)  评论(0)    收藏  举报