MIT-6.S081 | mmap(2021)

100 阅读4分钟

Lab: mmap

mmap

认真分析mmap

hints

  • char* mmap(void *addr, int length, int prot, int flags, int fd, int offset);
  • int munmap(void *addr, int length);
  • mmap() 映射的页面应该是 lazy alloc 的,以保证在映射大文件时不会阻塞
  • 每个进程应保持对 mmap() 映射的记录。创建一个符合 VMA 要求的结构体来保存它。每个进程的结构体数组应为16。
  • mmap() 的工作:在用户空间地址中寻找未映射的区域来映射文件,并用在进程信息中用结构体保存该映射的信息。该结构体应该保存指向被映射文件struct file 的指针,并且应该增加文件的引用次数
  • 处理缺页中断。在缺页中断中读入 对应位置的4096字节 文件放入内存并映射
  • munmap() 的工作:取消特定页面的映射,并将MAP_SHARED页面写回磁盘。如果一个映射被完全取消,记得减少相对应的文件引用
  • 你可以假设 munmap() 只从区域开头、结尾,或从中间开始到结尾的一整段开始,不会在中间打个洞这样子
  • 在本实验中不用考虑是否为脏页
  • 修改 exit() 来保证退出前所有的 mmap 已经取消
  • 修改 fork() 来确保子进程也拥有正确的映射

实现

首先是注册系统调用。

然后根据提示添加一个存储信息的结构体

//proc.h
struct vma_t{
  uint64 addr;  //addr
  int length;   //length
  int prot;     //prot
  int flags;    //flags
  struct file *f; // fd
  int offset;     //offset
  int used;  	
};
struct proc {
  ...
    struct vma_t vmas[16];
};

mmap的实现

uint64 sys_mmap(void)
{
  struct file *f;
  int length, prot, flags, offset;
  uint64 addr;
  if(argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0 || 
    argint(3, &flags) < 0 || argfd(4, 0, &f) < 0 || argint(5, &offset) < 0)
    return -1;
  // mmap不应分配物理内存或读取文件,文件不可写,但是prot 和 flags设置为写入
  if (f->readable && !f->writable && (prot & PROT_WRITE) && (flags & MAP_SHARED))
    return -1;

  struct proc *p = myproc();   //当前进程
  if(p->sz > MAXVA - length)
    return -1;
  //找一个没用过的
  for(int i=0; i<16; ++i)
  {
    if (p->vmas[i].addr == 0 && p->vmas[i].used == 0)
    {
      p->vmas[i].used = 1;
      p->vmas[i].addr = p->sz;
      p->vmas[i].length = length;
      p->vmas[i].prot = prot;
      p->vmas[i].flags = flags;
      p->vmas[i].f = f;
      p->vmas[i].offset = offset;
      filedup(f);  //计数+1
      p->sz += length;
      return p->vmas[i].addr;
    }
  }
  return -1;  //没找到
}

到此为止我们所有的映射都是懒分配的,所以需要一个处理缺页错误:

查询 riscv 的手册,以及实验提示,可以找到 scause 寄存器中储存 13 和 15 代表缺页错误

//trap.c
#include "fcntl.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"

void usertrap(void)
{
    ...
    else if((which_dev = devintr()) != 0){
    // ok
  } 
  else if(r_scause() == 13 || r_scause() == 15) 
  {
    uint64 va = r_stval(); // 读stval寄存器
    if(va >= p->sz || va >MAXVA || PGROUNDUP(va) == PGROUNDDOWN(p->trapframe->sp)) p->killed = 1;
    else
    {
      struct vma_t *vma = 0;
      // 找出错的vma
      for(int i=0; i<16; ++i)
      {
        if(p->vmas[i].used == 1 && va >= p->vmas[i].addr && va < p->vmas[i].addr + p->vmas[i].length)
        {
          vma = &p->vmas[i];
          break;
        }
      }
      if(vma)
      {
        va = PGROUNDDOWN(va);
        uint64 offset = va - vma->addr;
        uint64 mem = (uint64)kalloc();
        if(mem == 0)  //分配内存失败
        {
          p->killed = 1;
        }
        else
        {
          memset((void*)mem, 0, PGSIZE);
          ilock(vma->f->ip);
          readi(vma->f->ip, 0, mem, offset, PGSIZE);
          iunlock(vma->f->ip);
          //设置flag
          int flag = PTE_U;
          if (vma->prot & PROT_READ)
            flag |= PTE_R;
          if (vma->prot & PROT_WRITE)
            flag |= PTE_W;
          if (vma->prot & PROT_EXEC)
            flag |= PTE_X;
          if(mappages(p->pagetable, va, PGSIZE, mem, flag) != 0){
            kfree((void*) mem);
            p->killed = 1;
          }
        }
      }
    }
  }
}

这里需要注意,由于测试时会测试地址在栈空间之外等不合法的地方,因此产生读写中断时,需要首先判断地址是否合法。然后判断地址是否在某个文件映射的虚拟地址范围内,如果找到该文件,则读取磁盘,并将地址映射到产生中断的虚拟地址上。

还需要注意,由于一些地址并没有进行映射,因此在 walk 的时候,遇到这些地址直接跳过即可:

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)
      //panic("uvmcopy: page not present");
      continue;
...
}

接下来是munmap主要是取消虚拟地址的映射关系,同时,设置进程 VMA 结构体相应的 vma 为未使用状态。

uint64 sys_munmap(void)
{
  uint64 addr;
  int length;
  struct proc *p = myproc();
  struct vma_t *vma = 0;
  if(argaddr(0, &addr) || argint(1, &length))
    return -1;
  for(int i=0; i<16;++i)
  {
    if(addr >= p->vmas[i].addr && addr < p->vmas[i].addr + p->vmas[i].length)
    {
      vma = &p->vmas[i];
      break;
    }
  }

  if(vma == 0) return 0;
  if(vma->addr == addr)
  {
    vma->addr += length;
    vma->length -= length;
    if (vma->flags & MAP_SHARED)
      filewrite(vma->f, addr, length);
    uvmunmap(p->pagetable, addr, length/PGSIZE, 1);
    if(vma->length == 0)
    {
      fileclose(vma->f);
      vma->used = 0;
    }
  }
  return 0;
}

最后fork/exit,在进程创建和退出时,需要复制和清空相应的文件映射:

int
fork(void){
...
  np->state = RUNNABLE;
  for(int i = 0; i < VMASIZE; i++) {
    if(p->vma[i].used){
      memmove(&(np->vma[i]), &(p->vma[i]), sizeof(p->vma[i]));
      filedup(p->vma[i].file);
    }
  }
  release(&np->lock);
...
}

void
exit(int status){
...
  for(int i = 0; i < VMASIZE; i++) {
    if(p->vma[i].used) {
      if(p->vma[i].flags & MAP_SHARED)
        filewrite(p->vma[i].file, p->vma[i].addr, p->vma[i].length);
      fileclose(p->vma[i].file);
      uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].length/PGSIZE, 1);
      p->vma[i].used = 0;
    }
  }
  begin_op();
...
}

真难