实现原理

mmap

这次 lab 是要给 xv6 增加 mmap 和 munmap 体系调用。

mmap 的优点在于能够将一个文件直接映射到进程的地址空间中,从而避免了不必要的数据仿制,进步了文件操作的功率。与运用 read 和 write 体系调用不同,mmap 操作不需要将文件数据从内核缓冲区仿制到用户缓冲区,也不需要将用户缓冲区中的数据仿制回内核缓冲区。相反,它通过映射文件的方法,将文件数据直接映射到了进程的地址空间中,因此能够进步文件操作的功率。

同时 mmap 也避免了因为运用 read 和 write 体系调用而造成的在用户空间和内核空间的上下文切换,节省了体系调用的开销。

体系调用声明

mmap 体系调用的函数声明为:

void *mmap(void *addr, uint64 len, int prot, int flags, int fd, uint64 offset);
  • addr 为文件在用户地址空间的开端地址,一般传入 0,由内核设置;
  • len 为要映射的字节数量;
  • prot 为权限字段,指明该文件是可读(PROT_READ)、可写(PROT_WRITE)或可执行(PROT_EXEC)的;
  • flags 为符号位,符号映射的形式,MAP_SHARED 形式标识在 munmap 的时分需要把改动写回磁盘,MAP_PRIVATE 形式则不需要;
  • fd 是文件的描述符;
  • offset 为文件开端方位到开端映射的方位的偏移量。

munmap 体系调用的函数声明为:

int munmap(void *addr, uint64 len);
  • addr 为从哪里开端免除映射;
  • len 为免除映射的字节数。

代码实现

增加 mmap 和 munmap 体系调用的进程这儿就省掉了。直接来看实现。

首要,为了能够让用户进程知道关于文件映射的信息,需要在 proc 结构体记载下。新增 vma 结构体,来存储文件映射的相关信息:

struct vma {
  int    valid; // 该 vma 是否有用
  uint64 addr;  // 文件在进程地址空间中的开端地址
  uint64 len;   // 文件映射了多少字节
  int    prot;  // 文件权限
  int    flags; // 映射形式标识
  int    fd;    // 文件标识符
  struct file *file; // 指向对应的文件结构体
  uint64 offset; // 文件映射的偏移
};

并且在 proc 结构体中增加一个 vma 数组,依据 hint,大小为 16 即可:

struct vma vmatable[NVMA]; // NVMA 为定义在 kernel/param.h 中的宏

接着在 kernel/sysfile.c 中实现 sys_mmap 函数。大致流程如下:

  1. 接纳 mmap 体系调用传递的参数;
  2. 判别参数是否能够满意映射条件:
    1. 只读文件在 MAP_PRIVATE 形式下,是可写的;
    2. 只读文件在 MAP_SHARED 形式下,是不可写的。
  1. 从进程中记载的 vma 中找出一个空闲的 vma,并在进程的 heap 中找出一段可用的内存,将这段内存的开端地址作为体系调用的返回值。留意在这儿是不进行内存分配的,仅仅符号,跟 lazy alloction 是一样的,这样能够让映射比内存空间更大的文件成为或许。为了和进程正在运用的地址空间区分隔,挑选从 heap 的高方位开端向下扩展来映射文件,即从 TRAPFRAME 开端。
  2. 设置 vma 的值;
  3. filedup 对应文件;

mmap should increase the file’s reference count so that the structure doesn’t disappear when the file is closed.

close 体系调用封闭是的一个翻开的文件描述符,仅仅减少该文件的翻开引用数,在这儿增加一次引用后,就算调用了 close 也不会影响到对已经映射的内存。

  1. 返回映射的开端地址;

实现如下:

uint64
sys_mmap(void) {
  uint64 len, offset;
  int prot, flags, fd;
  if(argaddr(1, &len) < 0 || argint(2, &prot) < 0 || argint(3, &flags) < 0 || argint(4, &fd) < 0 || argaddr(5, &offset) < 0) {
    return -1;
  }
  struct proc *p = myproc();
  struct file *file = p->ofile[fd];
  if ((file->readable && !file->writable) && (prot & PROT_WRITE) && (flags & MAP_SHARED)) {
    return -1;
  }
  struct vma *vma = 0;
  int found = 0;
  uint64 addr = TRAPFRAME;
  for(int i = 0; i < 16; i++) {
    if(!p->vmatable[i].valid && !found) {
      found = 1;
      vma = &p->vmatable[i];
    } else if (p->vmatable[i].valid && p->vmatable[i].addr < addr) {
      addr = p->vmatable[i].addr;
    }
  }
  if (!found) {
    return -1;
  }
  addr = addr - len;
  vma->valid = 1;
  vma->fd = fd;
  vma->file = file;
  vma->len = len;
  vma->offset = offset;
  vma->prot = prot;
  vma->flags = flags;
  vma->addr = addr;
  filedup(vma->file);
  return addr;
}

完结这一步后,在用户程序中调用 mmap 就会返回一个正确的映射后的开端地址了,可是当进行拜访的时分,因为并没有分配内存,就会触发 page fault,所以跟 lazy alloction 一样,在 kernel/trap.c#usertrap 中处理 page fault。

// ...
} else if((which_dev = devintr()) != 0){
    // ok
} else if(r_scause() == 13 || r_scause() == 15) {
  uint64 va = r_stval();
  if (mmaphandler(va) == -1) {
    p->killed = 1;
  }
} else {
// ...

kernel/vm.c#mmaphandler 函数接纳一个虚拟内存地址(产生 page fault 的地址),来处理 pagefault。

在 mmaphandler 中,咱们需要做以下事情:

  1. 找出 va 是映射在哪个页中,也便是需要找出对应的 vma;
  2. 给 vma 正式分配内存;
  3. 依据 vma 中记载的 prot 来设置 PTE 的 flags;
  4. 将物理地址和虚拟地址进行映射;
  5. 运用 readi 将文件读到刚分配的内存中。在进行操作的时分要开启事务,并且对 inode 上锁。
int
mmaphandler(uint64 va) {
  int i;
  struct proc *p = myproc();
  struct vma *vma = 0;
  struct inode *ip;
  for(i = 0; i < NVMA; i++) {
    struct vma *v = &p->vmatable[i];
    if (v->valid) {
      if (va >= v->addr && va < (v->addr + v->len * PGSIZE)) {
        vma = v;
        break;
      }
    }
  }
  if (vma == 0) {
    return -1;
  }
  uint64 ka = (uint64)kalloc();
  if (ka == 0) {
    return -1;
  }
  memset((void *) ka, 0, PGSIZE);
  va = PGROUNDDOWN(va);
  pte_t * pte;
  // avoid remap panic.
  if ((pte = walk(p->pagetable, va, 0)) != 0 && (*pte & PTE_V) != 0) {
    kfree((void *) ka);
    return -1;
  }
  int flags = PTE_FLAGS(*pte);
  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, ka, flags | PTE_U) != 0) {
    kfree((void *) ka);
    return -1;
  }
  ip = vma->file->ip;
  begin_op();
  ilock(ip);
  if (readi(ip, 0, ka, PGROUNDDOWN(vma->offset + (va - vma->addr)), PGSIZE) < 0) {
    return -1;
  }
  iunlock(ip);
  end_op();
  return 0;
}

到这儿就能够拜访咱们映射到内存中的文件了。

接下来要实现 munmap 体系调用(kernel/sysfile.c#sys_munmap),留意依据文档,munmap 能够是一部分,可是不会是在中心。

An munmap call might cover only a portion of an mmap-ed region, but you can assume that it will either unmap at the start, or at the end, or the whole region (but not punch a hole in the middle of a region).

在 sys_munmap 函数中咱们要处理以下事情:

  1. 接纳 addr 和 len 参数;
  2. 找出 addr 对应的 vma;
  3. 判别 vma 是否是 MAP_SHARED 形式,如果是就调用 filewrite 将文件写回磁盘;
  4. 取消 munmap 部分的映射;
  5. 调整 vma 的长度和开端地址。
uint64
sys_munmap(void) {
  uint64 addr, len;
  if(argaddr(0, &addr) < 0 || argaddr(1, &len) < 0) {
    return -1;
  }
  int i;
  struct vma *vma = 0;
  struct proc *p = myproc();
  for (i = 0; i < NVMA; i++) {
    struct vma *v = &p->vmatable[i];
    if (v->valid && (v->addr <= addr && addr < (v->addr + len))) {
      vma = v;
    }
  }
  if (!vma) {
    return -1;
  }
  if (vma->flags & MAP_SHARED && vma->file->writable) {
    filewrite(vma->file, addr, len);
  }
  uvmunmap(p->pagetable, addr, len / PGSIZE, 1);
  vma->len -= len;
  if(vma->len == 0) vma->valid = 0;
  else {
    if (vma->addr == addr) vma->addr += len;
  }
  return 0;
}

留意修正 uvmunmap,否则会报 panic。

当进程退出的时分,即调用 kernel/proc.c#exit,咱们需要将它映射的所有文件都 munmap 掉,就像调用 munmap 体系调用。因为我的实现是父子进程并不同享物理内存,所以直接释放掉即可。

// ... kernel/proc.c#exit
int i;
for(i = 0; i < NVMA; i++) {
  struct vma *v = &p->vmatable[i];
  if (v->valid) {
    if (v->flags & MAP_SHARED && v->file->writable) {
      filewrite(v->file, v->addr, v->len);
    }
    uvmunmap(p->pagetable, v->addr, v->len/PGSIZE, 1);
    v->valid = 0;
  }
}

最终修正 kernel/proc.c#fork,在子进程仿制父进程的内存时,或许会仿制到没有映射或无效的条目,也要修正 uvmcopy 将 panic 去掉。在 fork 函数中只需要将 vma 仿制一份给子进程就能够了。

// ...kernel/proc.c#fork
for(i = 0; i < NVMA; i++) {
  np->vmatable[i] = p->vmatable[i];
}

到这儿 mmaptest 和 fork test 就都能够通过了。

运行成果

MIT 6.s081 Lab10: mmap

这次的 grader 倒是顺利跑过了。

总结

这个 lab 是对 file system 的进一步深化,不过我感觉跟虚拟内存或许愈加相关?难点主要是在 mmap 体系调用,要考虑怎么给 vma 找到一块适宜的内存空间,想清楚这儿之后其它的就比较简单了。page fault 的处理跟 lazy alloction 是一样的。munmap 体系调用就相当于做了一次反操作。