【XV6】 mmap

代码:https://github.com/JasenChao/xv6-labs.git

文件映射到进程地址

题目要求实现两个系统调用:mmapmunmap。主要功能就是将文件映射到进程的内存中。

题目给出了mmapmunmap的声明:

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

其中:

  • addr为映射的地址,在本实验中addr总是0,也就是由操作系统来决定映射的地址
  • length是映射的长度
  • prot对应读写的权限
  • flags为映射的类型,类型为shared时最终需要写回外存
  • fd是文件描述符
  • offset是偏移量,本实验中总是0

进程的虚拟内存

proc.h中定义一个结构体来标记虚拟内存,每个进程都需要有这样的一个结构体数组:

#define VMA_MAX 16
struct VMA{
  int valid;        //有效位,当值为 0 时表示无效,即为 empty element
  uint64 addr;      //记录起始地址
  int len;          //长度
  int prot;         //权限(read/write)
  int flags;        //区域类型(shared/private)
  int off;          //偏移量
  struct file* f;   //映射的文件
  uint64 mapcnt;    //(延迟申请)已经映射的页数量
};

struct proc {
  ...
  struct VMA vma[VMA_MAX];     // VMA
  uint64 maxaddr;              // heap可用最大地址
};

映射的地址从进程空间的高地址向低地址生长,因此增加一个变量maxaddr用来标记heap中可用的地址。

在进程初始化的时候,需要对应地处理新增的这部分内容,在proc.c中的allocproc函数中增加:

  for(int i = 0; i < VMA_MAX; i++){
    p->vma[i].valid = 0;              // 一开始所有的VMA都是无效的
    p->vma[i].mapcnt = 0;             // 一开始映射的数量为0
  }
  p->maxaddr = MAXVA - 2 * PGSIZE;    // 减去已被使用的trampoline和trapframe的空间

系统调用的具体实现

上面提到flagsshared时需要写回,先在file.c中实现写回的函数:

int
filewriteoff(struct file *f, uint64 addr, int n, int off)
{
  int r, ret = 0;

  if(f->writable == 0)
    return -1;

  if(f->type == FD_INODE){
    int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;
    int i = 0;
    while(i < n){
      int n1 = n - i;
      if(n1 > max)
        n1 = max;

      begin_op();
      ilock(f->ip);
      if ((r = writei(f->ip, 1, addr + i, off, n1)) > 0)
        off += r;
      iunlock(f->ip);
      end_op();

      if(r != n1){
        // error from writei
        break;
      }
      i += r;
    }
    ret = (i == n ? n : -1);
  } else {
    panic("my filewrite");
  }
  return ret;
}

defs.h中声明:

int             filewriteoff(struct file*, uint64, int n, int off);

sysfile.c中实现具体的系统调用函数:

uint64 sys_mmap(void)
{
  uint64 addr;
  int len , prot , flags , fd , off;
  argaddr(0, &addr);
  argint(1, &len);
  argint(2, &prot);
  argint(3, &flags);
  argint(4, &fd);
  argint(5, &off);
  
  struct proc* p = myproc();
  struct file* f = p->ofile[fd];
  
  // 检查权限和类型,MAP_SHARED类型对文件的修改会写回外存
  if(flags == MAP_SHARED && f->writable == 0 && (prot & PROT_WRITE))
    return -1;

  // 找到一块空的VMA
  int idx;
  for(idx = 0; idx < VMA_MAX; idx++)
    if(p->vma[idx].valid == 0)
      break;

  if(idx == VMA_MAX)
    panic("no empty vma field");
  
  struct VMA* vp = &p->vma[idx];
  vp->valid = 1;
  vp->len = len;
  vp->flags = flags;
  vp->off = off;
  vp->prot = prot;
  vp->f = f;
  filedup(f);                       // 保证文件不会被关闭
  vp->addr = (p->maxaddr -= len);   // len长度的内存分配出来,maxaddr减去len
  return vp->addr;
}

uint64 sys_munmap(void)
{
  uint64 addr;
  int len;
  argaddr(0, &addr);
  argint(1, &len);
  struct proc* p = myproc();

  struct VMA* vp = 0;
  for(int i = 0; i < VMA_MAX; i++)
    if(p->vma[i].addr <= addr && addr < p->vma[i].addr + p->vma[i].len && p->vma[i].valid == 1){
      vp = &p->vma[i];
      break;
    }
  if(vp == 0)
    panic("munmap no such vma");  

  // if the page has been mapped 
  if(walkaddr(p->pagetable , addr) != 0){
    // MAP_SHARED类型需要写回
    if(vp->flags == MAP_SHARED)
      filewriteoff(vp->f, addr, len, addr-vp->addr);
    uvmunmap(p->pagetable, addr, len/PGSIZE, 1);
    return 0;
  }
  // 引用计数为0时关闭文件
  if(0 == (vp->mapcnt -= len)){
    fileclose(vp->f);
    vp->valid = 0;
  }
  return 0;
}

按照之前的方法在以下文件中添加系统调用:

# user/user.h
void *mmap(void *addr, size_t len, int prot, int flags,
           int fd, off_t offset);
int munmap(void *addr, size_t len);

# user/usys.pl
entry("mmap");
entry("munmap");

# kernel/syscall.h
#define SYS_mmap   22
#define SYS_munmap 23

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

static uint64 (*syscalls[])(void) = {
...
[SYS_mmap]    sys_mmap,
[SYS_munmap]  sys_munmap,
};

注意到测试程序未进入编译,需要在Makefile中添加:

UPROGS=\
	$U/_cat\
	$U/_echo\
	$U/_forktest\
	$U/_grep\
	$U/_init\
	$U/_kill\
	$U/_ln\
	$U/_ls\
	$U/_mkdir\
	$U/_rm\
	$U/_sh\
	$U/_stressfs\
	$U/_usertests\
	$U/_grind\
	$U/_wc\
	$U/_zombie\
	$U/_mmaptest\

缺页中断

mmap不负责分配物理内存,因此只有在出现缺页中断时需要处理真实的内存分配,类似于之前实验中的写时复制,要在trap.c中的usertrap函数中增加对0xd缺页中断的处理:

  } else if(r_scause() == 0xd){
    uint64 addr = r_stval();
    struct VMA* vp = 0;
    // 找到缺页的VMA
    for(int i = 0; i < VMA_MAX; i++)
      if(p->vma[i].addr <= addr && addr < p->vma[i].addr + p->vma[i].len && p->vma[i].valid == 1){
        vp = &p->vma[i];
        break;
      }
    if(vp != 0){
      uint64 mem = (uint64)kalloc();
      memset((void*)mem, 0, PGSIZE);

      if(mappages(p->pagetable, addr, PGSIZE, mem, PTE_U | PTE_V | (vp->prot << 1)) == -1)
        panic("pagefault map error");
      
      vp->mapcnt += PGSIZE; //maintain the mapcnt
      ilock(vp->f->ip);
      readi(vp->f->ip, 0, mem, addr-vp->addr, PGSIZE); //copy a page of the file from the disk
      iunlock(vp->f->ip);
    }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;
    }

这里用到了很多无法调用的结构体,需要在trap.c中添加对应的头文件:

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

fork & exit

根据提示,fork需要保证父子进程有一样的内存映射,exit也需要处理取消映射,在proc.c中修改这两个函数,fork中需要多复制一份VMA数组:

  np->maxaddr = p->maxaddr;
  for(int i = 0; i < VMA_MAX; i++)
    if(p->vma[i].valid){
      filedup(p->vma[i].f);
      memmove(&np->vma[i], &p->vma[i], sizeof(struct VMA));
    }

exit需要取消已有的映射,发现类型为MAP_SHARED时调用写回函数:

  for(int i = 0; i < VMA_MAX; i++){
    if(p->vma[i].valid == 1){
      struct VMA* vp = &p->vma[i];
      for(uint64 addr = vp->addr; addr < vp->addr + vp->len; addr += PGSIZE){
        if(walkaddr(p->pagetable, addr) != 0){
          if(vp->flags == MAP_SHARED)
            filewriteoff(vp->f, addr, PGSIZE, addr-vp->addr);
          uvmunmap(p->pagetable, addr, 1, 1);
        }
      }
      fileclose(p->vma[i].f);
      p->vma[i].valid = 0;
    }
  }

这里用到了MAP_SHARED宏定义,因此要在proc.c中添加头文件:

#include "fcntl.h"

测试结果

使用make grade测试,结果如下:

== Test running mmaptest == 
$ make qemu-gdb
(2.4s) 
== Test   mmaptest: mmap f == 
  mmaptest: mmap f: OK 
== Test   mmaptest: mmap private == 
  mmaptest: mmap private: OK 
== Test   mmaptest: mmap read-only == 
  mmaptest: mmap read-only: OK 
== Test   mmaptest: mmap read/write == 
  mmaptest: mmap read/write: OK 
== Test   mmaptest: mmap dirty == 
  mmaptest: mmap dirty: OK 
== Test   mmaptest: not-mapped unmap == 
  mmaptest: not-mapped unmap: OK 
== Test   mmaptest: two files == 
  mmaptest: two files: OK 
== Test   mmaptest: fork_test == 
  mmaptest: fork_test: OK 
== Test usertests == 
$ make qemu-gdb
usertests: OK (38.6s)
posted on 2024-02-14 19:06  未连接到互联网  阅读(29)  评论(0编辑  收藏  举报