【MIT6.S081】Lab5: xv6 lazy page allocation(详细解答版)

本文详细描述了如何在XV6操作系统中实现懒惰式内存分配,包括修改sys_sbrk函数、处理pagefault、uvmunmap函数的调整以及用户态和系统调用中的内存管理,以解决内存增加时的页面错误问题。
摘要由CSDN通过智能技术生成

实验内容网址:https://xv6.dgs.zone/labs/requirements/lab5.html

本实验的代码分支:https://gitee.com/dragonlalala/xv6-labs-2020/tree/lazy3/

Eliminate allocation from sbrk()

关键点:p->sz的含义

思路:

sbrk(n)系统调用将进程的内存大小增加n个字节,然后返回新分配区域的开始部分(即旧的大小)。新的sbrk(n)应该只将进程的大小(myproc()->sz)增加n,然后返回旧的大小。因此直接修改sys_sbrk()函数,注释掉对growproc()函数的调用,直接修改p->sz。

uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;
  // lab5-1
  myproc()->sz += n;    // increase size but not allocate memory
//  if(growproc(n) < 0)
//    return -1;
  return addr;
}

进行 echo hi 测试,实验结果如图所示

Lazy allocation

这题才是本实验的主要部分,上面的实验在调用sbrk增加或者减少内存时并没有对物理内存进行操作,这会引起页面错误,本题就是为了解决内存增加时出现的页面错误。

思路&步骤

对于这部分的思路和步骤可以跟着提示进行展开。

  1. kernel/trap.c文件的usertrap()函数中对r_scause()为13或者15的情况进行处理,这两种情况是出现了页面错误。通过r_stval()函数获得出现页面错误的虚拟地址。参考growproc()函数中对kalloc()mappages()的调用,为出现报错的虚拟地址申请1页物理地址(内存),是的,1页。物理内存申请成功后将虚拟地址映射到物理地址上。这段代码中有一点要注意的是如果出现物理内存申请不成功 或者映射失败 的情况,需要将p->killed置为1,标记当前进程后就需要立即去exit(-1)了,所以下面的代码我添加了goto来实现这个效果。如果没加goto,则会导致物理内存申请不成功后反而去初始化物理内存,导致其他错误。
...
else if((which_dev = devintr()) != 0){
    // ok
  } else if(r_scause() == 13 || r_scause() == 15){
    
    // 出现报错的虚拟地址
      uint64 va = r_stval();
       // 虚拟地址高于sbrk分配的虚拟地址则杀死进程
      // 申请1页物理内存
      char* pa = kalloc();
      if(pa == 0){ // 申请不成功
        printf("alloc physical memory failed");
        p->killed = 1;
        goto end;
      }
      // 初始化物理内存
      memset(pa, 0, PGSIZE);
      // 映射, 将出错的虚拟地址向下舍入到页面边界,因为va所在的这一页还没有对应的物理内存
      if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_X|PTE_R|PTE_U)){
        // 映射失败,释放物理内存
        kfree(pa);
        p->killed = 1;
        goto end;
      }
    
    

  }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;
  }
  
  end:
  if(p->killed){
    exit(-1);
  }
    
  1. 在 xv6 的原本实现中, 由于是 Eager Allocation, 所以不存在分配的虚拟内存不对应物理内存的情况, 因此在 uvmunmap() 函数中取消映射时对于虚拟地址对应的 PTE 若是无效的, 便会引发 panic. 而此处改为 Lazy allocation 后, 对于用户进程的虚拟空间, 便会出现有的虚拟内存并不对应实际的物理内存的情况, 即页表中的 PTE 是无效的. 因此此时便不能引发 panic, 而是选择跳过该 PTE.
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");
    if((*pte & PTE_V) == 0) {
      continue;     // lab5-2
//      panic("uvmunmap: not mapped");  // lab5-2
    }
    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() 负数参数的情况
  • 引发 page fault 的地址低于用户栈和高于分配地址的情况杀死进程
  • 处理fork() 时的父进程向子进程拷贝内存的情况
  • 处理read()/write() 使用未分配物理内存的情况
  • kalloc() 失败时杀死进程(上一部分实验已处理).

步骤&代码:

  1. 处理 kernel/sysproc.csys_sbrk() 参数为负数的情况.

处理的方法可以直接参考原本调用的 growproc() 函数, 因为在 Lazy allocation 的情况下减少内存同样是将多余的内存进行释放, 此处便调用了 uvmdealloc() 函数.在sys_sbrk()函数中进行如下修改。

这里可以做一个改进,就是内存增加或者减少时可以判断一下会不会超出界限。

对于n>0的情况,可以做addr+n>addr?的判断,因为addr+n可能超出int的范围导致变成负数。

对于n<0的情况,可以做 n<0 && addr ``+`` n ``>=`` ``PGROUNDUP``(``p``->``trapframe``->``sp``)的判断。这里要注意sp是用户栈的栈顶指针,栈顶指针的初始地址是PGSIZE,然后向下生长,不超过一个PGSIZE。所以PGROUNDUP``(``p``->``trapframe``->``sp)就可以知道栈底的位置。

对于n==0的情况,要立即return -1;

uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  struct proc* p = myproc();
  // 保存原本旧的sz
  addr = p->sz;
  int sz = p->sz;
  if(n > 0){
    // 对于内存增加情况,直接增大p->sz
    p->sz += n;
  }else if(sz + n > 0){
    // 对于内存减少的情况,需要解映射,同时修改解映射后的p->sz
    sz = uvmdealloc(p->pagetable, sz, sz + n);
    p->sz = sz;
  }else{
    return -1;
  }
  // 返回旧地址
  return addr;
}
  1. 进一步修改 kernel/vm.c 中的 uvmunmap() 函数, 将 pte==0 的情况由引发 panic 改为 continue 跳过.

之所以这样操作, 原因在于 uvmunmap() 获取 pte 调用的 walk() 函数.walk() 用于根据虚拟地址 va 得到其相应页表中对应的 PTE. 而 xv6 的页表结构是有三级页目录, 因此可能在 L2 或 L1 的 PTE 可能就是无效的了, 此时便未分配低级的页目录, 自然也就未分配 va 对应的物理内存, 此时会根据 alloc 参数选择是否构建新的页目录, 但在 uvmunmap() 调用的 walk()的 alloc 参数为 0, 因为取消映射页表时不应该再新建页目录了. 此时 walk() 便返回 0.

由于 Lazy allocation, 自然很可能出现虚拟地址对应的 L2 或 L1 的 PTE 就是未分配(无效)的情况, 所以此时不应该引发 panic, 同样应该跳过处理.

简而言之, 对于 pte==0 的情况, 一般是 sbrk() 申请了较大内存, L2 或 L1 中的 PTE 就未分配, 致使 L0 页目录就不存在, 虚拟地址对应的 PTE 也就不存在; 而对于之前提到的 (*pte&PTE_V)==0 的情况, 一般是sbrk() 申请的内存不是特别大, 当前分配的 L0 页目录还有效, 但虚拟地址对应 L0 中的 PTE 无效. 这两种情况都是 Lazy Allocation 未实际分配内存所产生的情况, 因此在取消映射时都应该跳过而非 panic.

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){
      // panic("uvmunmap: not mapped");
      continue;
    }
      //continue;
      
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    *pte = 0;
  }
}
  1. 处理 kernel/proc.cfork() 函数中父进程向子进程拷贝时的 Lazy allocation 情况.

容易分析得到, fork() 是通过uvmcopy() 来进行父进程页表即用户空间向子进程拷贝的. 而对于 uvmcopy() 的处理和uvmunmap() 是一致的, 只需要将 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;
}
  1. 处理 page fault 的虚拟地址超过 p->sz 或低于用户栈的情况

超过p->sz即报错的虚拟地址大于p->sz

低于p->sz,笔者认为,报错的虚拟地址不应该低于栈底的位置(栈是向下生长的,不超过1页)。

还有要注意的是如果过程中有不成立的情况要立即将killed标志位置1并及时退出。

void
usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();
  
  // save user program counter.
  p->trapframe->epc = r_sepc();
  
  if(r_scause() == 8){
    // system call
    // printf("here r_scause=8\n");
    // printf("p-killed:%d\n",p->killed);
    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();

    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else if(r_scause() == 13 || r_scause() == 15){
    
    // 出现报错的虚拟地址
    uint64 va = r_stval();
    // 虚拟地址高于sbrk分配的虚拟地址则杀死进程
    if(va < PGROUNDUP(p->trapframe->sp) || va > p->sz){
      p->killed = 1;
      goto end;

    }else{
      // 申请1页物理内存
      char* pa = kalloc();
      if(pa == 0){ // 申请不成功
        printf("alloc physical memory failed");
        p->killed = 1;
        goto end;
      }
      // 初始化物理内存
      memset(pa, 0, PGSIZE);
      // 映射, 将出错的虚拟地址向下舍入到页面边界,因为va所在的这一页还没有对应的物理内存
      if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_X|PTE_R|PTE_U)){
        // 映射失败,释放物理内存
        kfree(pa);
        p->killed = 1;
        goto end;
      }
    }
    

  }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;
  }

end:
  if(p->killed){
    exit(-1);
  }
    

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}
  1. 处理进程从sbrk()向系统调用(如readwrite)传递有效地址,但尚未分配该地址的内存。可以从argaddr()函数入手。判断传入进来的用户的虚拟地址是否存在对应的pte,如果没有则为他申请物理内存并进行映射。

在这里walkaddr()如果虚拟地址有对应的pte,则返回具体的pte,如果没有则返回0,一开始我是判断walkaddr函数的返回值非零,则return -1。但是这样会导致系统崩溃,也很难查找问题,即使加入了backtrace也找不到问题的所在。如果返回值非零说明这个虚拟地址对应的pte是存在的,则正常返回0就行了。

int
argaddr(int n, uint64 *ip)
{
  *ip = argraw(n);
  // 获取当前的进程
  struct proc *p = myproc();
  uint64 va = *ip;
  // 检查传递进来的虚拟地址是否有效
  if(walkaddr(p->pagetable, va) == 0){
  
    // 检查虚拟地址是否高于sbrk分配的虚拟地址或者低于栈顶指针
    if(va < PGROUNDUP(p->trapframe->sp) || va >= p->sz){
      return -1;
    }
    // 申请1页物理内存
    char* pa = kalloc();
    if(pa == 0){ // 申请不成功
      return -1;
    }
    // 初始化物理内存
    memset(pa, 0, PGSIZE);
    // 映射, 将虚拟地址向下舍入到页面边界,因为va所在的这一页还没有对应的物理内存
    if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_X|PTE_R|PTE_U)){
      // 映射失败,释放物理内存
      kfree(pa);
      return -1;
    }
  }
  return 0;
}

参考:

[MIT 6.S081] Lab 5: xv6 lazy page allocation-CSDN博客

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值