Loading

6.S081 lab5 lazy

#lazy page allocation

  • 首先修改sbrk系统调用,找到sys_sbrk函数。可以发现此函数增加了用户进程的堆空间并调用growproc分配物理页。由于我们需要实现lazy策略,只让p->sz增加就行了,并不直接分配物理页,而是在缺页中断分配真正的物理页。这里需要注意sbrk的参数可以是一个负数(也就是回收一部分堆空间),如果这种情况出现,可以直接调用growproc函数来处理。代码如下:

    growproc(int n)
    {
      uint sz;
      struct proc *p = myproc();
    
      sz = p->sz;
      if(n > 0){
        if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
          return -1;
        }
      } else if(n < 0){
        sz = uvmdealloc(p->pagetable, sz, sz + n);
      }
      p->sz = sz;
      return 0;
    }
    
  • usertrap中增加代码以处理缺页中断。r_stval读取了导致缺页中断发生的虚拟地址值。加入lazy_alloc函数实现懒分配,若该函数返回-1,则懒分配失败,应该杀掉进程。

    void
    usertrap(void)
    {
      ...
      if(r_scause() == 8){
        // system call
    
        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(r_scause() == 13 || r_scause() == 15){
           uint64 va = r_stval();
           if (lazy_alloc(p, va) != 0){
               p->killed = -1;
           }
       
      } else if((which_dev = devintr()) != 0){
        // ok
      } 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;
      }
    
     ...
    }
    

    实现lazy_alloc函数:

    int lazy_alloc(struct proc *p, uint64 va)
    {
        uint64 new_sz = p->sz;
        if (va >= new_sz || va < PGROUNDDOWN(p->trapframe->sp)){
            return -1;
        }
        /* printf("page fault: %p\n", va); */
        uint64 pa = (uint64)kalloc();
        if (pa == 0){
            return -1;
        }
        memset((void*)pa, 0, PGSIZE);
        va = PGROUNDDOWN(va);
        if (mappages(p->pagetable, va, PGSIZE, pa, PTE_W | PTE_U | PTE_R) != 0){
           kfree((void*)pa);
           p->killed = 1;
        }
    
        return 0;
    }
    

    函数的4~6行检查了虚拟地址的合法性,va不能超过堆空间的大小,也不能超过栈顶。分配一个物理页后,把映射关系添加到进程的页表中。

  • 当进程回收时,会调用uvmunmap,更改uvmunmap函数。该函数的作用是清理页表中的映射关系。由于我们实现了懒分配(并没有真正分配物理页),所以页表可能中找不到pte ,或者找到的pte并不是有效的,此时不应该panic,因为这两种情况是可能出现的。

    void
    uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
    {
      ...
      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: uvmunmap: not mapped"); */
          continue;
        ...
      }
    }
    
  • fork系统调用会调用uvmcopy函数拷贝父进程的内存到子进程中,需要做与uvmunmap类似的处理。

    int
    uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
    {
      ...
      for(i = 0; i < sz; i += PGSIZE){
        if((pte = walk(old, i, 0)) == 0)
          /* panic("uvmcopy: pte should exist"); */
            continue;
        if((*pte & PTE_V) == 0)
          /* panic("uvmcopy: page not present"); */
            continue;
       ...
      }
      return 0;
    
     err:
      uvmunmap(new, 0, i / PGSIZE, 1);
      return -1;
    }
    
  • write等系统调用可能会使用未分配物理页的虚拟地址。查看filewrite函数,当文件是管道时,函数会调用pipewrite,在pipewrite中,会调用copyin把用户空间的内存拷贝到内核中。copyin中调用了walkaddr从用户页表中得到物理地址,此时物理页可能没有分配。修改walkaddr以分配物理页。代码如下:

    uint64
    walkaddr(pagetable_t pagetable, uint64 va)
    {
      ...
      pte = walk(pagetable, va, 0);
      if(pte == 0 || (*pte & PTE_V) == 0){
        if (lazy_alloc(myproc(), va) < 0)
            return 0;
      }
      ...
      return pa;
    }
    
posted @ 2022-01-18 11:05  Kyoz1  阅读(72)  评论(0)    收藏  举报
7