6.S081/6.828: 10 Lab mmap

2023-02-18 20:11:42 浏览数 (1)

1 目的

本实验实现mmap和munmap系统调用来更好的控制进程地址空间,可以向数组那样读写文件,写的数据放在buffer cache可以被其他进程所看到。

2 问题分析

代码语言:c复制
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);

实现内存映射机制需要维护用户地址和文件偏移量的映射关系vma,为了节省空间,采用lazy allocate方式分配,通过page fault来分配物理页。munmap时只能是从左边界开始或者在右边界结束,不能unmap中间部分。

  1. lazy分配,通过page fault来分配物理页,使得物理页少于文件大小时也能够可用;
  2. 定义数据结构VMA来追踪每个进程的mmap,VMA应该包含file;
  3. 增加代码来导致mmaped区域触发page fault,即创建页表项但不分配物理页;page fault时读取4KB文件内容填充物理页并映射到用户空间,通过readi进行读取;
  4. munmap时寻找VMA并unmap这个区域,如果unmap掉mmap映射的所有页,那么就减少对文件的引用。如果unmapped page被修改过且MAP_SHARED,filewrite写回文件;
  5. 修改exit来unmap进程内存映射区域,修改fork来确保子进程会复制mapped区域,需要对文件增加引用;

3 代码实现

3.1 接口定义

MAP_SHARED是进程共享,会将修改的数据写回文件,MAP_PRIVATE不会刷回文件,只在内存中临时保存。PROT_*是读写权限,在分配物理页时会根据这个来设置PTE的flag。vma是维护进程空间和文件偏移的映射关系的,每个进程都有一个vma数组来维护。

代码语言:c复制
void* mmap(void *addr,int len,int prot,int flags,int fd,int offset);
int munmap(void *addr,int len);

//fcntl.h
#ifdef LAB_MMAP
#define PROT_NONE       0x0
#define PROT_READ       0x1
#define PROT_WRITE      0x2
#define PROT_EXEC       0x4

#define MAP_SHARED      0x01 //刷盘使得其他进程能够看到
#define MAP_PRIVATE     0x02 //修改的数据不刷盘
#endif

//proc.h
struct vma{
  int valid;
  uint64 addr; //user space addr
  uint len; //按页向上取整
  struct file *f;
  uint offset;
  uint flags; //MAP_PRIVATE: 不会更新到磁盘 MAP_SHARED: 会刷盘
  uint prot; //访问权限
};
struct proc{
  //...
  struct vma pvma[MAXVMAPERPROC];
}

然后在user/user.h添加接口;

代码语言:c复制
void* mmap(void *addr,int len,int prot,int flags,int fd,int offset);
int munmap(void *addr,int len);

再在usys.pl添加entry;

代码语言:c复制
entry("mmap");
entry("munmap");

最后添加到系统调用表中。

代码语言:c复制
//syscall.h
#define SYS_mmap   22
#define SYS_munmap  23

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

[SYS_mmap]   sys_mmap,
[SYS_munmap]   sys_munmap,

3.2 sys_mmap

mmap实现放在sysfile.c中,首先校验参数,如果在MAP_SHARED且文件不可写且mmap可写,那么就error,因为文件可能也同时被其他进程读,会有影响。然后从进程的pvma数组中选择一个空槽位来分配,此时只需要移动sz,不需要真分配物理内存。

代码语言:c复制
uint64
sys_mmap(void){
  uint64 addr;
  int len,prot,flags,fd,offset;
  if(argaddr(0,&addr)<0 || argint(1,&len)<0 || argint(2,&prot)<0 
     || argint(3,&flags)<0 || argint(4,&fd)<0 || argint(5,&offset)<0 )
    return -1;
  struct proc *p=myproc();
  struct file *f=p->ofile[fd];
  if(f==0)
    return -1;
  if((flags&MAP_SHARED) && !f->writable && (prot & PROT_WRITE))
    return -1;
  //addr默认是0,由内核决定映射到进程空间的位置
  struct vma *pvma=p->pvma;
  for(int i=0;i< MAXVMAPERPROC;i  ){
    if(!pvma[i].valid){
      // printf("mmap: file.ref[%d]n",f->ref);
      pvma[i].f=filedup(f);
      pvma[i].addr=p->sz;
      pvma[i].len=PGROUNDUP(len);
      pvma[i].prot=prot;
      pvma[i].flags=flags;
      pvma[i].offset=offset;
      pvma[i].valid=1;
      p->sz =len;
      // printf("mmap: ref[%d], inode.ref[%d]n",f->ref,f->ip->ref);
      return pvma[i].addr;
    }
  } 
  return -1;
}

3.3 page fault handler

mmap时是lazy allocate,读写时会发生缺页中断,需要处理。先从stval寄存器中获取缺页地址,然后扫描proc.pvma数组,找到该地址所在的vma;然后根据分配一页内存并根据相对偏移量从文件指定位置读取一页;最后将这一页map到用户页表上。

代码语言:c复制
//trap.c
//...
} else if((which_dev = devintr()) != 0){
    // ok
  } else if(r_scause()==13 || r_scause()==15){
    uint64 va=r_stval();
    if(mmap_pgfaulthandler(va)!=0){
      printf("usertrap(): unexpected scause %p pid=%dn", r_scause(), p->pid);
      printf("            sepc=%p stval=%pn", r_sepc(), r_stval());
      p->killed = 1;
    }
  }
//...

int vm_exists(pagetable_t pagetable, uint64 va){
  pte_t *pte;
  return (pte=walk(pagetable,va,0)) && (*pte & PTE_V);
}
int mmap_pgfaulthandler(uint64 va){
  va=PGROUNDDOWN(va);
  struct vma *a=0;
  //缺页处理
  struct proc *p=myproc();
  struct vma *pvma=p->pvma;
  for(int i=0;i< MAXVMAPERPROC;i  ){
    if(p->pvma[i].valid && va>=pvma[i].addr && va<pvma[i].addr pvma[i].len){
      a=&pvma[i];
      break;
    }
  }       
  if(a==0)
    return -1;

  uint64 pa=(uint64)kalloc();
  if(pa==0)
    return -1;
  memset((void*)pa,0,PGSIZE);
  int flag=PTE_U;
  flag|=a->prot & PROT_READ ? PTE_R:0;
  flag|=a->prot & PROT_WRITE ? PTE_W:0;
  if(mappages(p->pagetable,va,PGSIZE,pa,flag)!=0){
    kfree((void*)pa);
    return -1;
  }
  ilock(a->f->ip);
  printf("trap: va[%p]n",va);
  if(readi(a->f->ip,0,pa,a->offset va-a->addr,PGSIZE)<=0){
    iunlock(a->f->ip);
    return -1;
  }
  iunlock(a->f->ip);
  return 0;  
}

3.4 sys_munmap

munmap会释放掉映射的内存,如果是MAP_SHARED还会写回文件中。先从pvma数组找到释放的vma,并决定需要释放左边、右边还是全部,释放左边需要移动vma.addr和vma.offset,释放右边减少vma.len即可;然后逐页判断PTE是否有效,有且MAP_SHARED则释放并写回文件;然后uvmunmap页表项映射;最后,如果整个释放,则关闭文件并将vma.valid=0。

代码语言:c复制
//sysfile.c
uint64
sys_munmap(void){
  uint64 addr;
  int len;
  if(argaddr(0,&addr)<0 || argint(1,&len)<0)
    return -1;
  return munmap(addr,len);
}

//vm.c
int munmap(uint64 addr,int length){

  struct proc *p = myproc();
  struct vma *a = 0;
  addr = PGROUNDDOWN(addr);

  for(int i = 0; i < MAXVMAPERPROC; i  ){
    if(p->pvma[i].valid && addr >= p->pvma[i].addr && addr < p->pvma[i].addr p->pvma[i].len){
      a = &p->pvma[i];
      break;
    }
  }

  if (a == 0) return -1;

  uint64 unstart, unlen;
  uint64 start = a->addr, offset = a->offset, orilen = a->len;

  if(addr == a->addr){
    // Unmap at the start
    unstart = addr;
    unlen = PGROUNDUP(length) < a->len ? PGROUNDUP(length) : a->len;

    a->addr = unstart   unlen; 
    a->len = start orilen - a->addr;
    a->offset = a->offset   unlen;
  } else if(addr   length >= start orilen){
    // Unmap at the end
    unstart = start;
    unlen = start orilen - unstart;

    a->len = unstart-start;
  } else{
    // Unmap the whole region
    unstart = start;
    unlen = orilen;
  }
  
  for(int i = 0; i < unlen / PGSIZE; i  ){
    uint64 va = unstart   i * PGSIZE;
    // May not be alloced due to lazy alloc through page fault.
    if(vm_exists(p->pagetable, va)){
      if(a->flags & MAP_SHARED){
        munmap_writeback(va, PGSIZE, start, offset, a);
      }

      uvmunmap(p->pagetable, va, 1, 1);
    }
  }

  if(unlen == orilen){
    fileclose(a->f);
    a->valid = 0;
  }
  
  return 0;
} 

因为采用了lazy allocate,uvmunmap也需要稍作修改;

代码语言:c复制
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
  uint64 a;
  pte_t *pte;

  if((va % PGSIZE) != 0)
    panic("uvmunmap: not aligned");

  for(a = va; a < va   npages*PGSIZE; a  = PGSIZE){
    if((pte = walk(pagetable, a, 0)) == 0)
      continue; //修改处
    if((*pte & PTE_V) == 0)
      continue;
    if(PTE_FLAGS(*pte) == PTE_V)
      continue;
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    *pte = 0;
  }
}

释放映射区域需要将数据写回文件,参照filewrite中的逻辑,通过log和inode层的函数写回。

代码语言:c复制
int
munmap_writeback(uint64 unstart, uint64 unlen, uint64 start, uint64 offset, struct vma *a)
{
  struct file *f = a->f;
  uint off = unstart - start   offset;
  uint size;

  ilock(f->ip);
  size = f->ip->size;
  iunlock(f->ip);

  if(off >= size) return -1;

  uint n = unlen < size - off ? unlen : size - off;

  int r, ret = 0;
  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);
    r = writei(f->ip, 1, unstart, off   i, n1);
    iunlock(f->ip);
    end_op();

    if(r != n1){
      // error from writei
      break;
    }
    i  = r;
  }
  ret = (i == n ? n : -1);

  return ret;
}

3.5 fork、exit

fork时需要拷贝vma数组,释放时也要重置。

代码语言:c复制
//proc.c
//fork()
for(int i=0;i< MAXVMAPERPROC;i  ){
    if(p->pvma[i].valid){
      np->pvma[i]=p->pvma[i];
      filedup(np->pvma[i].f);
    }
  } 

//exit()
//释放mmap region
  for(int i=0;i< MAXVMAPERPROC;i  ){
    if(p->pvma[i].valid){
      munmap(p->pvma[i].addr,p->pvma[i].len);
      p->pvma[i].valid=0;
    }
  } 

4 测试结果

测试结果测试结果

0 人点赞