2020 6.s081——Lab10:Mmap完成记录

解开我

最神秘的等待

星星坠落

风在吹动

——美丽的神话

 完整代码见:6.s081/kernel at mmap · SnowLegend-star/6.s081 (github.com)

这个lab其实是结合了COW、Lazy allocation还有fs三个实验为一体。虽然思维难度不像一般的hard那么大,但胜在代码量极多,需要修改的地方也纷繁复杂,故我完成得磕磕绊绊。由于这个lab的主体部分是重新完成mmap()函数,所以先看看标准库里面mmap()的分析。


头文件:

<unistd.h>

<sys/mman.h>

原型: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offsize);

返回值: 成功则返回映射区起始地址, 失败则返回MAP_FAILED(-1).

参数:

addr: 指定映射的起始地址, 通常设为NULL, 由系统指定.

length: 将文件的多大长度映射到内存.

prot: 映射区的保护方式, 可以是:

PROT_EXEC: 映射区可被执行.

PROT_READ: 映射区可被读取.

PROT_WRITE: 映射区可被写入.

PROT_NONE: 映射区不能存取.

flags: 映射区的特性, 可以是:

MAP_SHARED: 对映射区域的写入数据会复制回文件, 且允许其他映射该文件的进程共享.

MAP_PRIVATE: 对映射区域的写入操作会产生一个映射的复制(copy-on-write), 对此区域所做的修改不会写回原文件.

此外还有其他几个flags不很常用, 具体查看linux C函数说明.

fd: 由open返回的文件描述符, 代表要映射的文件.

offset: 以文件开始处的偏移量, 必须是分页大小的整数倍, 通常为0, 表示从文件头开始映射.

内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,相反,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<---->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。 


了解mmap()的工作原理之后,仔细阅读实验说明。

可以通过多种方式调用mmap,但本实验只需要与内存映射文件相关的功能子集。您可以假设addr始终为零,这意味着内核应该决定映射文件的虚拟地址。mmap返回该地址,如果失败则返回0xffffffffffffffff。length是要映射的字节数;它可能与文件的长度不同。prot指示内存是否应映射为可读、可写,以及/或者可执行的;您可以认为prot是PROT_READ或PROT_WRITE或两者兼有。flags要么是MAP_SHARED(映射内存的修改应写回文件),要么是MAP_PRIVATE(映射内存的修改不应写回文件)。您不必在flags中实现任何其他位。fd是要映射的文件的打开文件描述符。可以假定offset为零(它是要映射的文件的起点)。

允许进程映射同一个MAP_SHARED文件而不共享物理页面

这是什么意思呢?

问了下GPT,回答如下

You

“允许进程映射同一个MAP_SHARED文件而不共享物理页面。”这是什么意思呢

ChatGPT

这句话的意思是,即使多个进程都映射了同一个文件,并且都使用了 MAP_SHARED 标志,但操作系统并不会强制这些进程共享同一个物理页面。换句话说,每个进程会得到自己的一组物理页面来映射这个文件,而不是所有进程共享同一个物理页面。

这种情况可能会发生在操作系统对 MAP_SHARED 映射的处理方式上,通常情况下,操作系统会在内核中维护一张页面表,将不同进程的虚拟地址映射到相同的物理页面上,以实现对文件的共享访问。然而,如果操作系统允许进程映射同一个 MAP_SHARED 文件而不共享物理页面,那么即使多个进程都对文件进行了映射,并且使用了 MAP_SHARED 标志,它们在内存中的映射可能是相互独立的,各自占用不同的物理页面。

这种情况可能会增加系统的内存开销,因为每个进程都会分配自己的物理页面来映射同一个文件,但也有助于提高系统的安全性,因为各个进程之间不会共享物理页面,从而避免了可能的安全隐患。

munmap(addr, length)应删除指定地址范围内的mmap映射。如果进程修改了内存并将其映射为MAP_SHARED,则应首先将修改写入文件。munmap调用可能只覆盖mmap区域的一部分,但您可以认为它取消映射的位置要么在区域起始位置,要么在区域结束位置,要么就是整个区域(但不会在区域中间“打洞”)。

[!TIP|label:YOUR JOB] 您应该实现足够的mmap和munmap功能,以使mmaptest测试程序正常工作。如果mmaptest不会用到某个mmap的特性,则不需要实现该特性。


Hints:

1、首先,向UPROGS添加_mmaptest,以及mmap和munmap系统调用,以便让user/mmaptest.c进行编译。现在,只需从mmap和munmap返回错误。我们在kernel/fcntl.h中为您定义了PROT_READ等。运行mmaptest,它将在第一次mmap调用时失败。

要手动给用户空间添加mmap和munmap的系统调用吗?对的,就像Lab: fs一样。

2、惰性地填写页表,以响应页错误。也就是说,mmap不应该分配物理内存或读取文件。相反,在usertrap中(或由usertrap调用)的页面错误处理代码中执行此操作,就像在lazy page allocation实验中一样。惰性分配的原因是确保大文件的mmap是快速的,并且比物理内存大的文件的mmap是可能的

处理完trap.c函数后,对uvmcopy和uvmunmap进行painc的修改。这里对usertrap()的修改可以参照Lazy allocation,流程大致如下:

  1. 获取出错的addr
  2. 判断addr的合法性
  3. 找到出错addr属于哪块VMA元素
  4. 分配pa并设置perm
  5. 进行addr和pa的映射
  6. 用readi将映射的文件读入pa中(记得加锁操作)

这里要注意是用readi将文件读入pa中而不是addr,否则会出现以下报错

3、跟踪mmap为每个进程映射的内容。定义与第15课中描述的VMA(虚拟内存区域)对应的结构体,记录mmap创建的虚拟内存范围的地址、长度、权限、文件等。由于xv6内核中没有内存分配器,因此可以声明一个固定大小的VMA数组,并根据需要从该数组进行分配。大小为16应该就足够了。

在什么地方声明这个数组呢?就在proc.h中进行声明。

但是怎么记录这个文件呢?用argfd的方式把读入的fd转换为struct file格式。

一共有64个进程,却只有16个VMA数组,说明不是每个进程都会调用mmap进行映射。不是用含有16个元素的VMA数组来管理所有进程,而是给每个进程都分配一个VMA数组,故每个进程都可以打开的文件上限是16。当前进程结束后记得重置VMA元素

4、实现mmap:在进程的地址空间中找到一个未使用的区域来映射文件,并将VMA添加到进程的映射区域表中。VMA应该包含指向映射文件对应struct file的指针mmap应该增加文件的引用计数,以便在文件关闭时结构体不会消失(提示:请参阅filedup)。运行mmaptest:第一次mmap应该成功,但是第一次访问被mmap的内存将导致页面错误并终止mmaptest

该怎么从进程的地址空间中找一块未使用过的addr呢?遍历pagetable也许可行。也可以直接在进程的地址空间内位于p->sz的位置开始映射,但是如果进程进行了sbrk()会导致下一个VMA元素和当前VMA元素存储的位置不是连续的(从实现角度上没有问题)。

一个新的想法是从虚拟地址的高处开始映射,例如从trapframe-10*PAGESIZE开始映射,但这样会出现bug。

可能是高地址空间位于内核,用户空间不允许访问内核数据。

5、添加代码以导致在mmap的区域中产生页面错误,从而分配一页物理内存,将4096字节的相关文件读入该页面,并将其映射到用户地址空间。使用readi读取文件,它接受一个偏移量参数,在该偏移处读取文件(但必须lock/unlock传递给readi的索引结点)。不要忘记在页面上正确设置权限。运行mmaptest;它应该到达第一个munmap

这里也提到了在usertrap()中,是用readi读入文件内容到物理页面中,同时读入的字节大小为4096(即PGSIZE)。

6、实现munmap:找到地址范围的VMA并取消映射指定页面(提示:使用uvmunmap)。如果munmap删除了先前mmap所有页面,它应该减少相应struct file的引用计数。如果未映射的页面已被修改,并且文件已映射到MAP_SHARED,请将页面写回该文件。查看filewrite以获得灵感。

在munmap()的实现中,只有文件的flags是MAP_SHARED才需要重新写入到文件中,让所有共享这个文件的进程都能看到修改。MAP_SHARED类型的文件则不需要。同时,这个文件本身应该是writeable的。

写回文件的过程可以模仿file.c中的filewrite()。

7、理想情况下,您的实现将只写回程序实际修改的MAP_SHARED页面。RISC-V PTE中的脏位(D)表示是否已写入页面。但是,mmaptest不检查非脏页是否没有回写;因此,您可以不用看D位就写回页面

意思是只要修改了就直接写回,其实可以忽略这个hint。

8、修改exit将进程的已映射区域取消映射,就像调用了munmap一样。运行mmaptest;mmap_test应该通过,但可能不会通过fork_test。

freeproc不是会进行取消映射的处理吗?这里就是在exit()中额外增加对进程VMA数组的释放过程。

9、修改fork以确保子对象具有与父对象相同的映射区域。不要忘记增加VMA的struct file的引用计数。在子进程的页面错误处理程序中,可以分配新的物理页面,而不是与父级共享页面。后者会更酷,但需要更多的实施工作。运行mmaptest;它应该通过mmap_test和fork_test

是不是和Lab: Lazy alloctaion差不多?naive方案是直接把父进程的VMA数组复制给子进程的VMA。Challenge部分才和Lazy allocation差不多。

proc.h

#define VMASIZE 16
struct VMA{
  int valid;      //当前VMA的元素是否被使用
  uint64 addr;
  int length;
  int prot;
  int flags;
  int fd;
  int offset;
  struct file *f;
};

sys_mmap()

uint64
sys_mmap(){
  int fd, length, prot, flags, offset;
  uint64 addr;
  struct file *f;
  struct proc *p=myproc();
  int i;

  //读入mmap需要的六个参数
  if(argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0 ||
      argint(3, &flags) < 0 || argfd(4, &fd, &f) < 0 || argint(5, &offset) < 0)
      return -1;

  // f=p->ofile[fd];
  if( !(f->writable) && (prot & PROT_WRITE) && (flags == MAP_SHARED))
    return -1;


  for(i=0; i < VMASIZE; i++){
    if(p->VMA[i].valid==0){                       //默认值0代表可用
      // p->VMA[i].addr=MMAPADDR + PGSIZE*i*3;         //把第i个VMA元素映射到对应的位置
      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].offset=offset;
      p->VMA[i].f=filedup(f);
      p->VMA[i].valid=1;
      break;
    }
  }
  if(i == VMASIZE)    //当前进程没有空闲的VMA元素了
    return -1;
  p->sz+=length;
  return p->VMA[i].addr;
}

 sys_munmap()

uint64
sys_munmap(){
  uint64 addr;
  int length;

  if(argaddr(0, &addr) < 0 || argint(1, &length) < 0)
    return -1;

  if(addr%PGSIZE || length<0 )
    return -1;

  return (uint64)munmap_Real(addr, length);
}

由于 exit()也要用到和原始munmap()相同的部分,故抽象出函数munmap_Real()

int munmap_Real(uint64 addr, int length){
  int i;
  struct VMA *vma=0;
  struct proc *p=myproc();

    //寻找对应的VMA
  for(i=0; i<VMASIZE; i++){
    vma=&p->VMA[i];
    if(p->VMA[i].valid==1 && addr>= p->VMA[i].addr && addr+length < p->VMA[i].addr + p->VMA[i].length){

      break;
    }
    // vma = p->VMA + i;
    // if (vma->valid == 1 && addr >= vma->addr && (addr + length) < (vma->addr + vma->length)) {
    //   break;
    // }
  }
  if(i>VMASIZE)
    return -1;

  //将数据写回shared类型的文件中
  uint64 begin=addr;
  uint64 end=addr+length;
  if(vma->flags==MAP_SHARED && vma->f->writable){
    uint64 addr_Cur=begin;
    while(addr_Cur < end){
      int sz=end-addr_Cur>=PGSIZE?PGSIZE:end-addr_Cur; //这里把sz的大小限制为PGSIZE是为了好调用uvmunmap
      begin_op();
      ilock(vma->f->ip);
      if(writei(vma->f->ip, 1, addr_Cur, addr_Cur-vma->addr,sz) != sz)
        return -1;
      iunlock(vma->f->ip);
      end_op();
      uvmunmap(p->pagetable, addr_Cur,1,1);
      addr_Cur+=PGSIZE;
    }
  }

  //写回结束后,更新vma的内容
  if(addr==vma->addr){   //说明addr是mmap的头部
    vma->addr+=length;
    vma->length-=length;
  }
  else if(addr+length==vma->addr+vma->length){  //说明释放了vma的尾部
    vma->length-=length;
  }

  //如果vma全部被释放,则要释放对file的引用
  if(vma->length ==0 && vma->valid==1){
    // fileclose(vma->f);
    filedup(vma->f);
    vma->valid=0;
    vma->addr=0;
    vma->f=0;
    vma->fd=-1;
    vma->flags=0;
    vma->length=0;
    vma->offset=0;
    vma->prot=0;
  }
  return 0;
}

usertrap()的修改如下

  else if(r_scause() == 13 || r_scause()==15){
    uint64 pa;    
    uint64 addr=PGROUNDDOWN(r_stval());
    struct VMA *vma=0;
    int i;

    if(addr >= p->sz || addr < PGROUNDDOWN(p->trapframe->sp))
      exit(-1);

    //找到出错的VMA
    for(i=0;i<VMASIZE;i++)
      if(p->VMA[i].addr && p->VMA[i].addr <= addr && p->VMA[i].addr + p->VMA[i].length >addr){
        vma=&p->VMA[i];
      }
    if(i>VMASIZE)  //没有找到合适的VMA元素,直接退出
      exit(-1);

    //找到了vma,说明是mmap导致的缺页错误
    if((pa=(uint64)kalloc())==0)
      exit(-1);
    memset((char*)pa, 0, PGSIZE);

  int perm = PTE_U;
  if(vma->prot & PROT_READ)
    perm |= PTE_R;
  if(vma->prot & PROT_WRITE)
    perm |= PTE_W;
  if(vma->prot & PROT_EXEC)
    perm |= PTE_X;

  if((mappages(p->pagetable, addr, PGSIZE, (uint64) pa, perm)) < 0){
    exit(-1);
  }

  //将映射的文件读入pa中
  ilock(vma->f->ip);
  readi(vma->f->ip, 0, pa, addr - vma->addr, PGSIZE);
  iunlock(vma->f->ip);
  }

 exit()

    ......
 p->ofile[fd] = 0;
    }
  }

  //释放进程的VMA
  for(int i=0; i<VMASIZE; i++){
    if(p->VMA[i].valid==0)
      continue;
    if(munmap_Real(p->VMA[i].addr, p->VMA[i].length)!=0){
      panic("Something wrong with munmap when exit.");
    }
  }

  begin_op();
  iput(p->cwd);

fork()

  // increment reference counts on open file descriptors.
  for(i = 0; i < NOFILE; i++)
    if(p->ofile[i])
      np->ofile[i] = filedup(p->ofile[i]);
  np->cwd = idup(p->cwd);

  //拷贝父进程的VMA给子进程
  for(int i=0; i<VMASIZE; i++){
    if(p->VMA[i].valid==1){
      memmove(&np->VMA[i], &p->VMA[i], sizeof(struct VMA));
      filedup(np->VMA[i].f);
    }
  }

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

  pid = np->pid;
  np->state = RUNNABLE;

在完成lab的过程中还碰到了其他bug,例如

熟悉的stacktest error,猜测是没判断addr的合法性(即addr下溢到了guard page)。修改下判断即可通过。

下面这段玄学代码也带了了几个令人困惑的bug。

说实话这次lab虽然巨麻烦,不过测试条件是真的宽松。甚至在munmap()中把fileclose()写成了filedup()还是通过了测试。总而言之,涉及fs的lab内容实在是多,看得人头大。

  • 21
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值