首页 > 系统相关 >Mit6.S081笔记Lab10: mmap 文件内存映射

Mit6.S081笔记Lab10: mmap 文件内存映射

时间:2024-11-19 16:57:52浏览次数:1  
标签:uint64 sz addr 映射 Mit6 mmap vma Lab10

课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.html
Lab 地址:https://pdos.csail.mit.edu/6.S081/2020/labs/mmap.html
我的代码地址:https://github.com/Amroning/MIT6.S081/tree/mmap
xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf
相关翻译:https://xv6.dgs.zone/labs/requirements/lab10.html
参考博客:https://blog.miigon.net/posts/s081-lab10-mmap/

学习笔记记录,如有错误恳请各位大佬指正

Lab10: mmap

​ 仿照Linux实现mmap功能,即将文件映射到进程地址空间,如果进程修改了这部分内存,并且内存标记为映射内存的修改应写回文件,那么释放映射前需要把修改写入源文件。这样与文件交互的时候就可以减少磁盘操作。该实验需要用到很多前面实验的知识点

完整题目要求请去顶部链接查看

mmap(hard)

您应该实现足够的mmapmunmap功能,以使mmaptest测试程序正常工作。如果mmaptest不会用到某个mmap的特性,则不需要实现该特性。

​ 进程所使用的内存空间从低地址向高地址生长(sbrk调用),范围是stack到trapframe。为了不和进程使用的内存空间冲突,将mmap使用的地址空间映射到trapframe下面的页,从上往下生长。先定义mmap最后一页的地址:

// kernel/memlayout.h
// MMAP 进程映射文件内存最后一个页(开区间)
#define MMAPEND TRAPFRAME

定义一个vma结构体,表示虚拟内存区域,用来记录mmap创建的虚拟内存地址的范围、长度、权限、文件等。再声明一个vma结构体的数组,当mmap映射操作时,就来这个数组获取vma虚拟内存区域:

// kernel/proc.h
// 定义一个虚拟内存区域结构体,用来记录mmap创建的虚拟内存地址的范围、长度、权限、文件等
struct vma {
    int valid;              // 该虚拟内存区域是否已被映射
    uint64 vastart;         // 该虚拟内存区域开始地址
    uint64 sz;              // 该虚拟内存区域大小
    struct file* f;         // 该虚拟内存区域映射的文件
    int prot;               // 该虚拟内存区域权限
    int flags;              // 标记映射内存的修改是否写回文件
    uint64 offset;          // 映射文件的起点
};

#define NVMA 16             // VMA数组大小

// Per-process state
struct proc {
  struct spinlock lock;

  ......
  struct vma vmas[NVMA];       // mmap虚拟内存映射地址数组
};

​ 接下来实现mmap的系统调用。这个实验要做的事挺多,最后再添加系统调用的声明。参考Linux的mmap函数,需要在进程的vmas数组中遍历寻找空闲的vma,遍历的过程中也要计算当前正在使用的所有vma的最低地址,这是为了后面添加新的vma。找到空闲的vma后,设置他的地址为刚才找到的最低地址减去sz(因为mmap的地址是从高到低生长)。然后需要调用filedup函数将映射文件的引用数+1

​ 调用mmap函数的时候需要注意文件权限问题。如果文件不可读,vma映射为可读,则mmap失败;如果文件不可写,vma映射为可写,并且开启了回盘标志(MAP_SHARED),则mmap失败。据此写出mmap系统调用函数:

// kernel/sysfile.c
// mmap系统调用实现
uint sys_mmap(void) {
    uint64 addr, sz, offset;
    int prot, flag, fd;
    struct file* f;

    // 读取传入参数
    if (argaddr(0, &addr) < 0 || argaddr(1, &sz) < 0 || argint(2, &prot) < 0 || argint(3, &flag) < 0 || argfd(4, &fd, &f) < 0 || argaddr(5, &offset) < 0 || sz == 0)
        return -1;

    // 以下情况直接返回-1:
    if ((!f->readable && ((prot & (PROT_READ))))                                // 源文件不可读,vma映射为可读
        || (!f->writable && (prot & PROT_WRITE) && !(flag & MAP_PRIVATE)))      // 源文件不可写 ,vam映射为可写并且设置了将修改写回源文件
        return -1;

    sz = PGROUNDUP(sz);
    struct proc* p = myproc();
    struct vma* v = 0;
    uint64 vaend = MMAPEND;

    // 遍历查询未被使用的vma,并且计算当前已使用的va的最低地址
    for (int i = 0;i < NVMA;++i) {
        struct vma* vv = &p->vmas[i];
        if (vv->valid == 0) {           // 若找到了空闲vma就保存下来
            if (v == 0) {
                v = &p->vmas[i];
                v->valid = 1;
            }
        }
        else if (vv->vastart < vaend) {
            vaend = PGROUNDDOWN(vv->vastart);
        }
    }

    // 没找到空闲的vma
    if (v == 0)
        panic("mmap: no free vma");

    // 设置vma属性
    v->vastart = vaend - sz;
    v->sz = sz;
    v->f = f;
    v->prot = prot;
    v->flags = flag;
    v->offset = offset;

    // 增加源文件引用数
    filedup(v->f);

    return v->vastart;
}

kernel/fcntl.h中定义好了相关宏,编译器提示未定义标识符可以不用管:

// kernel/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

​ 映射功能使用写时复制机制实现,即只有在访问的时候才进行磁盘操作。具体原理参考Lab5:

// kernel/sysfile.c
// 通过虚拟地址寻到对应的vma
struct vma* findvma(struct proc* p, uint64 va) {
    for (int i = 0;i < NVMA;++i) {
        struct vma* vv = &p->vmas[i];
        // 如果va地址在某一个vma范围内,则返回这个vma
        if (vv->valid == 1 && va >= vv->vastart && va < vv->vastart + vv->sz) {
            return vv;
        }
    }

    return 0;
}

// 给虚拟地址分配物理页并建立映射
int vmaalloc(uint64 va) {
    struct proc* p = myproc();
    struct vma* v = findvma(p, va);
    if (v == 0)
        return 0;

    // 分配物理地址
    void* pa = kalloc();
    if (pa == 0)
        panic("vmaalloc:kalloc");
    memset(pa, 0, PGSIZE);

    // 从磁盘读取文件
    begin_op();
    ilock(v->f->ip);
    readi(v->f->ip, 0, (uint64)pa, v->offset + PGROUNDDOWN(va - v->vastart), PGSIZE);
    iunlock(v->f->ip);
    end_op();

    // 建立映射
    if (mappages(p->pagetable, va, PGSIZE, (uint64)pa, PTE_R | PTE_W | PTE_U) < 0)
        panic("vmaalloc: mappages");

    return 1;
}
// kernel/trap.c
void
usertrap(void)
{
  ......
  else if ((which_dev = devintr()) != 0) {
      // ok
  }
  else if (r_scause() == 13 || r_scause() == 15){
      uint64 va = r_stval();        // 读取当前发生页面错误的地址
      if (vmaalloc(va) == 0)
          panic("usertrap: wrong va");
  }
  ......
}

​ 接下来需要实现另一个系统调用munmap,释放所有的vma。如果设置了MAP_SHARED,还需要将修改写回磁盘源文件。

munmap传入的参数为释放映射的地址addr,释放地址的范围大小sz。需要检测释放的区域,取消映射的位置要么在区域起始位置,要么在区域结束位置,要么就是整个区域,但是不能在vma中间“打洞”。页有可能不是完整释放,如果 addr 处于一个页的中间,则那个页的后半部分释放,但是前半部分不释放,此时该页整体不应该被释放:

// kernel/sysfile.c
// 释放vma映射的页
uint64 sys_munmap(void) {
    uint64 addr, sz;

    if (argaddr(0, &addr) < 0 || argaddr(1, &sz) < 0 || sz == 0)
        return -1;

    struct proc* p = myproc();
    struct vma* v = findvma(p, addr);
    if (v == 0)
        return -1;

    if (addr > v->vastart && addr + sz < v->vastart + v->sz)        // 释放的区域不能在vma中“打洞”
        return -1;

    uint64 addr_alinged = addr;
    if (addr > v->vastart)
        addr_alinged = PGROUNDUP(addr);

    int nunmap = sz - (addr_alinged - addr);            // 计算要释放的字节数
    if (nunmap < 0)
        nunmap = 0;

    vmaunmap(p->pagetable, addr_alinged, nunmap, v);    // 从addr_alinged开始释放nunmap字节数

    if (addr <= v->vastart && addr + sz > v->vastart) {
        v->offset += addr + sz - v->vastart;
        v->vastart = addr + sz;
    }
    v->sz -= sz;

    if (v->sz <= 0) {
        fileclose(v->f);
        v->valid = 0;
    }

    return 0;
}

vmaunmap函数实现释放映射功能。释放映射之后,需要更新对应vma的offset、vastart、sz字段。如果释放完了vma的sz大小的范围,则应该关闭文件的引用,释放该vma。

vmaunmap函数仿照uvmunmap函数实现, 从传入的参数虚拟地址va开始遍历,查找va + nbytes范围内的每一个页,检查这个页是否被修改过,并且该vma设置了回盘MAP_SHARED,则需要把修改写回磁盘。注意不是每一个页都需要完整的写回,这里需要处理开头页不完整、结尾页不完整以及中间完整页的情况

​ 先加上PTE_D标志位,表示页表被修改过:

// kernel/riscv.h
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_G (1L << 5) 
#define PTE_A (1L << 6) 
#define PTE_D (1L << 7) // 页表被修改过

​ 实现vmaunmap函数:

// kernel/vm.c
// 添加必要的头文件
#include "fcntl.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "proc.h"

// 释放mmap映射的页,根据PTE_D和MAP_SHARED判断是否将修改写回磁盘
void vmaunmap(pagetable_t pagetable, uint64 va, uint64 nbytes, struct vma* v) {
    uint64 a;
    pte_t* pte;

    for (a = va;a < va + nbytes;a += PGSIZE) {
        if ((pte = walk(pagetable, a, 0)) == 0)     // 读取va对应pte
            continue;

        if (PTE_FLAGS(*pte) == PTE_V)
            panic("sys_munmap: not a leaf");

        if (*pte & PTE_V) {
            uint64 pa = PTE2PA(*pte);
            if ((*pte & PTE_D) && (v->flags & MAP_SHARED)) {        // 将修改写回磁盘
                begin_op();
                ilock(v->f->ip);
                uint64 aoff = a - v->vastart;                       // 相对于vma的偏移量
                if (aoff < 0)
                    writei(v->f->ip, 0, pa + (-aoff), v->offset, PGSIZE + aoff);        // 第一页是不满PGSIZE的一个页
                else if (aoff + PGSIZE > v->sz)
                    writei(v->f->ip, 0, pa, v->offset + aoff, v->sz - aoff);            // 最后一页是不满PGSIZE的一个页
                else
                    writei(v->f->ip, 0, pa, v->offset + aoff, PGSIZE);
                
                iunlock(v->f->ip);
                end_op();
            }
            kfree((void*)pa);
            *pte = 0;
        }
    }
}

​ 在proc.c中需要添加对vma的处理

​ 首先是初始化进程时,需要初始化一个进程的vmas数组:

// kernel/proc.c
static struct proc*
allocproc(void)
{
  ......

  // 初始化时清空vmas数组
  for (int i = 0;i < NVMA;++i)
      p->vmas[i].valid = 0;

  return p;
}

​ 释放进程时,要在释放页表前清空vmas数组:

// kernel/proc.c
static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  for (int i = 0;i < NVMA;++i) {                        // 释放页表前把vmas数组清空
      struct vma* v = &p->vmas[i];
      vmaunmap(p->pagetable, v->vastart, v->sz, v);
  }
  if (p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  ......
}

​ fork创建子进程时,子进程复制父进程的vmas数组,不复制物理页:

// kernel/proc.c
int
fork(void)
{
  ......

  // 父进程vmas复制到子进程中,实际内存页和pte不会被复制
  for (int i = 0;i < NVMA;++i) {
      struct vma* v = &p->vmas[i];
      if (v->valid) {
          np->vmas[i] = *v;
          filedup(v->f);
      }
  }

  safestrcpy(np->name, p->name, sizeof(p->name));

  pid = np->pid;

  np->state = RUNNABLE;

  release(&np->lock);

  return pid;
}

​ 主体做好了,现在添加系统调用声明

​ user.h:

// mmaptest中调用mmap时需要返回char*,这里可以把返回值设置为void*
void* mmap(void* addr, uint sz, int prot, int flag, int fd, uint offset);
int munmap(void* addr, uint sz);

usys.pl:

entry("mmap");
entry("munmap");

syscall.h:

#define SYS_mmap   22
#define SYS_munmap 23

syscall.c:

extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);

static uint64 (*syscalls[])(void) = {
......
[SYS_mmap]    sys_mmap,
[SYS_munmap]  sys_munmap,
};

​ 此时可以验证实验是否通过

标签:uint64,sz,addr,映射,Mit6,mmap,vma,Lab10
From: https://www.cnblogs.com/Amroning/p/18555179

相关文章

  • Mit6.S081笔记Lab9: file system 文件系统
    课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.htmlLab地址:https://pdos.csail.mit.edu/6.S081/2020/labs/fs.html我的代码地址:https://github.com/Amroning/MIT6.S081/tree/fsxv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf相关翻译:ht......
  • Mit6.S081笔记Lab7: Multithreading 多线程
    课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.htmlLab地址:https://pdos.csail.mit.edu/6.S081/2020/labs/thread.html我的代码地址:https://github.com/Amroning/MIT6.S081/tree/threadxv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf相......
  • Mit6.S081笔记:知识点记录
    课程地址翻译程序到汇编的转换​ 任何一个处理器都有一个关联的ISA(InstructionSetsArchitecture,指令集架构),ISA就是处理器能够理解的指令集。每一条指令都有一个对应的二进制编码或者一个Opcode。当处理器在运行时,如果看见了这些编码,那么处理器就知道该做什么样的操作。​ 写......
  • Mit6.S081-实验环境搭建
    Mit6.S081-实验环境搭建注:大家每次做一些操作的时候觉得不太保险就先把虚拟机克隆一份前言qemu(quickemulator):这是一个模拟硬件环境的软件,利用它可以运行我们编译好的操作系统。准备一个Linux系统,安装qemu以及其他依赖,通过git克隆下github的xv6源码,利用gcc编译源码得到......
  • Mit6.S081笔记Lab6: Lab6: Copy-on-Write Fork for xv6 写时复制
    课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.htmlLab地址:https://pdos.csail.mit.edu/6.S081/2020/labs/cow.html我的代码地址:https://github.com/Amroning/MIT6.S081/tree/cowxv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf相关翻译......
  • Mit6.S081笔记:页表笔记
    xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf相关翻译:http://xv6.dgs.zone/labs/requirements/lab5.html感觉页表很多地方没理解,学习的时候把一些关键地方记录起来,如有错误恳请各位大佬指正。页表笔记​​ 页表是操作系统为每个进程提供私有地址......
  • Mit6.S081笔记Lab5: Lazy Page Allocation 惰性分配
    课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.htmlLab地址:https://pdos.csail.mit.edu/6.S081/2020/labs/lazy.html我的代码地址:https://github.com/Amroning/MIT6.S081/tree/lazyxv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf相关翻......
  • 内存管理相关——malloc,mmap,mlock与unevictable列表
    一、背景之前的内核内存子系统的章节里已经介绍了内存回收有关的MIGRATE_TYPE的概念并做了不少的相关实验。详细见我之前写的博客 内存回收相关内核基础概念——MIGRATE_TYPE_kreclaimable没有回收-CSDN博客。锁内存相关的常用函数有四个,SetPageReserved/mlock/get_user_pa......
  • Mit6.S081笔记Lab3: page tables 页表
    课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.htmlLab地址:https://pdos.csail.mit.edu/6.S081/2020/labs/pgtbl.html我的代码地址:https://github.com/Amroning/MIT6.S081/tree/pgtbl相关翻译:http://xv6.dgs.zone/labs/requirements/lab3.html参考博客:https://ww......
  • Mit6.S081笔记Lab2: system calls 系统调用
    课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.htmlLab地址:https://pdos.csail.mit.edu/6.S081/2020/labs/syscall.html我的代码地址:https://github.com/Amroning/MIT6.S081/tree/syscall相关翻译:http://xv6.dgs.zone/labs/requirements/lab2.html参考博客:https:......