MIT6.s081 lab10 mmap 总结

时间及结果

总耗时: 6h27min

解析

系统调用原型

本次lab要求实现mmap和munmap系统调用,系统调用原型如下:

// xv6中没有定义size_t和off_t,故修改为uint64
void *mmap(void *addr, uint64 length, int prot, int flags,
           int fd, uint64 offset);
int munmap(void *addr, uint64 length);
// addr: 指定的虚拟地址,此lab中默认为0,即由内核决定映射的虚拟地址;
// length: 映射的字节数;
// prot: 可选 PROT_READ PROT_WRITE,用以表明映射部分的读写权限;
// flags: MAP_SHARED表示映射内存的修改要写回文件,MAP_PRIVATE则无需写回;
// fd: 要映射的文件;
// offset: 文件的偏移量

// mmap系统调用将文件fd从offset开始的length字节映射到虚拟地址addr处
// munmap系统调用取消虚拟地址addr开始的length字节的文件映射

思路

首先需要定义VMA数据结构来保存一段文件映射的相关信息:

// kernel/proc.h
#define MAXMMAP 16
struct VMA{
  uint64 addr;
  uint64 length;
  int prot;
  int flags;
  struct file *f;
  uint64 offset;
  int used; // 1 说明该VMA在使用中;0 说明空闲
  struct spinlock lock;
};
// 在struct proc中新增VMA数组,支持MAXMMAP个元素
// struct VMA proc_VMA[MAXMMAP];

该数据结构的初始化时机和释放时机与进程proc结构的其它项相同:

// kernel/proc.c: allocproc()
for(int i = 0; i < MAXMMAP; i++) {
  p->proc_VMA[i].addr = 0;
  p->proc_VMA[i].f = 0;
  p->proc_VMA[i].flags = 0;
  p->proc_VMA[i].length = 0;
  p->proc_VMA[i].offset = 0;
  p->proc_VMA[i].prot = 0;
  p->proc_VMA[i].used = 0;
}

// kernel/proc.c: freeproc()
for (int i = 0; i < MAXMMAP; i++) {
  p->proc_VMA[i].addr = 0;
  p->proc_VMA[i].f = 0;
  p->proc_VMA[i].flags = 0;
  p->proc_VMA[i].length = 0;
  p->proc_VMA[i].offset = 0;
  p->proc_VMA[i].prot = 0;
  p->proc_VMA[i].used = 0;
}

添加系统调用的过程可以参照前面的lab,此处只记录系统调用的实现。

对于mmap系统调用,可以参考lazy allocation中的sbrk系统调用,仅增加myproc()->sz,不实际分配物理内存,而是在缺页故障的处理中分配物理页。

此处实现的mmap系统调用是直接从堆区获取虚拟内存来存放文件映射部分,而没有像linux那样单独划给文件映射区一片内存。

// kernel/sysfile.c
uint64 sys_mmap(void) {
  uint64 addr, length, offset;
  int prot, flags, fd, i;
  struct proc *p = myproc();
  struct file *f;
  // 获取系统调用参数
  if(argaddr(0, &addr) < 0 || argaddr(1, &length) < 0 || argaddr(5, &offset) < 0) {
    return -1;
  }
  if(argint(2, &prot) < 0 || argint(3, &flags) < 0 || argint(4, &fd) < 0) {
    return -1;
  }
  if(addr != 0) {
    panic("sys_mmap: addr just can be 0 in my mmap");
  }

  acquire(&p->lock);
  // 如果flags指定映射区的修改要写回文件
  // 那么该文件的读写权限和prot参数指定的读写权限必须相容
  if(flags & MAP_SHARED) {
    if(p->ofile[fd]->readable == 0 && (prot & PROT_READ)) {
      release(&p->lock);
      return -1;
    }
    if(p->ofile[fd]->writable == 0 && (prot & PROT_WRITE)) {
      release(&p->lock);
      return -1;
    }
  }

  // 仅增加p->sz,等缺页故障时再实际分配物理页
  addr = p->sz;
  p->sz += length;
  f = filedup(p->ofile[fd]);  // 注意要增加文件的引用

  // 找到一个空闲VMA用来存放文件映射的相关信息
  for(i = 0; i < MAXMMAP; i++) {
    if(p->proc_VMA[i].used == 0) {
      p->proc_VMA[i].addr = addr;
      p->proc_VMA[i].f = f;
      p->proc_VMA[i].flags = flags;
      p->proc_VMA[i].length = length;
      p->proc_VMA[i].offset = offset;
      p->proc_VMA[i].prot = prot;
      p->proc_VMA[i].used = 1;
      break;
    }
  }
  if(i == MAXMMAP) {
    panic("no free VMA for mmap");
  }
  release(&p->lock);
  return addr;
}

接下来需要考虑在缺页故障中为文件映射实际分配物理内存,定义isMmap()函数用来判断一个地址是否是文件映射的页,定义handleMmap()函数为一个地址实际分配物理页并写入相关文件数据

// kernel/trap.c
/**
 * @description: 判断传入的地址是否是文件映射的地址
 * @param {uint64} addr 虚拟地址
 * @return {*} 返回VMA编号,可以从myproc()->proc_VMA中获取相应VMA
 *             返回-1说明该地址不是文件映射的地址
 */
int isMmap(uint64 addr) {
  struct proc *p = myproc();
  uint64 begin, end;
  // 遍历当前进程的每个使用中的VMA,找到包含地址addr的那个
  for(int i = 0; i < MAXMMAP; i++) {
    if(p->proc_VMA[i].used) {
      begin = p->proc_VMA[i].addr;
      end = p->proc_VMA[i].addr + p->proc_VMA[i].length;
      if(p->proc_VMA[i].used == 1 && (addr >=  begin && addr < end)) {
        return i;
      }
    }
  }
  return -1;
}

/**
 * @description:  为文件映射的一页分配物理页
 * @param {uint64} addr 文件映射的一个虚拟地址
 * @param {int} index 这个文件映射对应的VMA编号,是isMmap的返回值
 * @return {*}
 */
void handleMmap(uint64 addr, int index) {
  struct proc *p = myproc();
  uint64 pa = (uint64)kalloc();
  int perm = 0, prot, off;
  struct inode *ip;
  if(pa == 0) { // 物理页不够
    panic("handleMmap: no pa");
  } else {
    memset((void*)pa, 0, PGSIZE);

    // 将文件数据读入物理页
    ip = p->proc_VMA[index].f->ip;
    off = p->proc_VMA[index].offset + (PGROUNDDOWN(addr) - p->proc_VMA[index].addr);
    ilock(ip);
    begin_op();
    readi(ip, 0, pa, off, PGSIZE);
    end_op();
    iunlock(ip);

    // 设置PTE权限
    prot = p->proc_VMA[index].prot;
    if(prot & PROT_READ) {
      perm |= PTE_R;
    }
    if(prot & PROT_WRITE) {
      perm |= PTE_W;
    }
    if(prot & PROT_EXEC) {
      perm |= PTE_X;
    }
    perm |= PTE_U;

    // 设置虚拟地址和物理地址的映射
    if((mappages(p->pagetable, PGROUNDDOWN(addr), PGSIZE, pa, perm)) < 0) {
      panic("handleMmap: mappages f");
    }
  }
}

像lazy allocation那样,在usertrap()中处理缺页故障;并且在uvmunmap()和uvmcopy()中跳过处理未定义或未分配的页表项,在walkaddr中为文件映射分配物理页

// kernel/trap.c: usertrap()
else if(scause == 13 || scause == 15) {
  int index;
  // 是mmap则分配物理页
  if((index = isMmap(r_stval())) >= 0) { 
    handleMmap(r_stval(), index);
  } else { // 不是mmap则说明访问了未使用的页,应该杀死进程
    printf("is no mmap\n");
    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;
  }
} 

// kernel/vm.c: uvmunmap()
if((pte = walk(pagetable, a, 0)) == 0)
  continue;
  // panic("uvmunmap: walk");
if((*pte & PTE_V) == 0)
  continue;
  // panic("uvmunmap: not mapped");

// kernel/vm.c: uvmcopy()
if((pte = walk(old, i, 0)) == 0)
  continue;
  // panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
  continue;
  // panic("uvmcopy: page not present");

// kernel/vm.c: walkaddr()
if(pte == 0 || (*pte & PTE_V) == 0) {
  int index;
  if((index = isMmap(va)) >= 0) {
    handleMmap(va, index);
  } else {
    return 0;
  }
}

至此,mmap部分就完成了。

munmap系统调用取消一个文件映射的部分或全部,并将设置了MAP_SHARED的文件映射写回文件中

// kernel/sysfile.c
/**
 * @description: 将从addr开始的length取消文件映射
 * @param {uint64} addr 虚拟地址
 * @param {uint64} length 字节数
 * @return {*}
 */
uint64 munmap(uint64 addr, uint64 length) {
  struct proc *p = myproc();
  int i;
  uint64 begin = 0, end = 0;
  // 查找包含地址addr的文件映射所对应的VMA
  for(i = 0; i < MAXMMAP; i++) {
    if(p->proc_VMA[i].used) {
      begin = p->proc_VMA[i].addr;
      end = p->proc_VMA[i].addr + p->proc_VMA[i].length;
      if(addr >= begin && addr < end) {
        break;
      }
    }
  }
  if (addr < begin || addr >= end) {
    panic("sys_munmap: addr not mmap");
  }
  // flags设置了MAP_SHARED的文件映射应该写回文件中
  if(p->proc_VMA[i].flags & MAP_SHARED) {
    for(uint64 index = PGROUNDDOWN(addr); index != PGROUNDUP(addr + length); index += PGSIZE) {
      pte_t *pte = walk(p->pagetable, index, 0);
      // 未分配物理页的文件映射应该跳过
      if(pte == 0 || (*pte & PTE_V) == 0) {
        continue;
      }
      begin_op();
      if(writei(p->proc_VMA[i].f->ip, 1, index, p->proc_VMA[i].offset + (index - addr), PGSIZE) < 0) {
        panic("sys_munmap: write");
      }
      end_op();
    } 
  }
  // 在页表上取消对应映射区域的PTE
  uvmunmap(p->pagetable, PGROUNDDOWN(addr), (PGROUNDUP(addr + length) - PGROUNDDOWN(addr)) / PGSIZE, 1);
  // 进行munmap的地址从该文件映射的开头开始
  if(addr == begin) {
    if(addr + length == end) { // 说明munmap的地址范围覆盖了该文件映射的全部,应该释放相应VMA,并减少文件引用
      p->proc_VMA[i].used = 0;
      struct file *f = p->proc_VMA[i].f;
      fileclose(f);
      return 0;
    } else { // 说明munmap的地址范围只有该文件映射的前半部分
      p->proc_VMA[i].addr = addr + length;
      p->proc_VMA[i].length -= length;
      p->proc_VMA[i].offset += length;
    }
  } else { // 进行munmap的地址从该文件映射的中间开始,一直到结尾(题目提示此时munmap的地址范围一定会在该文件映射的最后结束)
    p->proc_VMA[i].length -= length;
  }

  return 0;
}

uint64 sys_munmap(void) {
  uint64 addr;
  uint64 length;
  if(argaddr(0, &addr) < 0 || argaddr(1, &length) < 0) {
    return -1;
  }
  return munmap(addr, length);
}

至此,munmap系统调用也实现了。

还需要再处理一下exit()和fork()。

exit()函数中需要对该进程的全部文件映射进行munmap

// kernel/proc.c: exit()
for(int i = 0; i < MAXMMAP; i++) {
  if(p->proc_VMA[i].used) {
    // 偷懒直接调用了sys_munmap用的munmap函数
    munmap(p->proc_VMA[i].addr, p->proc_VMA[i].length);
  }
}

fork函数应该让子进程继承父进程的VMA和相应的文件映射,注意文件引用要加1。

文件映射的PTE会在uvmcopy()中继承。

for(int i = 0; i < MAXMMAP; i++) {
  np->proc_VMA[i] = p->proc_VMA[i];
  if(np->proc_VMA[i].f) filedup(np->proc_VMA[i].f);
}

至此,即可通过mmaptest和usertests。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值