mit 6.s081 lab10 mmap

lab10 mmap

mmap,内存映射文件,是一种在内存和磁盘之间创建映射关系的机制。它允许将一个文件的内容映射到进程的虚拟地址空间中,使得进程可以想访问内存一样访问文件,省去了繁琐的磁盘IO操作。具体来说,进程可以向操纵变量一样去访问文件,而不需要调用read和write等系统调用进行文件IO,大大提高了文件访问的效率。
若将同一个文件映射到多个进程的地址空间,则可以实现进程间的共享内存通信。
对于read系统调用,其底层实现大致如下:

  • 内核首先检查文件描述符(file descriptor)的有效性。
  • 根据文件描述符找到相应的文件表项(file table entry)。
  • 检查文件表项的权限和打开方式,确保可以进行读操作。
  • 内核将读取请求添加到文件的读取队列中。
  • 当磁盘准备好数据后,内核将数据从磁盘读取到内核缓冲区(buffer cache)中。
  • 将数据从内核缓冲区复制到用户空间的缓冲区中。
  • 返回读取的字节数。

对于write系统调用,其底层实现大致如下:

  • 内核首先检查文件描述符的有效性。

  • 根据文件描述符找到相应的文件表项。

  • 检查文件表项的权限和打开方式,确保可以进行写操作。

  • 将数据从用户空间的缓冲区复制到内核缓冲区中。

  • 将数据从内核缓冲区写入到磁盘中。

  • 返回写入的字节数。

    在这个过程中,涉及到了多次内核态和用户态之间的切换,以及数据的拷贝操作。这些操作会带来一定的开销。相比之下,mmap可以将文件内容直接映射到内存中,避免了部分的数据拷贝和系统调用,从而提高了文件IO的性能。

一些限制和注意事项:

  • 文件大小限制:mmap只能映射文件的部分或全部内容,受限于操作系统的文件大小限制。
  • 内存限制:映射文件需要占用一定的虚拟内存空间,如果映射的文件过大,可能会导致内存不足。
  • 文件同步:对映射的文件进行写操作时,需要手动进行同步(使用msync或munmap)才能将修改写回磁盘。

实验步骤

  1. 配置mmap,munmap系统调用。注意在kernel/fcntl.h中定义了PROT权限相关的宏,只有在定义了LAB_MMAP时这些宏才生效,而LAB_MMAP是在编译时在命令行通过gcc的-D参数定义的
void* mmap(void* addr, int length, int prot, int flags, int fd, int offset);
int munmap(void* addr, int length);

  1. 定义VMA结构体,并添加了进程控制块结构体中。VMA属性存储了该进程的内存映射文件的相关信息,比如是否正在使用、映射的起始地址、文件映射长度、文件的偏移、权限、标志位、文件描述符、文件对应的结构体struct file *。在PCB中vma是一个数组,限制了同时能映射的文件个数,一个VMA元素对应一个内存映射文件。
#define NVMA 16
// 虚拟内存区域结构体
struct vm_area {
  int used;           // 是否已被使用
  uint64 addr;        // 起始地址
  int len;            // 长度
  int prot;           // 权限
  int flags;          // 标志位
  int vfd;            // 对应的文件描述符
  struct file* vfile; // 对应文件
  int offset;         // 文件偏移,本实验中一直为0
};

struct proc {
  ...
  struct vm_area vma[NVMA];    // 虚拟内存区域
}

  1. 实现系统调用函数sys_mmap:
    (1)首先获取进程传递的系统调用参数void* mmap(void* addr, int length, int prot, int flags, int fd, int offset);并对参数进行安全检查;
    (2)遍历查找未使用的VMA结构体,找到合适的之后将参数复制进去,并增加对应文件的引用技术。
    (3)增加进程的地址空间大小,p->sz,实际上是lazy allocation,真正分配物理内存是在发生缺页中断时。
    (4)返回映射的虚拟地址起点,就是映射之前的地址空间的终点
uint64
sys_mmap(void) {
  uint64 addr;
  int length;
  int prot;
  int flags;
  int vfd;
  struct file* vfile;
  int offset;
  uint64 err = 0xffffffffffffffff;

  // 获取系统调用参数
  if(argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0 ||
    argint(3, &flags) < 0 || argfd(4, &vfd, &vfile) < 0 || argint(5, &offset) < 0)
    return err;

  // 实验提示中假定addr和offset为0,简化程序可能发生的情况
  if(addr != 0 || offset != 0 || length < 0)
    return err;

  // 文件不可写则不允许拥有PROT_WRITE权限时映射为MAP_SHARED
  if(vfile->writable == 0 && (prot & PROT_WRITE) != 0 && flags == MAP_SHARED)
    return err;

  struct proc* p = myproc();
  // 没有足够的虚拟地址空间
  if(p->sz + length > MAXVA)
    return err;

  // 遍历查找未使用的VMA结构体
  for(int i = 0; i < NVMA; ++i) {
    if(p->vma[i].used == 0) {
      p->vma[i].used = 1;
      p->vma[i].addr = p->sz;
      p->vma[i].len = length;
      p->vma[i].flags = flags;
      p->vma[i].prot = prot;
      p->vma[i].vfile = vfile;
      p->vma[i].vfd = vfd;
      p->vma[i].offset = offset;

      // 增加文件的引用计数
      filedup(vfile);

      p->sz += length;
      return p->vma[i].addr;
    }
  }

  return err;
}

  1. 处理内存映射文件导致的缺页中断:
    (1)在usertrap函数中,首先新增r_cause == 13 或 15引发中断的处理分支,表示读取或写入页面时发生异常;
    (2) 通过r_stval读取STVAL寄存器中保存的发生中断的虚拟地址va,并判断该地址是否在进程的地址空间内;
    (3)遍历VMA数组,找到有效的且地址空间范围符合覆盖了va,即vma[i].addr <= va && vma[i].addr + length > va;
    (4)找到对应的vam之后,就要为分配物理页面做准备了,需要根据vma的权限PROT设置页表项pte的权限PTER,主要是是否可读、可写、可执行。 确定好权限之后,分配物理页面并初始化为0;
    (5)计算对应文件的块,因为只需要映射va对应的文件块到内存中,所以需要计算一下va对应的文件块的偏移地址是多少。offset = vma.offset + PGROUNDDOWN(va - vma.addr);因为readi函数需要的是相对文件头的偏移地址,所以需要加上一开始的默认偏移vma.offset,由于都是以页/块为单位进行读写的,所以需要向下取整找到对应的块的偏移地址。最后使用readi函数在文件的偏移offet的位置读取一个页面大小的数据并保存到分配的物理页面pa中;
    (6)使用mappages函数在进程的页表中添加va(向下取整)到pa的映射,并设置相应的权限。
void
usertrap(void)
{
  ...
  if(cause == 8) {
    ...
  } else if((which_dev = devintr()) != 0){
    // ok
  } else if(cause == 13 || cause == 15) {
#ifdef LAB_MMAP
    // 读取产生页面故障的虚拟地址,并判断是否位于有效区间
    uint64 fault_va = r_stval();
    if(PGROUNDUP(p->trapframe->sp) - 1 < fault_va && fault_va < p->sz) {
      if(mmap_handler(r_stval(), cause) != 0) p->killed = 1;
    } else
      p->killed = 1;
#endif
  } else {
    ...
  }

  ...
}

/**
 * @brief mmap_handler 处理mmap惰性分配导致的页面错误
 * @param va 页面故障虚拟地址
 * @param cause 页面故障原因
 * @return 0成功,-1失败
 */
int mmap_handler(int va, int cause) {
  int i;
  struct proc* p = myproc();
  // 根据地址查找属于哪一个VMA
  for(i = 0; i < NVMA; ++i) {
    if(p->vma[i].used && p->vma[i].addr <= va && va <= p->vma[i].addr + p->vma[i].len - 1) {
      break;
    }
  }
  if(i == NVMA)
    return -1;

  int pte_flags = PTE_U;
  if(p->vma[i].prot & PROT_READ) pte_flags |= PTE_R;
  if(p->vma[i].prot & PROT_WRITE) pte_flags |= PTE_W;
  if(p->vma[i].prot & PROT_EXEC) pte_flags |= PTE_X;


  struct file* vf = p->vma[i].vfile;
  // 读导致的页面错误
  if(cause == 13 && vf->readable == 0) return -1;
  // 写导致的页面错误
  if(cause == 15 && vf->writable == 0) return -1;

  void* pa = kalloc();
  if(pa == 0)
    return -1;
  memset(pa, 0, PGSIZE);

  // 读取文件内容
  ilock(vf->ip);
  // 计算当前页面读取文件的偏移量,实验中p->vma[i].offset总是0
  // 要按顺序读读取,例如内存页面A,B和文件块a,b
  // 则A读取a,B读取b,而不能A读取b,B读取a
  int offset = p->vma[i].offset + PGROUNDDOWN(va - p->vma[i].addr);
  int readbytes = readi(vf->ip, 0, (uint64)pa, offset, PGSIZE);
  // 什么都没有读到
  if(readbytes == 0) {
    iunlock(vf->ip);
    kfree(pa);
    return -1;
  }
  iunlock(vf->ip);

  // 添加页面映射
  if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, pte_flags) != 0) {
    kfree(pa);
    return -1;
  }

  return 0;
}

  1. 实现sys_munmap函数,该函数用于取消文件在内存中的映射,释放物理块并减少文件引用计数,若内存发生变动,则将数据写回文件, 本实验简化了流程,执行统一不看脏位标志就写回。具体步骤如下:
    (1)获取系统调用参数,int munmap(void* addr, int length);并对参数进行安全检查;
    (2)遍历VMA数组,找到对应的vma;根据提示,munmap的地址只能是在起始位置或结束位置。因此需要针对这两种情况分别计算,此步骤只是修改vma结构体的数据,根据释放后的addr和length重新设置vma.addr 和 vma.length。
    (3)将MAP_SHARED页面写回文件系统,使用filewrite函数写回文件;
    (4)调用uvmunmap取消该地址在页表中的映射,需要注意每次取消都是以页为单位的,所以需要对length计算块数,过程中要求释放物理内存;
    (5)如果vma.length == 0说明该文件在内存中的映射已经全部取消了,此时应该调用fileclose关闭文件(会减少文件引用计数,若为0才会真正关闭文件),重新设置vma状态为未使用。
uint64
sys_munmap(void) {
  uint64 addr;
  int length;
  if(argaddr(0, &addr) < 0 || argint(1, &length) < 0)
    return -1;

  int i;
  struct proc* p = myproc();
  for(i = 0; i < NVMA; ++i) {
    if(p->vma[i].used && p->vma[i].len >= length) {
      // 根据提示,munmap的地址范围只能是
      // 1. 起始位置
      if(p->vma[i].addr == addr) {
        p->vma[i].addr += length;
        p->vma[i].len -= length;
        break;
      }
      // 2. 结束位置
      if(addr + length == p->vma[i].addr + p->vma[i].len) {
        p->vma[i].len -= length;
        break;
      }
    }
  }
  if(i == NVMA)
    return -1;

  // 将MAP_SHARED页面写回文件系统
  if(p->vma[i].flags == MAP_SHARED && (p->vma[i].prot & PROT_WRITE) != 0) {
    filewrite(p->vma[i].vfile, addr, length);
  }

  // 判断此页面是否存在映射
  uvmunmap(p->pagetable, addr, length / PGSIZE, 1);


  // 当前VMA中全部映射都被取消
  if(p->vma[i].len == 0) {
    fileclose(p->vma[i].vfile);
    p->vma[i].used = 0;
  }

  return 0;
}

  1. 回忆lazy实验中,如果对惰性分配的页面调用了uvmunmap,或者子进程在fork中调用uvmcopy复制了父进程惰性分配的页面都会导致panic,因此需要修改uvmunmap和uvmcopy检查PTE_V后不再panic,
if((*pte & PTE_V) == 0)
  continue;

  1. 根据提示8修改exit,将进程的已映射区域取消映射。当进程执行exit退出时,需要将已经映射的MAP_SHARED文件写回,并关闭文件、取消映射、重置vma状态等。
void
exit(int status)
{
  // Close all open files.
  for(int fd = 0; fd < NOFILE; fd++){
    ...
  }

  // 将进程的已映射区域取消映射
  for(int i = 0; i < NVMA; ++i) {
    if(p->vma[i].used) {
      if(p->vma[i].flags == MAP_SHARED && (p->vma[i].prot & PROT_WRITE) != 0) {
        filewrite(p->vma[i].vfile, p->vma[i].addr, p->vma[i].len);
      }
      fileclose(p->vma[i].vfile);
      uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].len / PGSIZE, 1);
      p->vma[i].used = 0;
    }
  }

  begin_op();
  iput(p->cwd);
  end_op();
  ...
}

  1. 根据提示9,修改fork,复制父进程的VMA并增加文件引用计数。fork后子进程会复制父进程的VMA结构体,同时也就获得了内存映射文件的读写权限,因此需要增加文件的引用计数,防止父进程取消映射后关闭文件,导致子进程无法正常访问内存。
int
fork(void)
{
 // increment reference counts on open file descriptors.
  for(i = 0; i < NOFILE; i++)
    ...
  ...

  // 复制父进程的VMA
  for(i = 0; i < NVMA; ++i) {
    if(p->vma[i].used) {
      memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));
      filedup(p->vma[i].vfile);
    }
  }

  safestrcpy(np->name, p->name, sizeof(p->name));

  ...
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值