 [MIT6.S081]Lab10: Mmap
[MIT6.S081]Lab10: Mmap
  # [MIT6.S081]Lab10: Mmap
Lab: mmap (mit.edu) (opens new window)
# mmap
最后一个 lab,也是最后一个 assignment🌟!
我们要实现一个简易版的 mmap 和 munmap,具体的 mmap 可以查看手册 man 2 mmap,非常简单易读。
简单来说 mmap 可以在一个文件描述符和一段虚拟地址之间建立起映射,而 munmap 可以解除这个映射,而在本实验中我们将专注于 memory-mapped files。
而 mmap 又分 shared 映射 和 private 映射,我们知道可以将一个文件映射到多个进程的地址空间。如果此时是 shared 映射,那么我们会对该文件的修改会实际写回文件,那么不同进程都会接收到这一修改;而如果是 private 映射,那么就类似于写时复制 copy-on-write,当一个进程修改这部分内容后会拷贝独立的一份,这是对其他进程不可见的。不过在实验文档里说明了在本实验中如果被 shared 映射的文件是可以不共享物理页的。
另外在本实验中,第一个参数 void *addr 我们默认为 0,也就是 NULL,要求内核自己寻找合适的位置来进行映射,同时第六个参数 off_t offset 我们也默认为 0。
与此同时,每个进程中都维护了一段线性空间 VMA (Virtual Memory Area),其用来描述了虚拟空间的一些属性,在这里我们用它来描述不同虚拟地址的大小以及映射关系等信息。
我们先在 proc.h 中添加这部分内容
struct vma {
  int valid; // 0 表示空闲,1 表示不空闲
  uint64 addr; // 一段虚拟地址的起始点
  int len; // 这段虚拟地址的长度
  int offset; // 映射的偏移量
  int prot; // protection,标记读写执行权限
  int flags; // flags,比如标记文件是共享文件,还是私有文件 
  struct file *f;
};
struct proc {
// ......
  struct vma vmas[NVMA];
};
接着我们要添加两个系统调用 sys_mmap 和 sys_munmap,具体添加流程可以看之前的记录,这里不再阐述。
我们先开始实现 sys_mmap,我们要选用一段合适的虚拟空间来和文件进行映射,我们直接用 p->sz 作为起点,随后每添加一次映射,我们就令 p->sz += len。所以我们就先遍历 vmas 数组,找到一个空闲的 vma,接着填入一些参数即可。
uint64 sys_mmap(void) {
  int len, prot, flags, fd;
  struct file *f;
  if (argint(1, &len) < 0 || argint(2, &prot) < 0 || argint(3, &flags) < 0 || argfd(4, &fd, &f) < 0) { // 获取参数
    return -1;
  }
  if ((!f->readable && (prot & PROT_READ)) || ((flags & MAP_SHARED) && (!f->writable && (prot & PROT_WRITE)))) { // 共享文件要检查权限位一致
    return -1;
  }
  struct proc *p = myproc();
  struct vma *vma = p->vmas;
  for (int i = 0; i < NVMA; i++) {
    if (vma[i].valid == 0) { // 找到一个空闲的 VMA
      vma[i].valid = 1;
      vma[i].addr = p->sz;
      vma[i].len = PGROUNDUP(len); // page-aligned
      p->sz += vma[i].len;
      vma[i].flags = flags;
      vma[i].prot = prot;
      vma[i].f = filedup(f);
      return vma[i].addr;
    }
  }
  return -1;
}
需要注意的是,我们还要检查 prot 与文件的权限是否一致,并对 shared 文件进行特殊检查。
建立完映射后,当产生 page fault 时我们才会实际去分配页面,类似于 lazy allocation (不过 2021 的 lab 里没有这个实验,估计是和 mmap lab 合并了)
这里的处理方式我参考了 [mit6.s081] 笔记 Lab10: Mmap | 文件内存映射 | Miigon's blog (opens new window),模块化的写法感觉简洁很多,好处理很多。
// trap.c
void usertrap(void) {
// ......
  } else if((which_dev = devintr()) != 0){
    // ok
  } else if (r_scause() == 13 || r_scause() == 15) { // 处理 page fault 
    uint64 va = r_stval();
    if (!vmalazyload(va)) {
      goto error;
    }
  } else {
    error:
    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;
  }
// ......
}
struct vma *findvma(struct proc *p, uint64 va) {
  for (int i = 0; i < NVMA; i++) {
    struct vma *vma   = p->vmas + i;
    if (vma->valid == 1 && vma->addr <= va && vma->addr + vma->len > va) { // va 在其所属范围内
      return vma;
    }
  }
  return 0;
}
int vmalazyload(uint64 va) {
  struct proc *p = myproc();
  struct vma *vma = findvma(p, va); // 找 va 所在的合法的 vma
  if (vma == 0) {
    return 0;
  }
  // 分配物理页
  void *pa = kalloc();
  if (pa == 0) {
    panic("vmalazyload: kalloc");
  }
  memset(pa, 0, PGSIZE);
  // 从文件中读取数据到 pa 里
  struct inode *ip = vma->f->ip;
  begin_op();
  ilock(ip);
  readi(ip, 0, (uint64)pa, vma->offset + va - vma->addr, PGSIZE);
  iunlock(ip);
  end_op();
  //进行映射,先设置权限位
  uint flags = PTE_U;
  if (vma->prot & PROT_READ) {
    flags |= PTE_R;
  }
  if (vma->prot & PROT_WRITE) {
    flags |= PTE_W;
  }
  if (vma->prot & PROT_EXEC) {
    flags |= PTE_X;
  }
  if (mappages(p->pagetable, va, PGSIZE, (uint64)pa, flags) < 0) {
    panic("vmalazyload: mappages"); 
  }
  return 1;
}
findvma() 函数根据传入的 va,找到一个合法的 vma,并且满足 va 在 vma 指向的虚拟空间内。
在找到 vma 后我们要先实际的分配一块物理内存,然后将被映射的文件的内容拷贝到其中,最后我们设置权限以及添加映射。
如此一来我们的 mmap 部分就是实现完了,下面是 munmap 部分
这部分实现思路上有参考 xv6-labs-2020.lab9.mmap | Banbao (banbao991.github.io) (opens new window)
不过在分类讨论的地方可以进一步细化,但本实验并不需要)
在找到 vma 后,我们先将 addr 和 len 进行页对齐,接着我分为了两种情况,一种是整个空间全部正好释放,而其他情况我们又分为是否从头开始 unmap。
对于全部释放,则我们要关闭 vma->f,具体引用计数的讨论可以查看 fileclose() 函数的源码。
而其他情况中,如果释放从头开始,则我们要额外修改 vma->addr ,也就是虚拟空间的起始点。而最后我们如果文件是 shared 映射的话,我们还要把当前修改写回文件。
uint64 sys_munmap(void) {
  uint64 addr;
  int len;
  if (argaddr(0, &addr) < 0 || argint(1, &len) < 0) {
    return -1;
  }
  struct proc *p = myproc();
  struct vma *vma = findvma(p, addr);
  if (vma == 0) {
    return -1;
  }
  uint64 va = (uint64)addr;
  len += va - PGROUNDDOWN(va);
  va = PGROUNDDOWN(va); // page-aligned
  int offset = vma->offset;
  int flag = 0; // 记录是否要关闭文件
  if (addr == vma->addr && len == vma->len) { // 全部释放
      vma->len = 0;
      flag = 1;
  } else {
    vma->len -= len;
    if (va == vma->addr) { // 从头释放
      vma->addr += len;
    }
  }
  if (vma->flags & MAP_SHARED) {
    if (filewrite_offset(vma->f, va, len, offset) == 0) {
      return -1;
    }
  }
  if (walkaddr(p->pagetable, va) != 0) { // 如果映射了
    uvmunmap(p->pagetable, va, len / PGSIZE, 0);
  }
  if (flag) {
    fileclose(vma->f);
  }
  return 0;
} 
filewrite_offset() 函数的实现可以直接抄 filewrite 的源码,但需要注意修改一下 offset,并且确保文件类型是 FD_INODE
int filewrite_offset(struct file *f, uint64 addr, int n, int offset) {
  int r, ret = 0;
  if (f->writable == 0)
    return 0;
  if (f->type == FD_INODE) {
    // write a few blocks at a time to avoid exceeding
    // the maximum log transaction size, including
    // i-node, indirect block, allocation blocks,
    // and 2 blocks of slop for non-aligned writes.
    // this really belongs lower down, since writei()
    // might be writing a device like the console.
    int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;
    int i = 0;
    while(i < n){
      int n1 = n - i;
      if(n1 > max)
        n1 = max;
      begin_op();
      ilock(f->ip);
      if ((r = writei(f->ip, 1, addr + i, offset, n1)) > 0)
        offset += r;
      iunlock(f->ip);
      end_op();
      if(r != n1) {
        // error from writei
        break;
      }
      i += r;
    }
    ret = (i == n ? n : -1);
  } else {
    panic("filewrite");
  }
  return ret;
}
这样 unmap 也基本实现了,但我们还要按照文档的 hints 修改一下 exit() 以及 fork(),以确保进程结束后其相关映射都会解除以及 fork 出子进程后,子进程有着一模一样的映射,当然引用计数还要自增一。
void exit(int status) {
// ......
  for (int i = 0; i < NVMA; i++) {
    struct vma *vma = p->vmas + i;
    if (vma->len) {
      uvmunmap(p->pagetable, vma->addr, vma->len / PGSIZE, 0);
      vma->len = 0;
    }
  }
// ......
}
int fork(void) {
// ......
  for (int i = 0; i < NVMA; i++) {
    struct vma *vma = p->vmas + i;
    if (vma->len) {
      memmove(np->vmas + i, vma, sizeof(struct vma));
      filedup(vma->f);
    } else {
      (np->vmas + i)->len = 0;
    }
  }
  return pid;
}
本实验到此结束!
# 实验结果
 
 # 课程实验总结
在做 MIT6.S081 这门课程的 lab 之前,我并没有看过其官方课程,所以在做 lab 的过程中挺折磨的,遇到英文的官方文档说实话也不是很能读下去,经常会去网上找其他的中文资料(感觉在读手册这方面自己还得再加把劲)。
个人感觉 2021 的 lab 会比 2020 更加精简一点,感觉 20 版的会更好点,不过大差也不差。
在做实验之前,我是粗略地过过一遍蒋炎岩老师的课,已经看过 csapp,所以也不算零基础,还是在对 os 有一定理解的基础上做的吧,做完所有 lab 之后也确实感觉对操作系统有了更进一步感受,不过目前还有系统地读过一本 os 方面专门的书,目前是打算后面再刷一遍蒋炎岩老师 2023 年的课,并且顺带读一遍 OSTEP,希望自己能坚持读完英语版的,不过中文版的自己倒是有。当然最近还买了本交大 IPADS 组写的银杏书,像在进一步深入一下,就是可能得安排到大三上了。
所有实验中让我影响最深刻的其实是系统调用,虽然并不难实现,但因为之前听课一直会听到这个词,lab 做完后确实让我对系统调用有了个非常实际性的理解,而不仅仅停留在纸面上,不过自己在 networking 和 file system 上感觉掌握的不是很好,是后面要花时间补的。
不管怎么说,mit 的 lab 都让我确实有了很好的体验,设计精妙的 xv6,一环扣一环的 lab,还有这如此丰富详尽的文档,当然还有很重要且友好的本地 test!我看了部分的课程视频,老师也讲的非常清晰 (%%% Morris教授)
老实说,做 lab 的过程中我多么希望自己学校的课程也是如此精彩,多么希望我以及无数计算机专业的学子可以不用去国外名校寻找课程资源,而是在自己的学校课程中收获满满......这个学期大二下,我有计组和计网两门计算机专业课,听了后感觉真的非常应试,我在想如果是不考研的同学,听了这个课程刷了很多应试题,一行代码不写一学期下来真的能学到什么吗...在这种答辩的环境中真的好窒息,自己也只能像逆行者一样,与别人格格不入。
不过最近自己拉了个小群,希望能带着大伙尽可能的跳出国内的教学方案,避免成为做了大量无用功的理论做题小子,而是去接触更新更精彩的计算机世界,学到真本事,也期待着国内会涌现出更多类似于蒋炎岩老师的操作系统课程,交大 IPADS 组的银杏书这类优秀的学习资源。
