目录

6.S081 lab10 mmap

目录

mmapmunmap系统调用允许 UNIX 程序对其地址空间进行更为细致的控制。它们可用于在进程间共享内存,将文件映射到进程地址空间,并作为用户级page fault方案的一部分。在本实验室中,我们将在xv6中添加mmapmunmap系统调用,重点是memory-mapped files

Lab: mmap

mmap的 API 如下:

1
2
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);

xv6中,addr始终为 0,所以由kernel自行判断应当 map 的地址;prot表示了 mapped memory 的 R、W、X 权限,flagsMAP_SHARED或者MAP_PRIVATE,前者表示对 mapped memory 的修改应写回文件,后者则不需要;offer永远为 0,不用处理文件的偏移量;mmap 成功将返回对应内存起始地址;失败返回0xffffffffffffffff

munmap(addr, length)需要将从 addr 开始的长度为length的内存unmap。实验指导书保证被munmap的这段内存位于mmap内存区间的头部/尾部或者是全部,munmap不会在中间挖一个洞;当内存是以MAP_SHARED模式被mmap时,需要先将修改写回文件。

看完了对于mmapmunmap的要求,发现其实测试没有一些比较难的case,为我们的实现提供了便利。

之后,就可以跟着 hints 完成实验:

  • 首先添加mmapmunmap的系统调用声明,并且在Makefile中加入_mmaptest

  • 对 mapped memory 要使用 lazy allocation,就像在之前的实验中那样,这样子使得我们可以在物理内存有限的情况下mmap尽可能大的文件。

  • 记录mmap为每个进程 map 文件的情况,例如地址,长度,权限,对应的文件等等。由于xv6没有真正的内存分配器,所以我们使用一个定长的数组去存储,16 就足够了。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    struct VMA {
      int used;
      uint64 addr;
      uint64 end;
      int prot;
      int flags;
      int offset;
      struct file *f;
    };
    
    // in struct proc
    struct VMA vma[NVMA];
    uint64 mmap_start;
    
  • 实现mmap,从用户地址空间找到空闲处 map 文件,修改对应的VMA结构体记录,当对文件mmap后,应当增加文件的引用计数(filedup),这样当文件被关闭时,VMA持有的文件指针才不会失效。

    最重要的就是找到合适的空闲地址,用于mmapxv6的用户地址空间如下图:

    /6-S081-lab10-mmap/image-20210303172730172.png

    最顶部是trampolinetrapframe,它们占用了两个 page,和stack之间有很大的空闲地址,我们可以将文件 map 到trapframe之下,不断向下增长,mmap_start记录着trapframe下可用于mmap的起始地址,初始值为PGROUNDDOWN(MAXVA - (2 * PGSIZE))

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
    uint64
    sys_mmap(void)
    {
      int length, prot, flags, fd;
      struct file *f;
      if(argint(1, &length) < 0 || argint(2, &prot) < 0
        || argint(3, &flags) < 0 || argfd(4, &fd, &f) < 0) {
          return 0xffffffffffffffff;
      }
      if (!f->writable && flags == MAP_SHARED && (prot & PROT_WRITE)) {
        return 0xffffffffffffffff;
      }
      // find a vma
      struct proc *p = myproc();
      struct VMA *v;
      for (v = p->vma; v < p->vma + NVMA; v++) {
        if(!v->used) {
          break;
        }
      }
      if(v == p->vma + NVMA) {
        return -1;
      }
      filedup(f);
      v->addr = PGROUNDDOWN(p->mmap_start - length);
      v->end = v->addr + length;
      p->mmap_start = v->addr;
    
      v->used = 1;
      v->f = f;
      v->prot = prot;
      v->flags = flags;
      v->offset = 0;
      return v->addr;
    }
    
  • 当发生page fault时,为其分配一个真实的物理页面,使用readi将文件内容读入内存,然后将物理页面 map 到用户地址空间,记得正确设置页面的权限。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    
    else if (r_scause() == 13 || r_scause() == 15) {
        uint64 va = r_stval();
        if (va > MAXVA) {
          p->killed = 1;
        } else {
          if(mmap_alloc(p->pagetable, va) < 0) {
            p->killed = 1;
          }
        }
      }
    
    int
    mmap_alloc(pagetable_t pagetable, uint64 va)
    {
      char *mem;
      struct proc *p = myproc();
      struct VMA *v;
      // find vma struct
      for (v = p->vma; v < p->vma + NVMA; v++) {
        if(v->used && va >= v->addr && va < v->end) {
          break;
        }
      }
      if (v == p->vma + NVMA) {
        return -1;
      }
      mem = kalloc();
      if(mem == 0){
        return -1;
      }
      memset(mem, 0, PGSIZE);
      begin_op();
      ilock(v->f->ip);
      int len;
      if((len = readi(v->f->ip, 0, (uint64)mem, va - v->addr, PGSIZE)) < 0) {
        iunlock(v->f->ip);
        end_op();
        return -1;
      }
      iunlock(v->f->ip);
      end_op();
      int f = PTE_U | (v->prot << 1);
      if(mappages(pagetable, va, PGSIZE, (uint64)mem, f) != 0) {
        kfree(mem);
        return -1;
      }
      return 0;
    }
    
  • 实现munmap,找到对应的VMA,使用uvmunmap unmap 对应的内存,当一个mmap的所有内存都被 unmap 时,需要减少对应文件的引用计数;如果内存被修改过,且是以MAP_SHARED模式被mmap,那么需要先将内存内容写回文件。理想态下,我们只应当写回dirty page,但是测试中不会检查这一点,所以将所有内存写回文件即可了。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    
    struct file*
    fileundup(struct file *f)
    {
      acquire(&ftable.lock);
      if(f->ref < 1)
        panic("filedup");
      f->ref--;
    
      release(&ftable.lock);
      return f;
    }
    
    uint64
    sys_munmap(void)
    {
      uint64 addr;
      int length;
      if(argaddr(0, &addr) < 0 || argint(1, &length) < 0) {
        return -1;
      }
      return s_munmap(addr, length);
    
    }
    
    uint64
    s_munmap(uint64 addr, int length) {
      struct proc *p = myproc();
      struct VMA *v;
      for (v = p->vma; v < p->vma + NVMA; v++) {
        if(v->used && v->addr <= addr && addr + length <= v->end) {
          break;
        }
      }
      if(v == p->vma + NVMA) {
        return -1;
      }
    
      uint64 end = addr + length;
      uint64 _addr = addr;
      while (addr < end) {
        // if already load in
        if(walkaddr(p->pagetable, addr)) {
          if(v->flags == MAP_SHARED && v->f->writable) {
            begin_op();
            ilock(v->f->ip);
            int size = min(end-addr, PGSIZE);
            if(writei(v->f->ip, 1, addr, addr - v->addr, size) < size) {
              iunlock(v->f->ip);
              end_op();
              return -1;
            }
            iunlock(v->f->ip);
            end_op();
          }
          uvmunmap(p->pagetable, addr, 1, 1);
        }
        addr += PGSIZE;
      }
      if(_addr == v->addr) {
        v->addr += length;
      } else if(_addr + length == v->end) {
        v->end -= length;
      }
    
      if (v->addr == v->end) {
        fileundup(v->f);
        v->used = 0;
      }
    
      return 0;
    }
    

    实现时遇到两个坑:

    • uvmunmap时,首先要判断该内存是否真的被lazy allocation了,否则要像lazylab 中一样,修改uvmunmap,我觉得这样实现比较 ugly,因为破坏了uvmunmap发现错误的功能。
    • 最开始实现时,只要v->flags == MAP_SHARED就将文件写回,结果在forktest中父子进程内存内容不一致,查看forktest代码发现原来创建的只读文件,只要prot不标志为可写,那么制度文件也是可以用MAP_SHARED模式进行mmap的。所以还要加上v->f->writable或者prot & PROT_WRITE
  • 修改exit代码,使得exit被调用后,unmap 所有被mmap的内存。

    1
    2
    3
    4
    5
    6
    7
    8
    
    struct VMA *v;
    for (v = p->vma; v < p->vma + NVMA; v++) {
      if(v->used) {
        if (s_munmap(v->addr, v->end - v->addr) < 0) {
          panic("exit:munmap");
        }
      }
    }
    
  • 修改 fork 代码,使得子进程拥有和父进程相同的mmap文件。可以直接为子进程分配新的物理内存用于mmap,不用共享相同物理页面。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    np->mmap_start = p->mmap_start;
    for(i = 0; i < NVMA; i++) {
      np->vma[i].addr = p->vma[i].addr;
      np->vma[i].end = p->vma[i].end;
      np->vma[i].used = p->vma[i].used;
      np->vma[i].flags = p->vma[i].flags;
      np->vma[i].prot = p->vma[i].prot;
    
      if(p->vma[i].used && p->vma[i].f) {
        np->vma[i].f = filedup(p->vma[i].f);
      }
    }