xv6 6.S081 Lab4: lazy

lazy代码在这里。我去,lazy改好多文件啊。不过相比于buddy allocator,自认为lazy还是好多了😓。

写在前面

哈哈,老·写在前面了。在完成lazy的时候,一定要有Page的概念,说白了就是一个内存页,emmm,其实就是一片连续内存吧。另外,一定要理解lazy的思想:该分配Page时才分配,如果没有用到某一Page,我就不分配Page了

同样,可以参考我的开坑博客OS实验xv6 6.S081 开坑,这里给出了一些有用的参考资料,以防大家有不时之需。

实验介绍

官方指导书在这里,加收藏不迷路。
官方指导书给了四个小标题,事实上,我们只需要完成两个任务:

  • 打印页表
  • 实现Lazy Allocation

在开始本实验前,推荐阅读xv6 book的3.1和3.6节,不过没时间读也无所谓,我会对其进行介绍。

开始!

打印页表

在这里插入图片描述
打印内容什么的,我最喜欢了,递归就行了。
在这之前,我们需要知道xv6中的页表结构,它不是我们所学的一级或是二级页表,而是三级页表,这在xv6 book的3.1节中有所讲到,如下图所示:
在这里插入图片描述
我们的任务就是编写vmprint函数来打印页表地址,再打印有效的pte(page table entry)。
官方给出了一些Hints:
Hint 1:可以用risc.h文件末尾定义的宏
Hint 2:可以看vm.c中的freewalk来获得灵感
Hint 3:在kernel/defs.h定义函数原型vmprint,这样就可以在其他地方调用了

代码实现比较简单,如下:

int layer = 1;

void
vmprint(pagetable_t pagetable, int isinit){
  if(isinit){
    printf("page table %p\n", pagetable);
    isinit = 0;
  }
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    /* 如果该PTE是有效映射 */
    if(pte & PTE_V){
      /* 打印PTE信息 */
      for (int i = 0; i < layer; i++)
      {
        /* if(i == layer - 1)
          printf("..");
        else
          printf(".. "); */
        printf(" ..");
      }
      printf("%d: pte %p pa %p\n",i, pte, PTE2PA(pte));
      /* 这个PTE有孩子 */
      if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
        // this PTE points to a lower-level page table.
        uint64 child = PTE2PA(pte);
        layer++;
        vmprint((pagetable_t)child, 0);
        layer--;
      } 
    }
  } 
}

实现Lazy Allocation

在这里插入图片描述
首先,我们要修改sbrk()函数,使之能够支持lazy allocation;其次我们在trap.c中完成lazy allocation

修改sbrk()

根据指导书的Hints:

  • Your first task is to delete page allocation from the sbrk(n) system call implementation, which is the function sys_sbrk() in sysproc.c.
  • so you should delete the call to growproc() (but you still need to increase the process’s size!).

好,找到sysproc.c,删除sys_sbrk中的growproc(),并改为myproc()->sz += n,这一步的操作是将进程理论大小提高,但暂时并不为其分配内存页面,这是为lazy allocation的实现作铺垫。

uint64
sys_sbrk(void)
{
  int addr;
  int n;
  
  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;
  /* printf("old_addr: %d\n", addr); */
  myproc()->sz += n;
  /* printf("%d\n",myproc()->sz); */
  /* if(growproc(n) < 0)
    return -1; */
  return addr;
}

做了这一步操作后,make qemu后,执行一个echo hi,你会看到系统崩溃了,显示Kerneltrap(内核陷入):

init: starting sh
$ echo hiusertrap(): unexpected scause 0x000000000000000f pid=3
            sepc=0x0000000000001258 stval=0x0000000000004008
va=0x0000000000004000 pte=0x0000000000000000
panic: uvmunmap: not mapped

也许其中的sepc会不一样,但其他应该是差不多的。

MIT指导书告诉我们:
usertrap(): unexpected ...是来自trap.c中的,也就是内核碰到了一个它不能处理的异常,这里很明显就是缺页(page fault)异常,因为sbrk并没有真正分配Page页面。注意到stval = 0x4008,什么是stval呢?其实就是stop virtual address,也就表明在虚拟地址为0x4008的地方发生了缺页,系统在这里停了下来。于是,接下来的任务就很明确了,我们要修改trap.c,使之能够处理缺页异常,也就是能够为其加载一个页面,这就是Lazy Allocation的实现了

实现Lazy Allocation

首先我们分析MIT官方的所有Hints:

  1. You can check whether a fault is a page fault by seeing if r_scause() is 13 or 15 in usertrap().
  2. Look at the arguments to the printf() in usertrap() that reports the page fault, in order to see how to find the virtual address that caused the page fault.
  3. Steal code from uvmalloc() in vm.c, which is what sbrk() calls (via growproc()). You’ll need to call kalloc() and mappages().
  4. Use PGROUNDDOWN(va) to round the faulting virtual address down to a page boundary.
  5. uvmunmap() will panic; modify it to not panic if some pages aren’t mapped.
  6. If the kernel crashes, look up sepc in kernel/kernel.asm
  7. Use your print function from above to print the content of a page table.
  8. If you see the error “incomplete type proc”, include “proc.h” (and “spinlock.h”).
  9. If all goes well, your lazy allocation code should result in echo hi working

逐条解读:

  1. r_scause() == 13或者 r_scause() == 15表明缺页异常
  2. stval就是导致缺页的虚拟地址;
  3. 可以查看vm.c中的uvmalloc()是如何分配页面的;
  4. 用宏PGROUNDDOWN(va)让虚拟地址与页面向下对齐,例如:地址va=0x4008,PGROUNDDOWN(va)=0x4000;
  5. uvmunmap()会panic,我们需要修改它,使得当某些页面没有分配时能够成功运行。这是因为由于lazy allocation的机制,有些页面我们并没有分配,但是这些页面并非没有分配的的,它们是虚拟存在的;
  6. 内核崩了,可以看kernel/kernel.asm的信息;
  7. 可以用我们写的mvprint()来debug;
  8. 如果看到错误incomplete type proc,则说明需要导入proc.h头文件;
  9. 如果一切顺利,那么xv6就能够成功运行echo hi了;

这么多提示,那就先从trap.c下手吧,借鉴一下uvmalloc()代码,并修改trap.c中的usertrap()如下:

  /**
   * Page Fault
   */
  else if (r_scause() == 13 || r_scause() == 15)
  {
    /* printf("****************************page fault!****************************\n");
    printf("-------------------------before page table:\n");
    vmprint(p->pagetable, 1);
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
     */
    //char *mem;
    uint64 va;
    
    va = r_stval();
    /* printf("size alloc:%d\n", p->sz);
    printf("va:%d\n", va); */
    if(va >= p->sz)
    {
      /* 虚拟地址大于分配的地址 */
      printf("Virtual Address is greater than sbrk(n) \n");
      p->killed = 1;
    }
    else {
      /* 其他情况,懒加载一页 */
      uint64 va_boundry = PGROUNDDOWN(va);
      mem = kalloc();
      if(mem != 0){
        memset(mem, 0, PGSIZE);
        //printf("%p\n", (uint64)mem);
        //疑问:为何在mappages内对pa进行加减操作? 
        //回答:因为内存映射
        if(mappages(p->pagetable, va_boundry, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
          printf("Cannot allocate so much memory");
          kfree(mem);
          uvmdealloc(p->pagetable, va_boundry, p->sz);
          p->killed = 1;
        }
        //printf("%p\n", (uint64)mem);
        //printf("-------------------------after page table:\n");
        //vmprint(p->pagetable, 1);
        //printf("map:%p\n", walkaddr(p->pagetable, va)); 
      }
      else{
        printf("Mem is 0 \n");
        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;
  }

这里做了一个简单的判断if(va >= p->sz),虚拟地址总不能大于已分配的地址吧。
接下来,需要修改uvmunmap()让它不要恐惧(panic):

void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 size, int do_free)
{
  uint64 a, last;
  pte_t *pte;
  uint64 pa;

  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + size - 1);
  for(;;){
    /**
     * 移除Panic:当找不到相应pte时,我们应该继续查找
     */
    pte = walk(pagetable, a, 0);
    if(pte != 0 && PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    if(pte != 0 && do_free) {
      /**
       * 当且仅当PTE是有效映射时,才进行free操作
       */
      if((*pte & PTE_V) != 0){
        pa = PTE2PA(*pte);
        kfree((void*)pa);
      }
    }
    if(pte != 0)
      *pte = 0;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
}

来对比一下原来的代码,我们删去了if((pte = walk(pagetable, a, 0)) == 0)if((*pte & PTE_V) == 0),并且当且仅当if((*pte & PTE_V) != 0)才进行页的释放。这是因为还未分配上的页面的pte肯定是0,因此不用再对该页进行释放了。

	if((pte = walk(pagetable, a, 0)) == 0)
      panic("uvmunmap: walk");
    if((*pte & PTE_V) == 0){
      printf("va=%p pte=%p\n", a, *pte);
      panic("uvmunmap: not mapped");
    }
    if(pte != 0 && PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    if(do_free){
      pa = PTE2PA(*pte);
      kfree((void*)pa);

完成这里后,运行应该就能够正常运行echo hi了。

完善Lazy Allocation

最后,根据MIT官方指导书的说法,我们还需要进一步完善Lazy Allocator以通过usertests测试。
同样是先分析Hints:

  1. Handle negative sbrk() arguments.
  2. Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().
  3. Handle fork() correctly.
  4. Handle the case in which a process passes a valid address from sbrk() to a system call such as read or write, but the memory for that address has not yet been allocated.
  5. Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.
  6. Handle faults on the invalid page below the stack.

逐条解读:

  1. sys_sbrk需要处理传入的n为负数的情况;
  2. 如果虚拟地址大于sbrk分配的空间,则终止进程,幸运的是,这一点我们已经在前面的代码中实现了;
  3. 正确处理fork()
  4. 正确处理copyout()copyin()以及uvmcopy(),它们在Lazy Allocation的情况下都会不同程度出现panic;
  5. 正确处理内存耗尽的情况:例如kalloc()如果不能分配更多的内存了,就应该终止进程,事实上,这一点我们也已经通过if(mem != 0)解决了;
  6. 正确处理那些在用户栈之下的Page;

事实上,第1、2、5点我们很快就能解决。
其中,实现第1点的代码如下:

uint64
sys_sbrk(void)
{
  int addr;
  int n;
  
  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;
  myproc()->sz += n;
  /**
   * 
   * Handle negtive sbrk
   */
  if(n < 0){
    printf("n:%d\n",n);
    printf("oldsz:%d, newsz:%d\n ", addr, addr + n);
    uvmdealloc(myproc()->pagetable, addr, addr + n);
  }
  return addr;
}

第2点我们已经解决了,第5点我们也已经解决了。

接下来我们先解决第6点。要想解决第6点,那么首先得明白什么是用户栈。xv6 book中讲到,xv6为每个进程都分配了一个虚拟空间,如下图所示。其中,stack即用户栈。
在这里插入图片描述
进程运行时的虚拟地址范围应该在stack到trapframe之间,即在stack和heap内,它不应该向下越过stack进入guard page,于是,只要我们知道stack的起始地址,我们就能在trap.c中做出相应的处理。在exec()中,我们可以找到方案:

  // Allocate two pages at the next page boundary.
  // Use the second as the user stack.
  /**
   * ---------- <-sp
   * user satck
   * ---------- <-stackbase
   * gaurded page
   * ---------- 
   */
  sz = PGROUNDUP(sz);
  if((sz = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0)
    goto bad;
  uvmclear(pagetable, sz-2*PGSIZE);
  sp = sz;
  stackbase = sp - PGSIZE;
 
  
  // Push argument strings, prepare rest of stack in ustack.
  for(argc = 0; argv[argc]; argc++) {
    if(argc >= MAXARG)
      goto bad;
    sp -= strlen(argv[argc]) + 1;
    sp -= sp % 16; // riscv sp must be 16-byte aligned
    if(sp < stackbase)
      goto bad;
    if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
      goto bad;
    ustack[argc] = sp;
  }
  ustack[argc] = 0;

  /**
   * Lazy alloc implementation
   */
  //vmprint(pagetable);
  if (p->pid == 1)
  {
    vmprint(pagetable, 1);
  } 

可以看到,stackbase就是用户栈的首地址(见XV6官方注释与我的注释)。为了记录这个地址,我们需要为proc添加一个字段userstack,然后在exec中为其赋值:

 /**
   * 记录userstack的起始位置
   */
  p->userstack = stackbase;

从而,我们就完成了第6点。接下来,我们解决第3点,因为在fork的时候,由于我们为proc新加了一个字段,因此,fork之后的proc也应该维护这个字段,于是修改fork如下:

int
fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();

  /* printf("fork():\n");
  vmprint(p->pagetable, 1); */
  //printf("fork:basestack %p \n", p->userstack);
  // Allocate process.
  if((np = allocproc()) == 0){
    return -1;
  }

  // Copy user memory from parent to child.
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  /**
   * 应该复制Userstack,维护一下
   */
  np->userstack = p->userstack;

  np->sz = p->sz;
  
  np->parent = p;

  // copy saved user registers.
  *(np->tf) = *(p->tf);

  // Cause fork to return 0 in the child.
  np->tf->a0 = 0;
  
  // increment reference counts on open file descriptors.
  for(i = 0; i < NOFILE; i++)
    if(p->ofile[i])
      np->ofile[i] = filedup(p->ofile[i]);
  np->cwd = idup(p->cwd);

  safestrcpy(np->name, p->name, sizeof(p->name));

  pid = np->pid;

  np->state = RUNNABLE;

  release(&np->lock);

  return pid;
}

好了,接下来的任务便是完成第4点了。事实上,在copyout()copyin()中我们需要修改的地方与trap.c中需要完成的工作类似,只需要懒加载Page即可,由于有他们间有太多相似点,因此我在vm.c中封装了一个lazyalloc函数,如下所示:

int
lazyalloc(pagetable_t pagetable, uint64 va){ 
  if(va >= myproc()->sz){
    printf("lazyalloc: va is bigger than proc sz\n");
    return -1;
  }

  if(va < myproc()->userstack){
    printf("lazyalloc: va is enter guard page!\n");
    return -1;
  }
  
  if(walkaddr(pagetable, va) != 0)
    return 0;
  
  char *mem;
  mem = kalloc();
  if(mem != 0){
    memset(mem, 0, PGSIZE);
    if(mappages(pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
      printf("There is no page mapped");
      kfree(mem);
      uvmdealloc(pagetable, va, myproc()->sz);
      return -1;
    }
  }
  else{
    /**
     * Handle the kalloc is invalid
     */
    printf("Mem is not enough \n");
    return -1;
  }
  return 1;
}

然后实现copyout()copyin()如下:

int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  //copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)
  uint64 n, va0, pa0;

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0){
      /**
       * 为va0分配一页
       */
      //printf("*copyout*\n");
      if(lazyalloc(pagetable, va0)){
        //printf("copyout 1\n");
        pa0 = walkaddr(pagetable, va0);
      }
      else
      {
        //printf("copyout 2\n");
        return -1;
      }
    }
    if(pa0 == 0){
      return -1;
    }
    /**
     * 这样就是在计算offset了
     */
    uint64 offset = (dstva - va0);
    n = PGSIZE - offset;
    if(n > len)
      n = len;
    memmove((void *)(pa0 + offset), src, n);

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }
  return 0;
}

int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  /**
   * 系统调用时,os将用户态转化为内核态
   * 
   */
  uint64 n, va0, pa0;

  while(len > 0){
    va0 = PGROUNDDOWN(srcva);
    pa0 = walkaddr(pagetable, va0);
    
    if(pa0 == 0){
      /**
       * 为va0分配一页
       */
      //printf("copyin\n");
      if(lazyalloc(pagetable, va0)){
        pa0 = walkaddr(pagetable, va0);
      }
      else
      {
        return -1;
      }
    }
    /**
     * 分配了还是出错 
     * */
    if(pa0 == 0){
      return -1;
    }
      //return -1;
    n = PGSIZE - (srcva - va0);
    if(n > len)
      n = len;
    memmove(dst, (void *)(pa0 + (srcva - va0)), n);

    len -= n;
    dst += n;
    srcva = va0 + PGSIZE;
  }
  return 0;
}

而对于uvmcopy(),其与uvmunmap()的处理方式又几乎一样,因为当uvmcopy不应该在复制未找到Page时panic,而应该直接跳过,这是Lazy Allocation机制导致的。代码修改如下:

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){
    /**
     * 父进程中的page table中有些项并未映射,直接跳过
     */
    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, 1);
  return -1;
}

接着,我们重写trap.c,让它能够用上lazyalloc函数:

  /**
   * Page Fault
   */
  else if (r_scause() == 13 || r_scause() == 15)
  {
    uint64 va;
    
    va = r_stval();
    if(va >= p->sz)
    {
      /* 虚拟地址大于分配的地址 */
      printf("Virtual Address is greater than sbrk(n) \n");
      p->killed = 1;
    }
    else if (va < p->userstack)
    {
      printf("Guard page!");
      p->killed = 1;
    }
    else {
      /* 其他情况, */
      uint64 va_boundry = PGROUNDDOWN(va);
      if(lazyalloc(p->pagetable, va_boundry) < 0){
        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;
  }
  if(p->killed)
    exit(-1);
  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

运行usertests,测试成功!
在这里插入图片描述

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值