10. Lab: mmap

最后一个 lab 了... https://pdos.csail.mit.edu/6.S081/2021/labs/mmap.html

1. 要求

You should implement enough mmap and munmap functionality to make the mmaptest test program work. If mmaptest doesn't use a mmap feature, you don't need to implement that feature.

简单来说,就是实现一个阉割版的 mmap 和 munmap,因为完全体的 mmap 功能较多,这里只实现 memory-mapping a file ,文件映射。

  1. mmap

mmap 的接口形式为
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr 为文件要映射到的虚拟地址,该 lab 中默认为 0,length 为映射长度,prot 为权限,如 可读/可写,flags 为映射方式,这里只有 2 种情况:

  • MAP_SHARED ,执行 munmap 的时候,将改动写回到文件当中
  • MAP_PRIVATED,执行 munmap 的时候,不写回改动到文件当中

fd 为目标文件描述符,offset 为文件偏移量。

  1. munmap

munmap 的接口形式为:
int munmap(addr, length)
addr 为解除映射的起始位置,length 为解除映射的长度。

2. 分析

2.1 mmap

mmap 的实现主要注意如下几点:

  • 采用 lazy allocation 的方式,首次执行时,只对进程的虚拟内存进行分配(参考 sbrk ),但是不分配物理内存,因为假如文件过大时,效率会比较低下。
  • 由于我们是 lazy alloction,所以需要对文件引用计数 +1 ,防止外部 close 文件,导致无法读取内容。
  • 由于我们采用 lazy allocation 的方式,因此真正读取文件的时机,应该迁移到触发缺页异常的时候,当 page fault 时,分配物理页,建立映射,并且将文件内容读取到指定页。
    • 此处还可以进行一项优化,如当权限为只读的情况下,首先文件的内容已经在内核中有一份了(参考 inode 的 buffer),因此可以将用户进程虚拟页设置为只读,并且映射到该 buffer,需要注意的是,buffer 需要是 page align 的,其次还要用到物理页的引用技术,这里参考 lab-cow
  • 注意权限,比如 fd 打开的文件结构,如果不可写,但是 mmapprot 设置可写,则是不可行的。
  • 根据 hints,我们需要针对 struct proc 建立一个 VMA(virtual memory area) 结构数组,用于管理我们映射了哪些区域,记录权限。
  • fork 时,需要注意,将 VMA 结构拷贝过去
    • 此处可以进行一项优化,按上述的默认机制,我们触发缺页时会分配内存,并将文件内容写入。由此内存中已经有一份数据了,因此可以参考 copy-on-write 技术,子进程映射对应物理页,设置不可写,当触发写异常时,再分配新的物理页进行页映射。

2.2 munmap

munmap 的实现主要需要注意如下几点:

  • 由于页面解除映射可以只解除部分区域,因此我们在 VMA 结构中,需要记录哪些页已经解除映射了
  • 当全部区域都解除映射时,减少对应文件引用计数,回收 VMA 。

2.3 VMA

根据上述分析,VMA 结构定义如下:

struct vma {
  uint64 addr;        // map start addr
  uint64 length;          // map area size
  uint flags;          // MAP_SHARED or MAP_PRIVATE
  uint prot;          // permisson rwx
  struct file* file;   //refer file
  uint8 valid;        // vma is valid
  uint64 dirtyflag; 
};

// Per-process state
struct proc {
  // some code ...
  struct vma vma[16];         // virtual memory area by mmap
};

获取 vma 接口:

struct vma* getvma(uint64 va)
{
  struct proc* proc = myproc();
  struct vma* vma = 0;
  for (int i = 0; i < 16; i++) {
    if (proc->vma[i].valid == 0)
      continue;

    uint64 addr = proc->vma[i].addr;
    uint64 length = proc->vma[i].length;
    if (addr <= va && (va < (addr + length))) {
      vma = &proc->vma[i];
      break;
    }
  }

  return vma;
}

3. 实现

3.1 mmap

主要执行初始化 VMA 的工作。对于映射的区域,以页为单位,用 bitmap 的思路管理,方便 munmap 时确定什么时候执行释放。

/*
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
*/
uint64
sys_mmap(void)
{
  uint64 addr; // user pointer to array of two integers
  int length;
  int prot;
  int flags;
  struct file* file;
  int offset;

  if(argaddr(0, &addr) < 0)
    return -1;
  if(argint(1, &length) < 0)
    return -1;
  if(argint(2, &prot) < 0)
    return -1;
  if(argint(3, &flags) < 0)
    return -1;
  if(argfd(4, 0, &file) < 0)
    return -1;
  if(argint(5, &offset) < 0)
    return -1;

  if(!file->readable && (prot & PROT_READ))
    return -1;
  if(!file->writable && (prot & PROT_WRITE) && !(flags & MAP_PRIVATE))
    return -1;

  // offset and addr can asume 0
  struct proc* proc = myproc();
  // step 1 : alloc vma
  struct vma * vma = 0;
  for (int i = 0; i < 16; i++) {
    if (proc->vma[i].addr == 0) {
      vma = &proc->vma[i];
      break;
    }
  }

  if (vma == 0)
    return -1;

  vma->length = length;
  vma->flags = flags;
  vma->prot = prot;
  vma->file = file;
  vma->valid = 1;

  // step 2 : add heap size
  uint64 retaddr = PGROUNDUP(proc->sz);
  proc->sz = retaddr + length;
  vma->addr = retaddr;
  
  // step 3 : add file ref cnt
  filedup(vma->file);

  // step 4 : record which page been mapped
  int pgcnt = length / PGSIZE;
  int mask = 1;
  for (int i = 0; i < pgcnt; i++)
  {
    vma->dirtyflag |= (mask << i);
  }

  return retaddr;
}

缺页处理

void
usertrap(void)
{
  // some code ...
    else if (r_scause() == 15 || r_scause() == 13){ // 15 write page fault  13 read page fault
    uint64 va = r_stval();
    if(va >= MAXVA || (va <= PGROUNDDOWN(p->trapframe->sp) && va >= PGROUNDDOWN(p->trapframe->sp) - PGSIZE)){
        p->killed = 1;
    } else {
      if (pagefault(p->pagetable, va) < 0)
        p->killed = 1;
    }
  } 
  // some code ...
}

处理文件读取,需要根据偏移量,确定读取文件的哪一处内容,以页为单位。此外当读取的内容不足一页时,剩下的区域需要清 0 。因为 kalloc 分配的页中写了脏数据。

// only work for mmap
int
pagefault(pagetable_t pagetable, uint64 fault_va)
{
  // step 1 : check addr is in vma 
  struct vma* vma = getvma(fault_va);
  if (vma == 0)
    return -1;

  // step 2 : check permission
  /*
  #define PROT_NONE       0x0
  #define PROT_READ       0x1
  #define PROT_WRITE      0x2
  #define PROT_EXEC       0x4

  #define MAP_SHARED      0x01
  #define MAP_PRIVATE     0x02
  */
  if (r_scause() == 13 && (!(vma->prot & PROT_READ) || !(vma->file->readable)))
    return -1;    // not read permisson but excute read

  if (r_scause() == 15 && (!(vma->prot & PROT_WRITE) || !(vma->file->writable)))
    return -1;    // not write permisson but excute write

  // step 3 : alloc new page and map it , setup permission flag
  void* dst_pa = kalloc();
  if (dst_pa == 0){
    return -1;
  }
  uint8 flag = (vma->prot & PROT_READ) ? PTE_R : 0;
  flag = (vma->prot & PROT_WRITE) ? (flag | PTE_W) : flag;
  if (mappages(pagetable, PGROUNDDOWN(fault_va), PGSIZE, (uint64)dst_pa, PTE_U | PTE_X | flag) < 0){
    kfree(dst_pa);
    return -1;
  }

  // step 4 : load file content to memory
  uint offset = PGROUNDDOWN(fault_va) - vma->addr;
  vma->file->off = offset;

  int read = 0;
  if ((read = fileread(vma->file, PGROUNDDOWN(fault_va), PGSIZE)) < 0)
    return -1;

  // should clear zero
  if (read < PGSIZE) {
    uint64 pa = walkaddr(pagetable, PGROUNDDOWN(fault_va)) + read;
    memset((void*)pa, 0, PGSIZE - read);
  }

  return 0;
}

3.2 munmap

这里主要根据 flags 确定是否要将改动写回文件,由于前文的 lazy allocation 策略,此处写回执行 walkaddr 时,只会写回改动到的页面。
其次根据 vma.dirtyflag 确定是否已经全部接触映射。

uint64
sys_munmap(void)
{
  uint64 addr;
  int length;

  if(argaddr(0, &addr) < 0)
    return -1;
  if(argint(1, &length) < 0)
    return -1;

  struct vma* vma = getvma(addr);
  if (vma == 0)
    return -1;
  
  if (length > vma->length || addr < vma->addr)
    return -1;

  // step 1 : check strategy
  int start = ((addr - vma->addr) / PGSIZE);
  int end = (length % PGSIZE) == 0 ? length / PGSIZE : (length / PGSIZE) + 1;

  if (vma->flags & MAP_PRIVATE)
  {
     // do not write back to file 
     goto finish;
  } else if (vma->flags & MAP_SHARED) 
  {
    for (int i = start; i < end; i++)
    {
      if (walkaddr(myproc()->pagetable, (vma->addr + PGSIZE * i)))
      {
        vma->file->off = PGSIZE * i;
        filewrite(vma->file, vma->addr, PGSIZE);
      }
    }
  }

  uint mask = 1;
finish:
  for (int i = start; i < end; i++)
  {
    vma->dirtyflag &= ~(mask << i);
  }

  printf("dirty flag = %d\n", vma->dirtyflag);
  if (vma->dirtyflag == 0)
  {
    printf("all area unmmap , start recyle\n");
    vma->valid = 0;
    fileclose(vma->file);
  }
  return 0;
}

3.3 fork

fork 进程时也需要增加一些处理:

int
fork(void)
{
  // some code ...

  // copy mmap area
  for (i = 0; i < 16; i++)
  {
    if(p->vma[i].valid)
    {
      np->vma[i].addr = p->vma[i].addr;
      np->vma[i].dirtyflag = p->vma[i].dirtyflag;
      np->vma[i].file = p->vma[i].file;
      np->vma[i].length = p->vma[i].flags;
      np->vma[i].flags = p->vma[i].flags;
      np->vma[i].length = p->vma[i].length;
      np->vma[i].prot = p->vma[i].prot;
      np->vma[i].valid = 1;
      filedup(np->vma[i].file);
    }
      
  // some code ...
  }

其次,uvmcopy 及 uvmunmap 也需要允许 PTE_V 为 0 的 pte,因为前面实现了 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){
    if((pte = walk(old, i, 0)) == 0)
      panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      continue;					// panic 修改为 continue;
    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;
}

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;				// panic 修改为 continue;
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    *pte = 0;
  }
}


posted @ 2022-04-05 09:55  lawliet9  阅读(14)  评论(0编辑  收藏  举报