xv6 mmap

发布时间 2023-12-06 17:25:37作者: trashwin

in linux

调用mmap,会申请一段内存空间(文件的内存映射部分),并且自动映射到指定的文件内存映射部分。

mmap

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

addr为用户指定的内存起始地址,为0时由系统分配。
length制定映射的长度,单位是字节。
prot制定映射页面的权限

  • PROT_READ 映射内存可读
  • PROT_WRITE 映射内存可写
  • PROT_EXEC 映射内存可被执行
  • PROT_NONE 映射内存不可访问

flags控制映射内存修改后的行为

  • MAP_SHARED 进程共享映射内存,修改后会反映到文件中。精准控制修改时间需要用msyn。
  • MAP_PRIVATE 进程私有映射内存,修改后不会反映到文件中。Copy on write
  • MAP_FIXED 必须使用指定的映射内存的起始地址addr,页对齐。慎用,如果和其他映射内存重叠,会覆盖其他映射内存。
  • MAP_ANONYMOUS 映射匿名,初始化为0,不与文件关联即fd和offset无效,有些实现要求强制fd为-1,及时不强制程序中最好也用-1

fd映射文件的文件描述符
offset映射文件的偏移量,即从文件的什么位置开始映射,必须是页对齐的

成功返回0;失败返回-1即MAP_FAILED,并设置errno。

open系统调用的权限必须和mmap的prot权限符合,具体为

  • open必须可读
#define O_RDONLY  0x000
#define O_WRONLY  0x001
#define O_RDWR    0x002
  • mmap 指定了PROT_WRITE和MAP_SHARED,open必须RDWR
    进程终止内存映射需要单独调用munmap回收,映射完成后文件即可使用close,但实际并没有关闭,内存映射区域存在对文件的引用,导致并不会实际关闭。

使用场景

  • 共享文件映射,使用MAP_SHARED
  • 私有文件映射,使用MAP_PRIVATE,常用于加载动态共享库等程序
  • 共享匿名映射,使用MAP_ANONYMOUS|MAP_SHREAD,常用于进程间通信
  • 私有内存映射,用于分配较大的内存空间,供进程自身使用

munmap

int munmap(void *addr, size_t length);

指定范围内的所有页面都会被unmaped,成功返回0,失败返回-1并设置errno。除了程序内调用,进程终止时也会自动unmap。
不要求区域内页面是映射的,即使不包含任何映射页面,也不会出错。
一个例子

#include<stdio.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
    int *p;
    p=mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_ANONYMOUS|MAP_SHARED,-1,0);
    if(fork()==0){
        *p=100;
        printf("child: %d\n",*p);
        exit(0);
    }else{
        wait(NULL);
        printf("parent: %d\n",*p);
    }
    int err=munmap(p,4);
    printf("err: %d\n",err);
    err = munmap(p-4096,4);
    printf("err: %d\n",err);
}
// child: 100
// parent: 100
// err: 0
// err: 0

in xv6

不得不说,这个实验还是有难度的,做的时候有点面向测试用例编程的感觉了,但思路整体上应该没问题。

mmap

mmap的lab实现时,总是假设addr为0,offset为0,并且MAP_SHARED实际也可以不共享,只需要写回修改即可。
实现共享确实有点麻烦,需要考虑多个进程同时修改以及什么时候释放的问题,而且考虑到munmap可以释放部分区域,单个vma的引用计数也不足以解决问题,因此暂且不实现。一个简单的想法是在vma内同时维护原区域和实际区域,只有一个进程全部ummap时才递减引用计数,引用计数为0时才释放区域。
xv6里没有memory allocator,所以具体实现的时候简便即可,提供的方法是在进程内保存VMA数组。
具体vma里需要包含什么,其实文档说的很清楚了。

struct vma{
  uint64 start;
  uint64 len;
  uint64 offset;
  int prot;
  int flags;
  struct file *file;
};

mmap完成一下操作

  • 检查权限是否匹配,这点文档没说,对着测试代码看的
  • 找到合适的内存空间,为了避免和p->sz混淆带来的麻烦(需要修改很多函数,改变函数语义),选择从高地址TRAPFRAME下开始。并且维护一个vmastart作为内存中mmap分配的最低位置,uvmunmap时简单更新为现存映射空间最低位置。
  • 找到进程中空闲的vma项,将信息存入。

sys_mmap的helper函数,用于检查权限是否匹配。

uint64 mmap(uint64 addr, uint64 len, int prot, int flags, int fd, uint64 offset){
  int i;
  struct vma* vmamems;
  struct proc* p=myproc();
  struct file* file = p->ofile[fd];
  
  if(file == 0){
    return -1;
  }
  if((prot & PROT_READ) && file->readable == 0){
    return -1;
  }
  if((prot & PROT_WRITE) && p->ofile[fd]->writable == 0 && !(flags & MAP_PRIVATE)){
    return -1;
  }
  len = PGROUNDUP(len);
  if(addr == 0){
    addr = p->vmastart - len;
  }
  vmamems=p->vmamems;

  for(i=0;i<NVMA;i++){
    if(vmamems[i].file == 0){
      break;
    }
  }

  if(i >= NVMA){
    return -1;
  }

  vmamems[i].start=addr;
  vmamems[i].len=len;
  vmamems[i].offset=offset;
  vmamems[i].prot=prot;
  vmamems[i].flags=flags;
  vmamems[i].file=p->ofile[fd];
  p->vmastart = addr;
  filedup(vmamems[i].file);
  return addr;
}

munmap

在进程vma数组中找到对应项,低配版直接对比addr(实验保证了不会从中间进行ummap)。
完成写回文件和删除分配的物理内存,最后更改vma记录。
由于xv6日志一次最多修改MAXOPBLOCKS个块,得到的每次系统调用写入最多为3*BSIZE(为了省事我直接用PGSIZE了),因此需要循环写回(总感觉在本次lab下有些多余,但要写出能用的代码确实要这样,然而代码建立在不正确的假设上QAQ)。
sys_munmap的helper函数。

int munmap(uint64 addr, uint64 len){
  int i;
  struct proc * p = myproc();
  struct vma* vmamems = p->vmamems;
  for(i=0;i<NVMA;i++){
    if(vmamems[i].file && vmamems[i].start == addr){
      if(len > vmamems[i].len){
        return -1;
      }
      uint64 offset = vmamems[i].offset;
      uint64 finished = 0;
      pte_t *pte;
      while(finished < len){
        if((pte = walk(p->pagetable, addr + finished, 0)) == 0){
          return -1;
        }
        if(*pte & PTE_V)
        {
          uint64 r; 
          if((vmamems[i].flags & MAP_SHARED) &&*pte & PTE_D){
            // 以下过程参考的file.c,虽然不知道这样判断的意义是什么
            begin_op();
            ilock(vmamems[i].file->ip);
            if((r=writei(vmamems[i].file->ip, 1, addr + finished, offset, PGSIZE))>0){
              offset += r;
            }
            iunlock(vmamems[i].file->ip);
            end_op();
            if(r != PGSIZE){
              break;
            }
          }
          uvmunmap(p->pagetable, addr + finished, 1, 1);
        }
        finished+=PGSIZE;
      }
      if(finished != len){
        return -1;
      }
      vmamems[i].start += len;
      vmamems[i].len -= len;
      vmamems[i].offset = offset;
      if(vmamems[i].len == 0){
        fileclose(vmamems[i].file);
        vmamems[i].file=0;
      }
      break;
    }
  }
  if(i >= NVMA){
    return -1;
  }
  uint64 vmastart = TRAPFRAME;
  for(int i=0;i<NVMA;i++){
    if(vmamems[i].file != 0){
      vmastart = (vmastart > vmamems[i].start) ? vmamems[i].start : vmastart;
    }
  }
  p->vmastart = vmastart;
  return 0;
}

缺页处理程序,有上次写COW的经验这次写起来就很顺手了。

int MapHandler(uint64 addr){
  struct proc *p = myproc();
  struct vma* vmamems = p->vmamems;
  uint64 va;
  
  for(;vmamems < p->vmamems + NVMA; vmamems++){
    if(vmamems->file && addr >= vmamems->start && addr < vmamems->start + vmamems->len){
      break;
    }
  }
  if(vmamems == NVMA + p->vmamems){
    return -1;
  }
  uint64 mem = (uint64)kalloc();
  if(mem == 0){
    return -1;
  }
  memset((void*)mem, 0, PGSIZE);
  va = PGROUNDDOWN(addr);
  ilock(vmamems->file->ip);
  readi(vmamems->file->ip, 0, mem, vmamems->offset + (va - vmamems->start), PGSIZE);
  iunlock(vmamems->file->ip);
  int flags = PTE_U;
  if(vmamems->prot & PROT_WRITE)
    flags |= PTE_W;
  if(vmamems->prot & PROT_READ)
    flags |= PTE_R;
  if(vmamems->prot & PROT_EXEC)
    flags |= PTE_X;
  if(mappages(p->pagetable, va, PGSIZE, mem, flags) != 0){
    kfree((void*)mem);
    return -1;
  }
  return 0;
}

exit

  for(int i=0;i<NVMA;i++){
    if(p->vmamems[i].file){
      munmap(p->vmamems[i].start, p->vmamems[i].len);
    }
  }

fork

  for(int i=0;i<NVMA;i++){
    np->vmamems[i] = p->vmamems[i];
    if(p->vmamems[i].file){
      filedup(p->vmamems[i].file);
    }
  }