6.1810: Operating System Engineering 2023 <Lab9: mmap>

一、本节任务

二、Lab: mmap (hard)

2.1 mmap 介绍

mmap(2) 系统调用能将文件或者设备映射到内存中,返回映射区域的起始地址。

#include <sys/mman.h>

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

参数: 

  • addr: 指定映射的虚拟内存地址,若为 NULL 则由内核选择合适的虚拟内存地址;
  • length:映射的长度;
  • prot:映射内存的保护模式,可选值如下:

  PROT_EXEC:可以被执行;

  PROT_READ:可以被读取;

  PROT_WRITE:可以被写入;

  PROT_NONE:不可访问;

  • flags:指定映射的类型,可选值如下:

  MAP_FIXED:使用指定的起始虚拟内存地址进行映射;

  MAP_SHARED:与其他所有映射到这个文件的进程共享映射空间(可实现共享内存);

  MAP_PRIVATE:建立一个写时复制(copy-on-write)的私有映射空间;

  .....

  • fd:进行映射的文件描述符;
  • offset:文件偏移量(从文件何处开始映射);

例子

int fd = open(filepath, O_RDWR, 0644);                           // 打开文件
void *addr = mmap(NULL, 8192, PROT_WRITE, MAP_SHARED, fd, 4096); // 对文件进行映射

在上面的例子中,我们使用 open 以可读可写的方式打开文件,然后使用 mmap 对文件进行映射,映射的方式如下:

  • addr 为 NULL 表示让内核自动选择合适的虚拟内存地址进行映射;
  • length 为 8192 表示映射的区域为 2 个内存页的大小(一个内存页大小为 4KB);
  • prot 为 PROT_WRITE 表示映射的内存区域为可写;
  • flags 为 MAP_SHARED 表示映射区域为其他进程所共享;
  • fd 为打开文件描述符;
  • offset 为 4096 表示从文件的 4096 处开始映射;

下面是上述例子在内核中的结构: 

2.2 lab 实现

mmap 系统调用可以将文件映射到进程地址空间中、实现进程间的内存共享,而本次的 lab 就需要我们实现 mmap 和 munmap 系统调用。在实现 mmap 之前,有如下约定:

  • 测试文件中的 addr 一直为 0(NULL),所以内核应该决定在哪个地址映射文件,并且要把该地址返回,如果映射失败则返回 0xffffffffffffffff;
  • len 是要映射的字节个数,可能和文件长度不同;
  • prot 可以为 PROT_READ 和 PROT_WRITE;
  • flags 为 MAP_SHARED(映射内存区域的修改应该被写回文件中)和 MAP_PRIVATE(不写回文件),MAP_SHARED 的实现可以不共享内存区域(帮你减小难度);
  • offset 一直为 0(映射的开始位置为文件的开始);
  • munmap 需要能释放指定区域的映射空间,如果进程指定了 MAP_SHARED 并且修改了映射的内存区域,则需要先把它写回文件。munmap 释放的区域只会为映射区域的开头部分和结尾部分或者全部区域,不会出现释放中间部分的情况。

代码实现

首先为 struct proc 结构体增加一个长度为 16 的 vma 表:

/** kernel/proc.h **/

// VMA struct 
#define VMASIZE 16
struct vma {
  int valid;
  uint64 addr;
  int length;
  int prot;
  int flags;
  int offset;
  struct file *fp;
};


// Per-process state
struct proc {
  ......
  char name[16];               // Process name (debugging)
  struct vma vmas[VMASIZE];    // Virtual memory area array
};

接下来实现 mmap 系统调用, 在 mmap 中不需要分配实际的物理页面,等用户访问到相应页面时再触发中断,进入 trap 中拷贝相应页面。

/** kernel/sysfile.c **/

uint64
sys_mmap(void)
{
  uint64 failure = (uint64)((char *) -1);
  uint64 addr;
  int len, prot, flags, offset;
  struct file *f;
  struct proc *p = myproc();
  argaddr(0, &addr);
  argint(1, &len);
  argint(2, &prot);
  argint(3, &flags);
  if(argfd(4, 0, &f) < 0)
    return failure;
  argint(5, &offset);

  len = PGROUNDUP(len);
  if (MAXVA - len < p->sz)
    return failure;
  if(!f->readable && (prot & PROT_READ))
    return failure;
  if(!f->writable && (prot & PROT_WRITE) && (flags == MAP_SHARED))
    return failure;

  // find a empty vma
  struct vma *vp = p->vmas;
  for (int i = 0; i < VMASIZE; i++) {
    if (vp[i].valid == 0) {
      vp[i].valid = 1;
      vp[i].addr = p->sz;
      vp[i].length = len;
      p->sz += len; // 虚拟的增加进程大小, 但没有实际分配物理页
      vp[i].prot = prot;
      vp[i].flags = flags;
      vp[i].offset = offset;
      vp[i].fp = f;
      filedup(f); // add the file ref count
      return vp[i].addr;
    }
  }
  return failure;
}

在这样写完 mmap 后,当用户试图去访问 mmap 所返回的地址时,由于我们没有分配物理页,将会触发缺页中断。这个时候我们就需要在 usertrap 里把对应 offset 的文件内容读到一个新分配的物理页中,并把这个物理页加入这个进程的虚拟内存映射表里。 

/** kernel/trap.c **/

void
usertrap(void)
{
.....
.....
  } else if(r_scause() == 13 || r_scause() == 15) {
    uint64 va = r_stval();
    struct proc *p = myproc();
    if (va > MAXVA || va > p->sz) {
      p->killed = 1;
    } else {
      struct vma *vp = p->vmas;
      int found = 0;
      for (int i = 0; i < VMASIZE; i++) {
        if (vp[i].valid && va >= vp[i].addr && va < vp[i].addr + vp[i].length) {
          va = PGROUNDDOWN(va);
          uint64 pa = (uint64)kalloc();
          if (pa == 0)
            break;
          memset((void*)pa, 0, PGSIZE);
          ilock(vp[i].fp->ip);
          if (readi(vp[i].fp->ip, 0, pa, vp[i].offset + va - vp[i].addr, PGSIZE) < 0) {
            iunlock(vp[i].fp->ip);
            break;
          }
          iunlock(vp[i].fp->ip);
          int perm = PTE_U;
          if (vp[i].prot & PROT_READ)
            perm |= PTE_R;
          if (vp[i].prot & PROT_WRITE)
            perm |= PTE_W;
          if (vp[i].prot & PROT_EXEC)
            perm |= PTE_X;
          if (mappages(p->pagetable, va, PGSIZE, pa, perm) < 0) {
            kfree((void*)pa);
            break;
          }
          found = 1;
          break;
        }
      }
    if (!found)
        p->killed = 1;
    }
}
....

然后, 在 munmap 时,我们需要把分配的物理页释放掉,而且如果 flag 是 MAP_SHARED,直接把 unmap 的区域无脑复写回文件中,不管有没有被修改 (其实可以优化, 通过观察dirty bit来决定一个页是否需要被复写) 。

/** kernel/sysfile.c **/

uint64
sys_munmap(void)
{
  uint64 addr;
  int length;
  argaddr(0, &addr);
  argint(1, &length);
  struct proc *p = myproc();
  struct vma* vma = 0;
  int idx = -1;
  // find the corresponding vma
  for (int i = 0; i < VMASIZE; i++) {
    if (p->vmas[i].valid && addr >= p->vmas[i].addr && addr <= p->vmas[i].addr + p->vmas[i].length) {
      idx = i;
      vma = &p->vmas[i];
      break;
    }
  }
  if (idx == -1)
    // not in a valid VMA
    return -1;

  addr = PGROUNDDOWN(addr);
  length = PGROUNDUP(length);
  if (vma->flags & MAP_SHARED) {
    // write back 将区域复写回文件
    filewrite(vma->fp, addr, length);
  }
  // 删除虚拟内存映射并释放物理页
  uvmunmap(p->pagetable, addr, length/PGSIZE, 1);

  // change the mmap parameter
  if (addr == vma->addr && length == vma->length) {
    // fully unmapped 完全释放
    fileclose(vma->fp);
    vma->valid = 0;
  } else if (addr == vma->addr) {
    // cover the beginning 释放区域包括头部
    vma->addr += length;
    vma->length -= length;
    vma->offset += length;
  } else if ((addr + length) == (vma->addr + vma->length)) {
    // cover the end 释放区域包括尾部
    vma->length -= length;
  } else {
    panic("munmap neither cover beginning or end of mapped region");
  }
  return 0;
}

由于 uvmunmap 和 uvmcopy 两个函数会检查页面的 PTE_V,即页面是否加载到内存中来,这里需要跳过而不是 panic。 

/** kernel/vm.c **/

void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
...
    if((*pte & PTE_V) == 0)
      continue;
      //panic("uvmunmap: not mapped");
...
}

int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
...
    if((*pte & PTE_V) == 0)
      continue;
      //panic("uvmcopy: page not present");
...
}

下面就是在 fork 的时候需要复制父进程的 vmas 到子进程,并且在 exit 后需要解除映射。 

/** kernel/proc.c **/

int
fork(void)
{
...
  acquire(&np->lock);
  for (int i = 0; i < VMASIZE; i++) {
    np->vmas[i].valid = 0;
    if (p->vmas[i].valid) { // 复制vma entry
      memmove(&np->vmas[i], &p->vmas[i], sizeof(struct vma));
      filedup(p->vmas[i].fp); // 增加引用次数
    }
  }
  np->state = RUNNABLE;
  release(&np->lock);
...
}

void
exit(int status)
{
...
  // unmap any mmapped region
  for (int i = 0; i < VMASIZE; i++) {
    if (p->vmas[i].valid) {
      if (p->vmas[i].flags & MAP_SHARED) {
        filewrite(p->vmas[i].fp, p->vmas[i].addr, p->vmas[i].length);
      }
      fileclose(p->vmas[i].fp);
      uvmunmap(p->pagetable, p->vmas[i].addr, p->vmas[i].length / PGSIZE, 1);
      p->vmas[i].valid = 0;
    }
  }
...
}

最后通过所有测试!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值