Task: mmap

Lab 介绍

这个实验需要在 xv6 中实现 mmap 功能。mmap 和 munmap 系统调用允许 UNIX 程序对其地址空间进行详细控制。 它们可用于在进程之间共享内存,将文件映射到进程地址空间,以及作为用户级页面错误方案的一部分。在本实验中,我们需要实现 mmap 和 munmap 两个 system call,只关注 mmap 的“将文件映射到进程地址空间”的功能。

mmap 的函数声明如下:

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

我们只关注使用 mmap 来实现内存映射文件的功能,这些参数中:

  • addr 参数始终传入 0,所以由 kernel 来决定文件映射后的内存地址即可;
  • length 是要映射的字节数,因为调用者可能只想映射文件的一部分而并非全部;
  • prot 指示了内存是否被映射为可读、可写 or/and 可执行,这里的 prot 是 PROT_READPROT_WRITE 或 both
  • flags 指示了在 munmap 时,对映射内存的修改是否要写回到文件中,如果是 MAP_SHARED 表示对映射内存的修改应写回到文件,如果是 MAP_PRIVATE 则表示不需要写回。
  • fd 是要映射的文件的描述符
  • offset:指示了在文件中要映射的起点,在这里假设始终传入 0,即总是从文件开头开始映射
  • 返回值是映射的内存地址,0xffffffffffffffff 代表失败。

这个 system call 将文件映射到一块内存中,并将这个映射信息记录到一个 VMA 结构体中。每个进程维护自己已经 mmap 映射的信息,在代码中就是在 struct proc 结构体中放一个 VMA 数组字段 vma_pool 来存放当前所有 mmap 的信息。

munmap 用来删除一个内存地址范围的 mmap 映射,同时如果有 MAP_SHARED 标志位且进程修改了内存,那么需要写回到文件中。注意,munmap 调用可能只覆盖了 mmap-ed 区域的一部分,但是您可以假设它将在开始或结束时取消对整个区域的映射(但不会在区域的中间打孔)。munmap 函数调用如下:

int munmap(char *addr, int length);

代码实现后,实验要求通过 mmaptest 测试。

Lab 的代码实现如下:

Lab 代码实现

1)增加 mmap 和 munmap 的 system call 声明

在 syscall.h 中增加 system call 号:

syscall.h

将 mmap 和 munmap 加入到 syscall 表中(syscall.c):

syscall.c

在 sysfile.c 中增加 sys_mmap() 和 sys_munmap() 的实现,实现部分先直接 return 0 即可:

uint64
sys_mmap(void)
{
	return 0;
}

uint64
sys_munmap(void)
{
	return 0;
}

在 user/user.h 中增加 mmap 和 munmap 的用户空间代码的声明:

user.h

在 usys.pl 中增加两个系统调用的 entry:

usys.pl

2)Makefile 中增加对 mmaptest 的编译

在 Makefile 中的 UPROUGS 中增加对 mmaptest 的编译:

Makefile

由此,make qemu 后即可运行 mmaptest 程序,不再报编译错误,接下来需要实现 mmap 和 munmap 函数。

3)使用 VMA 存储 mmap 映射信息

VMA 存储了 mmap-ed 的信息。当一个进程中用户程序调用 mmap 后,kernel 会将已经 mmap 的信息记录到进程的一个 VMA 数组中(一次 mmap 一个 VMA),在实现里,struct proc 中有一个 vma_pool 字段存储了所有的 VMA,是一个固定大小的数组(长度为 16),当我们在 mmap 后需要存储一个 VMA 时,就是从 vma_pool 中找一个空闲的位置并将 VMA 放进去。

VMA 结构体声明(proc.h):

proc.h

VMA 结构体中字段的含义:

  • used:这个 VMA 是否已经使用,用于在 vma_pool 中寻找空闲的位置
  • addr:在内存中映射的内存地址
  • length:映射的大小
  • prot:mmap 参数中的权限
  • flags:mmap 参数中的 flags
  • offset:mmap 参数中的 offset,恒为 0
  • f:所要映射的文件

在 struct proc 结构体中增加 vma_pool 字段:

struct proc
在进程初始化函数(proc.c 中的 allocproc 函数)中增加对 vma_pool 的初始化:

allocproc

实现在 vma_pool 中分配和释放 VMA 的逻辑(proc.c),所谓的“分配”其实也就是在 vma 数组中寻找一个未使用的位置:

proc.c

将以上函数声明写到 defs.h 中:

defs.h

4)实现 sys_mmap

sys_mmap() 函数的实现逻辑:

  1. 先解析调用 system call 传入的参数
  2. 检查权限是否能够 mmap
  3. 从进程的 vma_pool 的分配一个空的 VMA 位置,并将其填充 mmap 的相关信息
  4. 把内存分配出去(其实就是增加当前进程的 sz)

sys_mmap

5)实现 sys_munmap

sys_munmap() 函数的实现逻辑:

  1. 先解析调用 system call 的参数
  2. 在 vma_pool 中找到 munmap 的内存地址所对应的 VMA
  3. 根据 VMA 的信息,将修改数据回写到文件中
  4. 更新 VMA 中的信息,同时如果发现当初 mmap 的内存全部被 munmap,那就需要释放 VMA 以及对 file 的引用

sys_munmap

int
munmap_impl(uint64 addr, int length)
{
  struct proc *p = myproc();

  // 在 vma pool 中找到 addr 对应的 vma
  struct VMA *vma = 0;
  int i;
  for (i = 0; i < MAX_VMA_POOL; i++) {
    vma = p->vma_pool + i;
    if (vma->used == 1 && addr >= vma->addr && (addr + length) < (vma->addr + vma->length)) {
      break;
    }
  }
  if (i > MAX_VMA_POOL) {
    return -1;
  }
  // 根据 vma 的信息,将数据回写入文件中
  uint64 begin_addr = addr;
  uint64 end_addr = addr + length;
  if (vma->flags == MAP_SHARED && vma->f->writable) {
    uint64 cur_addr = begin_addr;
    while (cur_addr < end_addr) {
      int sz = end_addr - cur_addr >= PGSIZE? PGSIZE: end_addr - cur_addr;
      begin_op();
      ilock(vma->f->ip);
      if (writei(vma->f->ip, 1, cur_addr, cur_addr - vma->addr, sz) != sz) {
        return -1;
      }
      iunlock(vma->f->ip);
      end_op();
      uvmunmap(p->pagetable, cur_addr, 1, 1);
      cur_addr += PGSIZE;
    }
  }
  // 完成回写后,更新 vma 中的信息
  if (addr == vma->addr) {  // 说明 addr 是 mmap 内存的头部
    vma->addr += length;
    vma->length -= length;
  } else if (addr + length == vma->addr + vma->length) { // 说明 addr 是 mmap 的尾部
    vma->length -= length;
  }
  // 如果 mmap 的内存全部被 munmmap,那需要释放 vma 以及对 file 的引用
  if (vma->length == 0 && vma->used == 1) {
    filedup(vma->f);
    vma->used = 0;
  }
  return 0;
}
6)在 page fault handler 中分配物理内存

我们在 mmap 中只是分配了进程的虚拟内存,并未实际分配物理内存,当用户访问被 mmap 的内存时,会产生 page fault,我们需要在 page fault handler 中分配实际的物理内存。

在 trap.c 中的 usertrap 中增加 page fault 的处理:

usertrap

实现 page_fault_handler() 完成物理内存的分配:

#include "fcntl.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"

......


void page_fault_handler() {
  struct proc *p = myproc();
  // 检查 va 合法性
  uint64 va = r_stval();
  if (va >= p->sz || va < PGROUNDDOWN(p->trapframe->sp)) {
    p->killed = 1;
    return;
  }
  va = PGROUNDDOWN(va);
  // 遍历 vma pool,判断 va 是否是被 mmapp 的
  struct VMA *vma = 0;
  for (int i = 0; i < MAX_VMA_POOL; i++) {
    vma = p->vma_pool + i;
    if (vma->used == 1 && (va >= vma->addr) && (va < (vma->addr + vma->length))) {
      break;
    }
  }
  // 如果 `vma` 不为 0,说明 va 是被 mmap 的,需要为其分配物理内存
  if (vma != 0) {
    // 先分配物理内存
    char *kmem = kalloc();
    if (kmem == 0) {
      p->killed = 1;
      return;
    }
    memset(kmem, 0, PGSIZE); 
    // 将 va -> kmem 的映射加入到 pagetable 中
    if (mappages(p->pagetable, va, PGSIZE, (uint64) kmem, (vma->prot << 1) | PTE_U) != 0) { 
      kfree(kmem);
      p->killed = 1;
      return;
    }
    // 将映射文件的数据读入 kmem page 中
    ilock(vma->f->ip);
    readi(vma->f->ip, 0, (uint64) kmem, va - vma->addr, PGSIZE);
    iunlock(vma->f->ip);
  }
}

因为我们实现了 COW,也就是 mmap 的内存是 lazy allocation 的,这导致了虚拟内存并不一定有对应的物理内存,所以需要修改一下 vm.c 中的 uvmcopy 和 uvmunmap:

uvmummap

uvmcopy

7)修改 exit 和 fork

按照 Lab 的要求:

  • Modify exit to unmap the process’s mapped regions as if munmap had been called. Run mmaptest; mmap_test should pass, but probably not fork_test.
  • Modify fork to ensure that the child has the same mapped regions as the parent. Don’t forget to increment the reference count for a VMA’s struct file. In the page fault handler of the child, it is OK to allocate a new physical page instead of sharing a page with the parent. The latter would be cooler, but it would require more implementation work. Run mmaptest; it should pass both mmap_test and fork_test.

也就是说:

  • exit 应当增加释放当前进程全部 mmap-ed 的内存区域
  • fork 应当让 child process 拥有与 parent process 相同的 mapped regions

exit 的修改(proc.c 的 exit()):

exit 修改

fork 的修改(proc.c 的 fork()):

fork 的修改

注意,这里修改 exit 函数时使用了 munmap_impl() 函数,所以应当把这个函数的声明放到 defs.h 中:

munmap_impl

Lab 测试

至此,Lab 的测试即可通过:

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐