MIT 6.s081 Lab10 mmap


本实验只有一个hard级别的任务,基本可以算是最综合的任务了。要求是实现mmap()munmap()这两个系统调用。

什么是mmap?

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

  • mmap函数是将文件地址映射到虚拟内存中,返回映射后的地址,同时记录该进程映射到的文件信息。
  • munmap函数就是取消进程地址空间中,文件地址某一部分的映射。

从上可以看出,这个实验涉及到了系统调用,虚拟内存,陷阱还有文件系统等。

首先还是增加系统调用老四样:

// user/usys.pl

entry("mmap");
entry("munmap");

// user/user.h

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

// kernel/syscall.h

#define SYS_mmap   22
#define SYS_munmap 23

// kernel/syscall.c
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);

[SYS_mmap]    sys_mmap,
[SYS_munmap]  sys_munmap,

根据manpage和实验提示,我们需要新建一个vma结构体来记录参数,推荐的大小为16,然后在进程结构体中添加这个字段:

// kernel/proc.h

#define VMASIZE 16
struct vma {
  int used;
  uint64 addr;
  int length;
  int prot;
  int flags;
  int fd;
  int offset;
  struct file *file;
};

// Per-process state
struct proc {
  ...
  struct vma vma[VMASIZE];
};

然后实现 mmap 函数,首先接收传来的参数,判断参数的合法性,然后遍历 VMA 数组,找到还没有使用的 vma,将参数信息添加进去。这里映射的虚拟地址,可以直接填写堆的最高地址,然后让堆继续生长。

// kernel/sysfile.c

uint64
sys_mmap(void)
{
  uint64 addr;
  int length, prot, flags, fd, offset;
  struct file *file;
  struct proc *p = myproc();
  if(argaddr(0, &addr) || argint(1, &length) || argint(2, &prot) ||
    argint(3, &flags) || argfd(4, &fd, &file) || argint(5, &offset)) {
    return -1;
  }
  if(!file->writable && (prot & PROT_WRITE) && flags == MAP_SHARED)
    return -1;
  length = PGROUNDUP(length);
  if(p->sz > MAXVA - length)
    return -1;
  for(int i = 0; i < VMASIZE; i++) {
    if(p->vma[i].used == 0) {
      p->vma[i].used = 1;
      p->vma[i].addr = p->sz;
      p->vma[i].length = length;
      p->vma[i].prot = prot;
      p->vma[i].flags = flags;
      p->vma[i].fd = fd;
      p->vma[i].file = file;
      p->vma[i].offset = offset;
      filedup(file);
      p->sz += length;
      return p->vma[i].addr;
    }
  }
  return -1;
}

处理页中断的方法与Lab5的懒分配差不多,如果有地址未映射的情况。需要将物理地址上的数据读到虚拟地址中,然后重新进行读取或写入操作。同时还要注意判断地址的合法性,然后判断地址是否在某个文件映射的虚拟地址范围内,如果找到该文件,则读取磁盘,并将地址映射到产生中断的虚拟地址上。

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

// kernel/trap.c

void
usertrap(void)
{
  ...
  else if((which_dev = devintr()) != 0){
    // ok
  } else if(r_scause() == 13 || r_scause() == 15) {
    uint64 va = r_stval();
    if(va >= p->sz || va > MAXVA || PGROUNDUP(va) == PGROUNDDOWN(p->trapframe->sp)) p->killed = 1;
    else {
      struct vma *vma = 0;
      for (int i = 0; i < VMASIZE; i++) {
        if (p->vma[i].used == 1 && va >= p->vma[i].addr && va < p->vma[i].addr + p->vma[i].length) {
          vma = &p->vma[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->file->ip);
          readi(vma->file->ip, 0, mem, offset, PGSIZE);
          iunlock(vma->file->ip);
          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;
          }
        }
      }
    }
  } else {
    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;
  }
...
}

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

至此mmap的实现基本完成,接下来实现munmap。这部分的逻辑比较简单,主要是取消虚拟地址的映射关系,同时,设置进程 VMA 结构体相应的 vma 为未使用状态。这部分函数,实验中做了简化,只需要取消地址与传入地址相同的文件的映射即可。

// kernel/sysfile.c

uint64
sys_munmap(void)
{
  uint64 addr;
  int length;
  struct proc *p = myproc();
  struct vma *vma = 0;
  if(argaddr(0, &addr) || argint(1, &length))
    return -1;
  addr = PGROUNDDOWN(addr);
  length = PGROUNDUP(length);
  for(int i = 0; i < VMASIZE; i++) {
    if (addr >= p->vma[i].addr || addr < p->vma[i].addr + p->vma[i].length) {
      vma = &p->vma[i];
      break;
    }
  }
  if(vma == 0) return 0;
  if(vma->addr == addr) {
    vma->addr += length;
    vma->length -= length;
    if(vma->flags & MAP_SHARED)
      filewrite(vma->file, addr, length);
    uvmunmap(p->pagetable, addr, length/PGSIZE, 1);
    if(vma->length == 0) {
      fileclose(vma->file);
      vma->used = 0;
    }
  }
  return 0;
}

最后,根据实验提示,我们还需要修改fork和exit,来保证文件映射的复制和清空。

// kernel/proc.c

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();
...
}

总结

感觉本实验是除了Lab3之外难度最大的实验了,但因为之前已经做过了每个部分的实验,所以上手还算比较快的。


文章作者: Watari
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Watari !
  目录