【MTI 6.S081 Lab】Mmap

mmap/munmap

mmap和munmap系统调用允许UNIX程序对其地址空间进行详细控制。它们可以用于在进程之间共享内存,将文件映射到进程地址空间,并作为用户级页面错误方案的一部分,如讲座中讨论的垃圾收集算法。在这个实验中,您将向xv6添加mmap和munmap,重点关注内存映射文件。

手册页面(运行 man 2 mmap)显示了mmap的声明:

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

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

如果映射同一个MAP_SHARED文件的进程不共享物理页面,也可以。

munmap(addr,length)应该删除指示地址范围内的mmap映射。如果进程已修改内存并将其映射到MAP_SHARED,则应首先将修改写入文件。munmap调用可能只覆盖mmap-ed区域的一部分,但您可以假设它将在开始、结束或整个区域取消映射(但不能在区域中间打孔)。

实验任务

实验任务:您应该实现足够的mmap和munmap功能,以使mmaptest测试程序正常工作。如果mmaptest不使用mmap功能,则不需要实现该功能。

完成后,您应该会看到以下输出:

$ mmaptest
mmap_test starting
test mmap f
test mmap f: OK
test mmap private
test mmap private: OK
test mmap read-only
test mmap read-only: OK
test mmap read/write
test mmap read/write: OK
test mmap dirty
test mmap dirty: OK
test not-mapped unmap
test not-mapped unmap: OK
test mmap two files
test mmap two files: OK
mmap_test: ALL OK
fork_test starting
fork_test OK
mmaptest: all tests succeeded
$ usertests -q
usertests starting
...
ALL TESTS PASSED

Hints

  • 首先,将_mmaptest添加到UPROGS,以及mmap和munmap系统调用,以便让user/maptest.c进行编译。现在,只返回mmap和munmap中的错误。我们在kernel/fcntl.h中为您定义了PROT_READ等。运行mmaptest,它将在第一次mmap调用时失败。
  • Lazy分配页面,以应对页面错误。也就是说,mmap不应该分配物理内存或读取文件。相反,在usertrap中(或由usertrap调用)的页面错误处理代码中这样做,就像在lazy page allocation lab中一样。lazy的原因是确保大文件的mmap很快,并且文件的mmap大于物理内存是可能的。
  • 跟踪mmap为每个进程映射了什么。定义与第15讲中描述的VMA(虚拟内存区域)相对应的结构,记录mmap创建的虚拟内存范围的地址、长度、权限、文件等。由于xv6内核中没有内存分配器,因此可以声明一个固定大小的VMA数组,并根据需要从该数组中进行分配。16号应该足够了。
  • 实现mmap:在进程的地址空间中找到一个未使用的区域来映射文件,并将VMA添加到进程的映射区域表中。VMA应包含指向要映射的文件的结构文件的指针;mmap应该增加文件的引用计数,这样当文件关闭时结构就不会消失(提示:请参阅filedup)。运行mmaptest:第一个mmap应该成功,但第一次访问mmap-ed内存将导致页面错误并终止mmaptest。
  • 在mmap-ed区域中添加导致页面错误的代码,以分配一页物理内存,将相关文件的4096字节读取到该页面中,并将其映射到用户地址空间中。使用readi读取文件,它采用一个偏移量参数来读取文件(但您必须锁定/解锁传递给readi的inode)。别忘了在页面上正确设置权限。运行mmaptest;它应该到达第一个munmap。
  • 实现munmap:找到地址范围的VMA并取消映射指定的页面(提示:使用uvmunmap)。如果munmap删除了前一个mmap的所有页面,那么它应该减少相应结构文件的引用计数。如果未映射的页面已被修改,并且文件已映射为MAP_SHARED,请将页面写回该文件。从filewrite中寻找灵感。
  • 理想情况下,您的实现只会写回程序实际修改的MAP_SHARED页面。RISC-V PTE中的脏比特(D)指示页面是否已经被写入。但是,mmaptest不会检查是否未回写非脏页;因此,您可以在不看D位的情况下写回页面。
  • 修改exit以取消映射进程的映射区域,就好像调用了munmap一样。运行mmaptest;mmap_test应该通过,但可能不会通过fork_test。
  • 修改fork以确保子对象具有与父对象相同的映射区域。不要忘记增加VMA结构文件的引用计数。在子级的页面错误处理程序中,可以分配一个新的物理页面,而不是与父级共享页面。后者会更酷,但需要更多的实施工作。运行mmaptest;它应该同时通过mmaptest和forktest。

运行usertests -q以确保一切正常。

解决方案

我们可以根据Hints来构建解决方案。

准备

在这个实验以前肯定已经完成很多的实验了,这里主要是添加函数原型,以便能调用mmap/munmap

VMA

struct vma {
  struct spinlock lock;
  uint64 start;
  uint64 end;
  int length;
  int off;
  int perm;
  int flags;
  struct file *file;
  struct vma *next;
}; 
struct vma VMA[NVMA];

注意将VMA中每个length变为-1,说明此结构没有被占用,内核可以分配给任意进程。

struct vma *
vma_alloc(void) {
  for(int i = 0; i < NVMA; i++){
    if (VMA[i].length == -1) {
      acquire(&VMA[i].lock);
      if(VMA[i].length == -1) {
        VMA[i].length = 0;
        release(&VMA[i].lock);
        return &VMA[i];
      }
      release(&VMA[i].lock);
    }
  }
  panic("no enough vma");
}

先看一下是否有可能有要释放的结构,如果有,去夺取锁,然后再次检测,看是否已经被分配,没有被分配就可以分配给当前任务。

mmap

uint64
sys_mmap(void) {
  uint64 addr;
  int length;
  int prot;
  int flags;
  int fd;
  int offset;
  argaddr(0, &addr);
  argint(1, &length);
  argint(2, &prot);
  argint(3, &flags);
  argint(4, &fd);
  argint(5, &offset);
  if (addr != 0 || offset != 0) {
    return -1;
  }
  struct proc *p = myproc();
  struct file* f = p->ofile[fd];

  int pte_flag = PTE_U;
  if (prot & PROT_WRITE) {    // 想要写,那么文件必须是可写, 或者标志是私有
    if(!f->writable && !(flags & MAP_PRIVATE)) {
      return -1; 
    }
    pte_flag |= PTE_W;
  }
  if (prot & PROT_READ) {
    if(!f->readable) {
      return -1;
    }
    pte_flag |= PTE_R;
  }

  struct vma* v = vma_alloc();
  
  v->length = length;
  v->off = offset;
  v->perm = pte_flag;
  v->flags = flags;
  filedup(f);
  v->file = f;
  v->next = (struct vma*)0;
  struct vma *pv = p->vma;
  if (pv) {
    while (pv->next) {
      pv = pv->next;
    }
    v->start = PGROUNDUP(pv->end); // 使得开始的位置为一页的整数
    v->end = v->start + length;
    pv->next = v;
  } else {
    v->start = VMA_START;
    v->end = v->start + length;
    p->vma = v;
  }
  addr = v->start;
  printf("mmap: [%p, %p)\n", addr, v->end);  // for debugging
  return addr;
}

Page fault 处理

在trap.c中,如果是因为缺页导致的错误,进入缺页处理程序,此时检测这个地址是否可以被翻译。

因为这个实验中,我们只对mmap做了lazy分配,所以地址要在vma的地址之间,否则就是因为其他的原因导致的缺页,不归当前的实验处理,返回-1,同时要注意在trap.c中的这个分支,看到了返回-1,需要杀死这个进程。因为这个进程可能访问、写、执行了他没有权限的地址,这是一个致命的错误。

int
mmap_handler(uint64 scause, uint64 va) {
  struct proc *p = myproc();
  struct vma *v = p->vma;

  while (v) {
    if (va >= v->start && va < v->end) {
      break;
    }
    v = v->next;
  }
  
  if (v == 0 || scause == 12 || (scause == 13 && !(v->perm & PTE_R)) || (scause == 15 && !(v->perm & PTE_W))) {
    // 12  执行指令,在这里不可能映射动态链接库用于执行指令,所以不行
    // 13  load 引起,但是如果不可读,那么会错误
    // 15  store 引起,不可写也会错误
    return -1;
  }
  va = PGROUNDDOWN(va);   // 找打页面
  char *mem = kalloc();
  if (mem == 0) {
    // panic("mmap_handler: kalloc error");
    return -1;
  }

  memset(mem, 0, PGSIZE);

  uint doff = va + v->off - v->start;   // 在文件中的实际开始位置
  ilock(v->file->ip);
  readi(v->file->ip, 0, (uint64)mem, doff, PGSIZE);
  iunlock(v->file->ip);

  // 此时已经读入了文件的内容
  if (mappages(p->pagetable, va, PGSIZE, (uint64)mem, v->perm) < 0) {
    kfree(mem);
    // panic("mmap_handler: mappages error");
    return -1;
  } 
  return 0;
}

munmap

这个是比较难处理的,看了好些解答,但是没有一个解答是让我满意的,基本看了头几行,就发现他们代码不行,主要是他们的处理太过理想了。

很多的代码有如下的假设:

  • 每次释放的地址addr都是PGSIZE的整数倍
  • 每次释放的长度length都是PGSIZE的整数倍
  • 在释放的时间点,需要释放的页面,在之前肯定都已经访问过了,所以在页面中肯定有其相应的PTE,而且是有效的,所以直接使用void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)

这上面的假设都是不正确的,特别是,在释放的时间点,很可能没有访问过这个页面,那么此时释放会导致释放一个未映射的页面,导致panic。

在mmaptest的代码中,上面头两个假设还是满足了,但是第三个假设在mmaptest中都没有满足。

uint64
sys_munmap(void) {
  uint64 addr;
  int length;
  argaddr(0, &addr);
  argint(1, &length);
  if (addr == 0 || length == 0) {
    return 0;   // 不用取消映射
  }

  struct proc *p = myproc();
  struct vma *v = p->vma;
  struct vma *pre = 0;
  while(v != 0){
    if(addr >= v->start && addr < v->end) break; // found
    pre = v;
    v = v->next;
  }

  if(v == 0)
    return -1;
  printf("munmap: %p %d\n", addr, length);
  if(addr != v->start && addr + length != v->end) 
    panic("munmap middle of vma");

  if (length > v->length) {
    length = v->length;
  }

  if (addr == v->start) {
    writeback(v, addr, length);
    // 由于可能在同一个映射空间上调用两次munmap,所以这里的addr不一定是PGSIZE的整数倍
    // 实际要释放的页为 从 PGROUNDDOWN(addr)开始的 (len + (addr - PGROUNDDOWN(addr))) / PGSIZE 数量的页。又因为可能当前还没有实际分配,所以不能使用uvmunmap
    uint64 end = (length + (addr - PGROUNDDOWN(addr))) / PGSIZE * PGSIZE + PGROUNDDOWN(addr);
    pte_t *pte;
    for (uint64 va = PGROUNDDOWN(addr); va < end; va += PGSIZE) {
      if((pte = walk(p->pagetable, va, 0)) == 0) {
        continue;
      }
      if (*pte & PTE_V) {
        uint64 pa = PTE2PA(*pte);
        kfree((void*)pa);
        *pte = 0;
      } // else 无效,不要释放
    } 
    if(length == v->length){
      // 将会全部释放,那么对于最后一页需要一些特别的操作
      uint64 va = PGROUNDDOWN(v->end - 1);
      if((pte = walk(p->pagetable, va, 0)) != 0) {
        if (*pte & PTE_V) {
          uint64 pa = PTE2PA(*pte);
          kfree((void*)pa);   // 释放最后一页
          *pte = 0;
        }
      }
      fileclose(v->file);
      if(pre == 0){
        p->vma = v->next; // head
      }else{
        pre->next = v->next;
      }
      v->next = 0;
      acquire(&v->lock);
      v->length = -1;
      release(&v->lock);
    } else {
      v->start += length;   // 开始位置应该加length
      v->off += length;
      v->length -= length;
    }
  } else {
    // 从addr释放到end 那么要释放页,从UP(addr) ~ up(end)
    pte_t *pte;
    for(uint64 va = PGROUNDUP(addr); va < PGROUNDUP(v->end); va += PGSIZE){
      if((pte = walk(p->pagetable, va, 0)) == 0) {
        continue;
      }
      if (*pte & PTE_V) {
        uint64 pa = PTE2PA(*pte);
        kfree((void*)pa);
        *pte = 0;
      }
    }
    v->length -= length;
    v->end -= length;
  }
  return 0;
}

在我的实现中,考虑到了,这个条目根本没有在页表中的情况,这个代码更鲁棒。

共享文件取消映射后写回

这个同样有上面的问题,当没有访问过,也就不存在页表中,那就不要放回了,否则,会每次写入文件中的字节数为0,导致死循环。

void
writeback(struct vma* v, uint64 addr, int n)
{
  if(!(v->perm & PTE_W) || (v->flags & MAP_PRIVATE)) // 不可写,或者这是一个私有的,那么不应该写回
    return;
  struct proc *p = myproc();
  struct file* f = v->file;

  int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;
  int i = 0;
  pte_t *pte;
  while(i < n){
    if ((pte = walk(p->pagetable, PGROUNDDOWN(addr), 0)) == 0 || !(*pte & PTE_V)) {
      // pte不存在或者无效,说明这部分没有更改
      i += (PGROUNDDOWN(addr) + PGSIZE - addr);
      addr += PGROUNDUP(addr + 1);
      continue;
    }
    // 找到了物理位置
    uint64 pa = PTE2PA(*pte);
    uint64 start = addr - PGROUNDDOWN(addr);    // 开始位置

    int n1 = n - i;
    if (n1 > PGSIZE - start) {
      // 不超过这个页面
      n1 = PGSIZE - start;
    }
    if (n1 > max) {
      n1 = max;
    }
    begin_op();
    ilock(f->ip);
    int r = writei(f->ip, 0, pa + start, addr + v->off - v->start, n1);
    iunlock(f->ip);
    end_op();
    i += r;
    addr += r;    // 下次从r位置开始写
  }
}

exit 取消所有映射的文件

同样的问题,只释放实际加载的页面。

// Exit the current process.  Does not return.
// An exited process remains in the zombie state
// until its parent calls wait().
void
exit(int status)
{
  struct proc *p = myproc();

  if(p == initproc)
    panic("init exiting");

  // munmap all mmap vma
  struct vma* v = p->vma;
  struct vma* pv;
  while(v){
    writeback(v, v->start, v->length);
    uint64 start = PGROUNDDOWN(v->start);
    uint64 end = PGROUNDUP(v->end);
    pte_t *pte;
    for (uint64 va = start; va < end; va += PGSIZE) {
      if ((pte = walk(p->pagetable, va, 0)) == 0 || !(*pte & PTE_V)) {
        continue;   // 没有加载
      }
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
      *pte = 0;
    }
    fileclose(v->file);
    pv = v->next;
    acquire(&v->lock);
    v->next = 0;
    v->length = -1;
    release(&v->lock);
    v = pv;
  }
  p->vma = 0;

  // Close all open files.
  ...
}

fork 复制父进程的VMA

由于fork的mapcopy已经为子进程copy了所有父进程的页面,所以此时父进程中映射页面的分布是什么样的,子进程中就会是什么样的,直接复制他的vma结构就可以了。

当然,即使是lazy allocation,这样直接复制也没问题,因此此时子进程vma对应的所有页都是无效的,在访问时,会触发page fault,会使得其有效。

注意,不能直接使得子进程的vma指向父进程的vma,这样,父子进程不能写时复制,破坏了其能指向的页面,可能会造成严重的内存泄漏。

// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();
  
  ...

  acquire(&np->lock);
  np->state = RUNNABLE;
  np->vma = 0;
  struct vma *pv = p->vma;
  struct vma *pre = 0;
  while(pv){
    struct vma *vma = vma_alloc();
    vma->start = pv->start;
    vma->end = pv->end;
    vma->off = pv->off;
    vma->length = pv->length;
    vma->perm = pv->perm;
    vma->flags = pv->flags;
    vma->file = pv->file;
    filedup(vma->file);
    vma->next = 0;
    if(pre == 0){
      np->vma = vma;
    } else {
      pre->next = vma;
    }
    pre = vma;
    pv = pv->next;
  }
  release(&np->lock);

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值