6.s081 学习实验记录(十一)mmap

mmap

简介

切换到 mmap 分支。

实现 mmap 以及 munmap 系统调用,mmap 的函数声明如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); mmap的用处很多,当前实验仅关注内存映射文件的功能。

  • addr参数表示映射的虚拟地址的起始,也可以指定为0,此时内核将会决定映射文件的起始虚拟地址,mmap成功后会返回该地址,失败则返回0xffffffffffffffff
  • length参数表示要映射的字节,可以与文件的长度不同
  • prot参数表示内存是否映射为可读、可写、可读写、可执行等,可以假设protPROT_READ 或者 PROT_WRITE 或者两者
  • flags参数只能为MAP_SHARED或者 MAP_PRIVATE,前者表示对于映射内存的修改需要写回内存,后者表示不应该写回
  • fd表示要映射的已打开文件的描述符
  • offset参数表示从文件的哪里开始映射,可以假设该参数为0

munmap(addr, length) 应删除指定地址范围内的mmap映射,如果进程修改了内存且映射方式为MAP_SHARED,则应首先将修改写回到文件。munmap 可能只覆盖 mmap 区域的一部分,可以假设它会在开始、结束或整个区域取消映射,而不会在中间打洞。

目标:实现 mmapmunmap 函数并通过 mmaptest 测试

提示

  • kernel/fcntl.h 中定义了PROT_READ相关的宏
  • mmap不应分配物理内存或读取文件,而应该在映射文件发生读写时通过page fault 的页错误处理代码中实现页面的申请及内容修改,这样做的原因是保证大文件的mmap速度,大于物理内存的文件mmap也是可行的
  • 定义一个 struct vma ,用于记录 mmap 创建的虚拟内存的地址、长度、权限以及对应的文件等信息。由于xv6内核中没有内存分配器,因此可以声明固定大小的 vma 数组并根据需要从该数组中分配 vma,大小 16 就可以了。
  • 实现mmap:在进程的地址空间中找到一块未使用的区域来映射文件,并将vma添加到进程的映射区域表中。vma应该包含一个指向被映射文件的文件指针(struct file),mmap还需要增加该文件的引用计数,以防止当文件被关闭时,该结构体失效。
  • 添加页错误的处理代码,当mmap区域发生page fault时,分配一个4KB的页,并从文件中读取4KB填充到该页,需要记录文件的读取位置。
  • RISC-V PTE中的dirty bit (D) 标记了当前该页是否被修改过
  • Modify exit to unmap the process’s mapped regions as if munmap had been called.
  • Modify fork to ensure that the child has the same mapped regions as the parent. Don’t forget to increment the reference count for a VMA’s struct file. In the page fault handler of the child, it is OK to allocate a new physical page instead of sharing a page with the parent.

实验代码

  • kernel/param.h
#define NVMA         16    // 每个进程最大的VMA区域数量
  • kernel/proc.h
struct vma {
  int valid;
  uint64 addr; //起始虚拟地址
  int length; //映射区域的长度
  int prot;
  int flags;
  struct file *mapfile;
};

struct proc {
  // ...
  // mmap lab 
  struct vma vmas[NVMA];       // Process virtual memory area
};
  • kernel/proc.c

需要注意,exit 退出的时候由于mmap映射的vma区域有的地方没有被写过,因此没有触发 page fault,所以这些虚拟地址没有关联物理地址。如果不改动 uvmunmap 函数,则在解除该虚拟地址和物理地址的映射时,由于该虚拟地址对应的页框没有分配物理地址,PTE_V标志为0,会导致panic。所以,在exit中添加了虚拟地址是否关联物理地址的判断

#include "fcntl.h"
// 创建进程时初始化vma区域
static struct proc*
allocproc(void)
{
  struct proc *p;
  // ... 
found:
  p->pid = allocpid();
  // 新增代码
  for (int i = 0; i < NVMA; i++) {
    p->vmas[i].valid = 0;
  }
  // ...
  return p;
}

// 创建子进程时拷贝mmap区域
int
fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();
  // ...
  np->sz = p->sz;
  // 拷贝vma区域到子进程
  for (int i = 0; i < NVMA; i++) {
    if (p->vmas[i].valid) {
      np->vmas[i].valid = 1;
      np->vmas[i].addr = p->vmas[i].addr;
      np->vmas[i].length = p->vmas[i].length;
      np->vmas[i].prot = p->vmas[i].prot;
      np->vmas[i].flags = p->vmas[i].flags;
      np->vmas[i].mapfile = p->vmas[i].mapfile;
      filedup(np->vmas[i].mapfile); // 增加引用计数
    }
  }
  // ...其他代码
}

// exit退出时,解除当前进程所有的mmap,需要注意有的vma没有被写过,没有触发page fault,因此需要跳过
void
exit(int status)
{
  struct proc *p = myproc();
  pte_t *pte;
  // munmap所有映射
  for (int i = 0; i < NVMA; i++) {
    if (p->vmas[i].valid) {
      if (p->vmas[i].flags & MAP_SHARED) {
         // 如果是 MAP_SHARED,将修改写回
        filewrite(p->vmas[i].mapfile, p->vmas[i].addr, p->vmas[i].length);
      }
      // 解除虚拟地址和物理页的映射
      uint64 a;
      uint64 addr = p->vmas[i].addr;
      for(a = addr; a < addr + p->vmas[i].length; a += PGSIZE){
        if((pte = walk(p->pagetable, a, 0)) == 0)
          panic("uvmunmap: walk");
        if((*pte & PTE_V) == 0)
          continue;  
        // 关联了物理页,执行uvmunmap
        uvmunmap(p->pagetable, a, 1, 1);
      }
      // uvmunmap(p->pagetable, p->vmas[i].addr, p->vmas[i].length/PGSIZE, 1);
      // 引用计数减一
      fileclose(p->vmas[i].mapfile);
    }
  }
}
  • makefile
$U/_mmaptest\
  • user/usys.pl
entry("mmap");
entry("munmap");
  • user/user.h
void *mmap(void *addr, uint length, int prot, int flags,
           int fd, uint offset);
int munmap(void *addr, uint len);
  • kernel/syscall.h
#define SYS_mmap   22
#define SYS_munmap 23
  • kernel/syscall.c
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);

static uint64 (*syscalls[])(void) = {
 // ...
[SYS_mmap]    sys_mmap,
[SYS_munmap]  sys_munmap,
};
  • kernel/sysfile.c

exit函数,在sys_unmap函数中也需要添加同样的虚拟地址是否关联物理地址的判断

uint64
sys_mmap(void)
{
  uint64 addr;
  int length, prot, flags, fd, i;
  struct proc* p;
  argint(1, &length);
  argint(2, &prot);
  argint(3, &flags);
  argint(4, &fd);
  if (length < 0 || prot < 0 || flags < 0 || fd < 0)
    return -1;
  p = myproc();
  struct file *mapfile = p->ofile[fd];
  if ((!mapfile->writable)&&(prot&PROT_WRITE)&&(!(flags&MAP_PRIVATE)))
    return -1;
  for (i = 0; i < NVMA; i++) {
    // 分配一个新的vma区域
    if (p->vmas[i].valid == 0) {
      p->vmas[i].valid = 1;
      p->vmas[i].addr = addr = p->sz;
      p->vmas[i].length = length;
      p->vmas[i].prot = prot;
      p->vmas[i].flags = flags;
      p->vmas[i].mapfile = mapfile;
      filedup(p->ofile[fd]); // 文件的引用计数加一
      break;
    }
  }
  // have no vma slot
  if (i == NVMA) {
    return -1;
  }
  p->sz += length; // lazy mapping
  return addr;
}

uint64
sys_munmap(void)
{
  uint64 addr;
  int length, i;
  struct proc* p;
  pte_t *pte;
  
  argaddr(0, &addr);
  argint(1, &length);
  if (addr < 0 || length < 0)
    return -1;
  p = myproc();
  for (i = 0; i < NVMA; i++) {
    if (p->vmas[i].valid == 1) {
      if (p->vmas[i].addr <= addr && (p->vmas[i].addr + p->vmas[i].length) > addr)
            break;
    }
  }

  // have no vma slot
  if (i == NVMA) {
    return -1;
  }

  struct vma *vmap = &p->vmas[i];
  if (vmap->flags & MAP_SHARED) { 
    filewrite(vmap->mapfile, addr, length);
  }

  // 同理,需要判断虚拟地址是否关联物理地址
  if (vmap->addr == addr && vmap->length == length) {
    // unmap the whole vma
    // 先判断哪些虚拟地址已经关联了物理页,因为有的va可能还没被写,没有触发缺页中断
    uint64 a;
    int i = length / PGSIZE;
    for(a = addr; a < addr + i*PGSIZE; a += PGSIZE){
      if((pte = walk(p->pagetable, a, 0)) == 0)
        panic("uvmunmap: walk");
      if((*pte & PTE_V) == 0)
        continue;  
      // 关联了物理页,执行uvmunmap
      uvmunmap(p->pagetable, a, 1, 1);
    }
    fileclose(vmap->mapfile);
    vmap->valid = 0;
  } else if (vmap->addr == addr) {
    // unmap from the beginning
    // 同理,需要判断是否关联物理地址
    uint64 a;
    int i = length / PGSIZE;
    for(a = addr; a < addr + i*PGSIZE; a += PGSIZE){
      if((pte = walk(p->pagetable, a, 0)) == 0)
        panic("uvmunmap: walk");
      if((*pte & PTE_V) == 0)
        continue;  
      // 关联了物理页,执行uvmunmap
      uvmunmap(p->pagetable, a, 1, 1);
    }
    vmap->addr += length;
    vmap->length -= length;
  } else if (vmap->addr + vmap->length == addr + length){
    // unmap from the end
    uint64 a;
    int i = length / PGSIZE;
    for(a = addr; a < addr + i*PGSIZE; a += PGSIZE){
      if((pte = walk(p->pagetable, a, 0)) == 0)
        panic("uvmunmap: walk");
      if((*pte & PTE_V) == 0)
        continue;  
      // 关联了物理页,执行uvmunmap
      uvmunmap(p->pagetable, a, 1, 1);
    }
    vmap->length -= length;
  }

  return 0;
}
  • kernel/trap.c
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "fcntl.h"

void
usertrap(void)
{
  // ... 
  if(r_scause() == 8){
    // ...
  } else if((which_dev = devintr()) != 0){
    // ok
  } else if (r_scause() == 13 || r_scause() == 15) {
    // page fault (write or read)
    uint64 va = r_stval();

    if (va >= p->sz || va < p->trapframe->sp) {
      // 如果进程读写的虚拟地址不在堆栈之间,报错,因为mmap映射的区域为堆和栈中间未使用的区域
      p->killed = 1;
    } else {
      int i;
      // check if the pagefault page is in one virtual memory area
      for (i = 0; i < NVMA; i++) {
        if (p->vmas[i].valid) {
          if (p->vmas[i].addr <= va && (p->vmas[i].addr + p->vmas[i].length) > va)
            break;
        }
      }
      if (i == NVMA) {
        // not in any vma
        p->killed = 1;
      } else {
        // allocate page
        uint64 ka = (uint64) kalloc();
        if (ka == 0){
          p->killed = 1;
        } else {
          // printf("access va %d\n", va);
          // printf("sz : %d\n", p->sz);
          // printf("addr : %d\n", p->vmas[i].addr);
          memset((void *)ka, 0, PGSIZE);
          va = PGROUNDDOWN(va);
          ilock(p->vmas[i].mapfile->ip);
          readi(p->vmas[i].mapfile->ip, 0, ka, va - p->vmas[i].addr, PGSIZE);
          iunlock(p->vmas[i].mapfile->ip);
          uint64 pm = PTE_U;
          if (p->vmas[i].prot & PROT_READ)
            pm |= PTE_R;
          if (p->vmas[i].prot & PROT_WRITE)
            pm |= PTE_W;
          if(mappages(p->pagetable, va, PGSIZE, ka, pm) != 0) {
            kfree((void *)ka);
            p->killed = 1;
          }
        }
      }
    }
  } else {
    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;
  }
  // ...
}

此时可以通过 mmaptest 的前半部分,而forktest会失败。这是因为forktest中父进程会fork一个子进程验证mmap区域是否复制,验证完成子进程退出后,父进程通过wait等待子进程退出信号,并在收到信号后清理子进程的所有资源。此时会调用freeproc清理子进程,该函数会通过proc_freepagetable函数从虚拟地址 0 到 p->sz(sz记录了子进程已分配的虚拟地址的最大值(要求vm连续分配)) ,将这两个值作为参数调用 uvmunmap

因此,这里假定了从 0 到 p->sz 范围内的va都关联了pa,否则会由于前面所述原因而panic。而我们在前面实现为mmap区域分配va的逻辑借助了p->sz,即每次从 p->sz的位置开始往后分配 length 长度,然后 p->sz += length,这就导致xv6原先保证的 0 到 p->sz 区域va都关联 pa不成立了。所以,我们要不另外选择 mmap 区域的va分配算法,要不更改 uvmunmap 函数,使得该函数在检测到 va 没有关联 pa时,不要panic,而是跳过(这里时间原因,选择这种,正规来说应该重新选择va分配算法)

同理,还有创建子进程时使用的 uvmcopy 函数,因此我们都需要进行修改

  • kernel/vm.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)
      panic("uvmunmap: walk");
    if((*pte & PTE_V) == 0)
      continue; //当检测到 va 没有关联 pa时,跳过,用于处理父进程释放子进程资源的情况(freeproc函数的调用链)
      //panic("uvmunmap: not mapped");
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    *pte = 0;
  }
}

int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  char *mem;

  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0)
      panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      continue;
      //panic("uvmcopy: page not present");
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    if((mem = kalloc()) == 0)
      goto err;
    memmove(mem, (char*)pa, PGSIZE);
    if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
      kfree(mem);
      goto err;
    }
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}

实验结果

  • mmaptest
    在这里插入图片描述

  • usertests -q
    在这里插入图片描述

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值