6.S081 lab10 mmap

mmapmunmap系统调用允许 UNIX 程序对其地址空间进行更为细致的控制。它们可用于在进程间共享内存,将文件映射到进程地址空间,并作为用户级page fault方案的一部分。在本实验室中,我们将在xv6中添加mmapmunmap系统调用,重点是memory-mapped files

Lab: mmap

mmap的 API 如下:

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

xv6中,addr始终为 0,所以由kernel自行判断应当 map 的地址;prot表示了 mapped memory 的 R、W、X 权限,flagsMAP_SHARED或者MAP_PRIVATE,前者表示对 mapped memory 的修改应写回文件,后者则不需要;offer永远为 0,不用处理文件的偏移量;mmap 成功将返回对应内存起始地址;失败返回0xffffffffffffffff

munmap(addr, length)需要将从 addr 开始的长度为length的内存unmap。实验指导书保证被munmap的这段内存位于mmap内存区间的头部/尾部或者是全部,munmap不会在中间挖一个洞;当内存是以MAP_SHARED模式被mmap时,需要先将修改写回文件。

看完了对于mmapmunmap的要求,发现其实测试没有一些比较难的case,为我们的实现提供了便利。

之后,就可以跟着 hints 完成实验:

  • 首先添加mmapmunmap的系统调用声明,并且在Makefile中加入_mmaptest

  • 对 mapped memory 要使用 lazy allocation,就像在之前的实验中那样,这样子使得我们可以在物理内存有限的情况下mmap尽可能大的文件。

  • 记录mmap为每个进程 map 文件的情况,例如地址,长度,权限,对应的文件等等。由于xv6没有真正的内存分配器,所以我们使用一个定长的数组去存储,16 就足够了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct VMA {
    int used;
    uint64 addr;
    uint64 end;
    int prot;
    int flags;
    int offset;
    struct file *f;
    };

    // in struct proc
    struct VMA vma[NVMA];
    uint64 mmap_start;
  • 实现mmap,从用户地址空间找到空闲处 map 文件,修改对应的VMA结构体记录,当对文件mmap后,应当增加文件的引用计数(filedup),这样当文件被关闭时,VMA持有的文件指针才不会失效。

    最重要的就是找到合适的空闲地址,用于mmapxv6的用户地址空间如下图:

    image-20210303172730172

    最顶部是trampolinetrapframe,它们占用了两个 page,和stack之间有很大的空闲地址,我们可以将文件 map 到trapframe之下,不断向下增长,mmap_start记录着trapframe下可用于mmap的起始地址,初始值为PGROUNDDOWN(MAXVA - (2 * PGSIZE))

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    uint64
    sys_mmap(void)
    {
    int length, prot, flags, fd;
    struct file *f;
    if(argint(1, &length) < 0 || argint(2, &prot) < 0
    || argint(3, &flags) < 0 || argfd(4, &fd, &f) < 0) {
    return 0xffffffffffffffff;
    }
    if (!f->writable && flags == MAP_SHARED && (prot & PROT_WRITE)) {
    return 0xffffffffffffffff;
    }
    // find a vma
    struct proc *p = myproc();
    struct VMA *v;
    for (v = p->vma; v < p->vma + NVMA; v++) {
    if(!v->used) {
    break;
    }
    }
    if(v == p->vma + NVMA) {
    return -1;
    }
    filedup(f);
    v->addr = PGROUNDDOWN(p->mmap_start - length);
    v->end = v->addr + length;
    p->mmap_start = v->addr;

    v->used = 1;
    v->f = f;
    v->prot = prot;
    v->flags = flags;
    v->offset = 0;
    return v->addr;
    }
  • 当发生page fault时,为其分配一个真实的物理页面,使用readi将文件内容读入内存,然后将物理页面 map 到用户地址空间,记得正确设置页面的权限。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    else if (r_scause() == 13 || r_scause() == 15) {
    uint64 va = r_stval();
    if (va > MAXVA) {
    p->killed = 1;
    } else {
    if(mmap_alloc(p->pagetable, va) < 0) {
    p->killed = 1;
    }
    }
    }

    int
    mmap_alloc(pagetable_t pagetable, uint64 va)
    {
    char *mem;
    struct proc *p = myproc();
    struct VMA *v;
    // find vma struct
    for (v = p->vma; v < p->vma + NVMA; v++) {
    if(v->used && va >= v->addr && va < v->end) {
    break;
    }
    }
    if (v == p->vma + NVMA) {
    return -1;
    }
    mem = kalloc();
    if(mem == 0){
    return -1;
    }
    memset(mem, 0, PGSIZE);
    begin_op();
    ilock(v->f->ip);
    int len;
    if((len = readi(v->f->ip, 0, (uint64)mem, va - v->addr, PGSIZE)) < 0) {
    iunlock(v->f->ip);
    end_op();
    return -1;
    }
    iunlock(v->f->ip);
    end_op();
    int f = PTE_U | (v->prot << 1);
    if(mappages(pagetable, va, PGSIZE, (uint64)mem, f) != 0) {
    kfree(mem);
    return -1;
    }
    return 0;
    }
  • 实现munmap,找到对应的VMA,使用uvmunmap unmap 对应的内存,当一个mmap的所有内存都被 unmap 时,需要减少对应文件的引用计数;如果内存被修改过,且是以MAP_SHARED模式被mmap,那么需要先将内存内容写回文件。理想态下,我们只应当写回dirty page,但是测试中不会检查这一点,所以将所有内存写回文件即可了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    struct file*
    fileundup(struct file *f)
    {
    acquire(&ftable.lock);
    if(f->ref < 1)
    panic("filedup");
    f->ref--;

    release(&ftable.lock);
    return f;
    }

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

    }

    uint64
    s_munmap(uint64 addr, int length) {
    struct proc *p = myproc();
    struct VMA *v;
    for (v = p->vma; v < p->vma + NVMA; v++) {
    if(v->used && v->addr <= addr && addr + length <= v->end) {
    break;
    }
    }
    if(v == p->vma + NVMA) {
    return -1;
    }

    uint64 end = addr + length;
    uint64 _addr = addr;
    while (addr < end) {
    // if already load in
    if(walkaddr(p->pagetable, addr)) {
    if(v->flags == MAP_SHARED && v->f->writable) {
    begin_op();
    ilock(v->f->ip);
    int size = min(end-addr, PGSIZE);
    if(writei(v->f->ip, 1, addr, addr - v->addr, size) < size) {
    iunlock(v->f->ip);
    end_op();
    return -1;
    }
    iunlock(v->f->ip);
    end_op();
    }
    uvmunmap(p->pagetable, addr, 1, 1);
    }
    addr += PGSIZE;
    }
    if(_addr == v->addr) {
    v->addr += length;
    } else if(_addr + length == v->end) {
    v->end -= length;
    }

    if (v->addr == v->end) {
    fileundup(v->f);
    v->used = 0;
    }

    return 0;
    }

    实现时遇到两个坑:

    • uvmunmap时,首先要判断该内存是否真的被lazy allocation了,否则要像lazylab 中一样,修改uvmunmap,我觉得这样实现比较 ugly,因为破坏了uvmunmap发现错误的功能。
    • 最开始实现时,只要v->flags == MAP_SHARED就将文件写回,结果在forktest中父子进程内存内容不一致,查看forktest代码发现原来创建的只读文件,只要prot不标志为可写,那么制度文件也是可以用MAP_SHARED模式进行mmap的。所以还要加上v->f->writable或者prot & PROT_WRITE
  • 修改exit代码,使得exit被调用后,unmap 所有被mmap的内存。

    1
    2
    3
    4
    5
    6
    7
    8
    struct VMA *v;
    for (v = p->vma; v < p->vma + NVMA; v++) {
    if(v->used) {
    if (s_munmap(v->addr, v->end - v->addr) < 0) {
    panic("exit:munmap");
    }
    }
    }
  • 修改 fork 代码,使得子进程拥有和父进程相同的mmap文件。可以直接为子进程分配新的物理内存用于mmap,不用共享相同物理页面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    np->mmap_start = p->mmap_start;
    for(i = 0; i < NVMA; i++) {
    np->vma[i].addr = p->vma[i].addr;
    np->vma[i].end = p->vma[i].end;
    np->vma[i].used = p->vma[i].used;
    np->vma[i].flags = p->vma[i].flags;
    np->vma[i].prot = p->vma[i].prot;

    if(p->vma[i].used && p->vma[i].f) {
    np->vma[i].f = filedup(p->vma[i].f);
    }
    }

这样 mmap lab 就完成了,最终代码见GitHub 仓库

作者

Naruto210

发布于

2021-03-03

更新于

2021-08-11

许可协议

评论