Loading

6.S081 lab10 mmap

#mmap

这个实验个人感觉还是有点难度的,关键要理解mmap这个系统调用的工作原理,关于mmap的相关博文可以参考:https://www.cnblogs.com/huxiao-tee/p/4660352.html

  • 添加系统调用等操作不再赘述。我在defs.h中添加的函数原型如下图

    79Z_F@G__@__9_U`AX90XC6.png

  • 完成sys_mmap函数,添加vma结构到struct proc中。

    image.png

    struct proc中添加16vma

    image.png

    vma是用来记录每次文件映射信息的数据结构,sys_mmap函数的目的就是找到一个空闲的vma,把本次需要映射的文件相关信息加入到此vma

  • sys_mmap函数首先获取6个参数。需要注意:如果磁盘中的文件没有写权限,但在prot中同时开启了写权限和回写权限,需要返回错误,这里的逻辑是:sys_mmap相当于在内存中拷贝了一份文件的副本,并且添加地址映射使得用户进程可以直接在内存中通过指针来读写文件,如果同时开启了写权限和回写权限,那么在内存中写完文件后是要回写到磁盘的,而磁盘中的文件并没有写权限,所以这种操作是不允许的。(有部分测试会有上述情况)

    上面说到sys_mmap会在内存中拷贝一份文件的副本,但真正分配物理页是在usertrap中缺页中断时(参考lazy page allocation)。现在需要选择一个虚拟地址开始映射,我选择的是堆空间的上方,并且做了相应的页面对齐处理防止堆被覆盖。别忘记增加文件的引用计数,完整代码如下:

    uint64
    sys_mmap(void){
      uint64 addr, length, offset;
      int prot, flags, fd;
      struct file *fp;
      uint64 error = 0xffffffffffffffff;
      if (argaddr(0, &addr) < 0 || argaddr(1, &length) < 0 || argint(2, &prot) < 0 || 
          argint(3, &flags) < 0 || argfd(4, &fd, &fp) < 0 || argaddr(5, &offset) < 0)
        return error;
      if (fp->writable == 0 && prot & PROT_WRITE && flags & MAP_SHARED)
        return error;
    
      struct proc *p = myproc();
      if (offset < 0 || length < 0 || p->sz + length >= MAXVA)
        panic("mmap: error");
      
      // 为映射起点找一个地址,由于p->sz不是页对齐的,对其上取整
      addr = PGROUNDUP(p->sz);
      length = PGROUNDUP(length);
      printf("addr = %p, length = %p\n", addr, length);
      p->sz += length;
      struct VMA *vma = 0;
      for (int i = 0; i < NVMA; i++){
        if (p->vma[i].valid == 0){
          vma = &p->vma[i];
          break;
        }
      }
      if (vma == 0)
        panic("mmap: no vma");
      vma->valid = 1;
      vma->addr = addr;
      vma->flags = flags;
      vma->length = length;
      vma->offset = offset;
      vma->prot = prot;
      vma->fp = fp;
      filedup(fp);
      return addr;
    }
    
  • 现在需要在usertrap中添加相应逻辑来处理缺页错误,当发生缺页错误时,首先通过出错的虚拟地址得到出错的页面,之后在进程的所有vma中找满足条件的vma如果找不到,说明用户进程访问了一个非法的虚拟地址,应该kill掉,否则则分配一个物理页,把对应的文件读到新分配的物理页中,根据找到的vmaprot设置好相应的权限,把映射关系添加到用户页表中。完整代码如下:

    else if (r_scause() == 13 || r_scause() == 15){
        uint64 va = r_stval();
        struct VMA *vma = 0;
        for (int i = 0; i < NVMA; i++){
          // 查找造成缺页错误的虚拟地址是否在vma中
          if (p->vma[i].valid == 1){
            if (va >= p->vma[i].addr && va < p->vma[i].addr + p->vma[i].length){
              vma = &p->vma[i];
              break;
            }
          }
        }
        // 若无法找到对应的vma,说明进程访问了非法的虚拟地址
        if (vma == 0){
          p->killed = 1;
        }
        else {
          uint64 pa = (uint64)kalloc();
          if (pa == 0)
            panic("usertrap: no pages");
          memset((void*)pa, 0, PGSIZE);
          struct inode *ip = vma->fp->ip;
          va = PGROUNDDOWN(va);
          ilock(ip);
          if (readi(ip, 0, pa, va - vma->addr + vma->offset, PGSIZE) == 0)
            panic("usertrap: readi error");
          iunlock(ip);
          int perm = PTE_U;
          if (vma->prot & PROT_READ)
            perm |= PTE_R;
          if (vma->prot & PROT_WRITE)
            perm |= PTE_W;
          if (vma->prot & PROT_EXEC)
            perm |= PTE_X;
          if (mappages(p->pagetable, va, PGSIZE, pa, perm) < 0)
            panic("usertrap: mappages error");
        }
    
  • 完成sys_munmap函数。该函数的作用是取消一段地址的文件映射关系。首先还是找对应的vma(本实验的测试中的开始地址addr只会与vma中的addr相等,相当于简化了设计),更改vma->lengthvma->addr,在取消映射之前,如果开启了写回标志,需要用filewrite函数把文件写回磁盘。调用uvmunmap函数删除相关pte。 若vma->length为0,说明文件的所有物理页已被sys_munmap函数处理完了,这时需要减少文件的引用计数。完整代码如下:

    uint64
    sys_munmap(void){
      uint64 addr, length;
      if (argaddr(0, &addr) < 0 || argaddr(1, &length) < 0)
        return -1;
      
      // 找满足条件的vma
      struct proc *p = myproc();
      struct VMA *vma = 0;
      for (int i = 0; i < NVMA; i++){
        vma = &p->vma[i];
        if (vma->valid && vma->addr == addr){
            vma->addr += length;
            vma->length -= length;
            break;
        }
      }
      if (vma == 0){
        printf("munmap: not find vma");
        return -1;
      }
      
      if (vma->flags & MAP_SHARED)
        if (filewrite(vma->fp, addr, length) < 0)
          /* panic("munmap: filewrite error"); */
    
      uvmunmap(p->pagetable, addr, length / PGSIZE, 1);
      // 如果文件对应的物理页已被处理完,则关闭文件
      if (vma->length == 0){
        fileclose(vma->fp);
        vma->valid = 0;
      }
    
      return 0;
    }
    
  • 两个系统调用写完后,接下来处理一些细节问题。当进程调用exit函数时,说明进程要退出了,那么应该释放进程所有的vma,就像sys_munmap函数一样。代码如下:

    void
    exit(int status)
    {
      ...
      // 清除进程的VMA
      for (int i = 0; i < NVMA; i++){
        struct VMA *vma = &p->vma[i];
        if (vma->valid == 1){
          uvmunmap(p->pagetable, vma->addr, vma->length / PGSIZE, 1);
        }
      }
      // Jump into the scheduler, never to return.
      sched();
      panic("zombie exit");
    }
    

    当进程调用fork创建子进程时,子进程应该拥有与父进程相同的vma,代码如下:

    int
    fork(void)
    {
      ...
      // 拷贝父进程的vma
      for (i = 0; i < NVMA; i++){
        if (p->vma[i].valid){
          np->vma[i] = p->vma[i];
          filedup(p->vma[i].fp);
        }
      }
    
      release(&np->lock);
    
      return pid;
    }
    

    当回收进程时,应该把进程的vma全部置0,在freeproc函数中添加相应代码:

    static void
    freeproc(struct proc *p)
    {
      ...
      // 清除进程的vma
      memset(p->vma, 0, sizeof(p->vma));
      p->pagetable = 0;
      p->sz = 0;
      p->pid = 0;
      p->parent = 0;
      p->name[0] = 0;
      p->chan = 0;
      p->killed = 0;
      p->xstate = 0;
      p->state = UNUSED;
    }
    
posted @ 2022-01-18 11:08  Kyoz1  阅读(122)  评论(0)    收藏  举报
7