Lab 9 mmap

这个 Lab 要求我们要实现简单版的 mmap,它把文件映射到内存中以提高访问效率。mmap 的定义可以参考 man 2 mmap,总之它的参数如下:

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

我们跳过系统调用的配置,因为想必大家已经品鉴的够多了。

sys_mmap 和 vmfault

Lab 要求 lazy allocation,因此我们需要把 mmap 被调用时传入的参数记录在一个结构体中以便后续在 page fault 时按需分配,我设计的结构体如下:

struct {
    int valid;      // Is this idx being used?
    uint64 st;     // Start of the mmap, page aligned
    int len;     // Number of bytes to map
    int prot;       // PROT_READ or PROT_WRITE or both
    int flags;      // MAP_SHARED or MAP_PRIVATE
    struct file *f;         // Mapped file
    int off;
} mmap[NMMAP];

其中 NMMAP 被定义在 param.h 中。这里定义成 16 是因为 Lab 文档说用长度为 16 的定长数组来记录 mmap 信息就够了。

#define NMMAP 16

这里简单聊下我是怎么设计结构体的。我在设计结构体时习惯快速写一个初版,然后在写后续代码的过程中迭代。像这里的初版就是把 mmap 的参数全塞进去再加个 valid 位,然后写 sys_mmap 时发现 offset 默认是 0 于是就删掉了 off,后来在写 sys_munmap 时又发现 off 是有用的就加了回来。

有了结构体以后我们就能实现 sys_mmap 了。在这个函数里简单地保存数据即可,无需复制文件内容到物理内存中,毕竟我们是 lazy allocation.

uint64 sys_mmap(void) {
    int len, prot, flags;
    struct file *f;
    struct proc *p;
    int i;

    // Ignore args[0], `addr`, which is always zero
    argint(1, &len);
    argint(2, &prot);
    argint(3, &flags);
    if (argfd(4, 0, &f) < 0) {
        return -1;
    }
    // Ignore arg[5], `offset`, which is always zero

    if (!f->writable && (flags & MAP_SHARED) && (prot & PROT_WRITE)) {
        return -1;
    }

    p = myproc();
    for (i = 0; i < NMMAP; i++) {
        if (!p->mmap[i].valid) {
            p->mmap[i].valid = 1;
            p->mmap[i].st = PGROUNDUP(p->sz);
            p->mmap[i].len = len;
            p->mmap[i].prot = prot;
            p->mmap[i].flags = flags;
            p->mmap[i].f = f;
            p->mmap[i].off = 0;
            filedup(f);
            break;
        }
    }
    if (i == NMMAP) {
        // No space to save metadata for this mmap
        return -1;
    }

    p->sz = PGROUNDUP(p->sz) + PGROUNDUP(len);

    return p->mmap[i].st;
}

然后我们需要实现 page fault 来把传入 mmap 的文件复制到物理内存中。和 cow lab 类似,要修改 trap.c 的 usertrap 函数:

if (r_scause() == 8) {
    // system call
    // ...
} else if ((which_dev = devintr()) != 0) {
    // ok
} else if ((r_scause() == 15 || r_scause() == 13) &&
           vmfault(p->pagetable, r_stval(), (r_scause() == 13) ? 1 : 0) != 0) {
    // page fault on mmap page
} else {
    // kill process
    // ...
}

然后这里是我对 vmfault 的实现:

int ismapped(pagetable_t pagetable, uint64 va) {
    pte_t *pte = walk(pagetable, va, 0);
    if (pte == 0) {
        return 0;
    }
    if (*pte & PTE_V) {
        return 1;
    }
    return 0;
}

uint64 vmfault(pagetable_t pagetable, uint64 va, int read) {
    printf("vmfault\n");
    printf("va: %lu, related page: %lu\n", va, PGROUNDDOWN(va));
    uint64 mem;
    int perm, i;
    uint off;
    struct file *f = 0;
    struct proc *p = myproc();

    if (va >= p->sz)
        return 0;
    if (ismapped(pagetable, va)) {
        return 0;
    }

    // Find mmap metadata corresponding to va
    for (i = 0; i < NMMAP; i++) {
        if (p->mmap[i].valid && p->mmap[i].st <= va &&
            va < p->mmap[i].st + p->mmap[i].len) {
            f = p->mmap[i].f;
            break;
        }
    }
    if (!f)
        return 0;

    // Alloc new page and fill with file data
    mem = (uint64)kalloc();
    if (mem == 0)
        return 0;
    memset((void *)mem, 0, PGSIZE);
    ilock(f->ip);
    off = PGROUNDDOWN(va) - p->mmap[i].st + p->mmap[i].off;
    if (off > f->ip->size)
        return 0;
    readi(f->ip, 0, mem, off, PGSIZE);
    iunlock(f->ip);

    // Map page
    perm = PTE_U;
    if (p->mmap[i].prot & PROT_READ)
        perm |= PTE_R;
    if (p->mmap[i].prot & PROT_WRITE)
        perm |= PTE_W;
    if (mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, mem, perm) != 0) {
        kfree((void *)mem);
        return 0;
    }

    return mem;
}

至此我们就能通过第一个 munmap 之前的所有测试了。我们可能会疑惑,如果传入 mmap 的 len 比 f->ip->size 大很多怎么办?对照 man 文档可以发现我们应该在访问非文件区域时(超出文件大小的区域)应该发 SIGBUS 信号,但所幸测试代码没有测这一点,因此我们只要按需分页并复制就行了。如实验文档所言,“If mmaptest doesn’t use a mmap feature, you don’t need to implement that feature.”

sys_munmap

接下来我们实现一个便宜版本的 sys_munmap. 感觉在这个 Lab 里要不断回顾 ”If mmaptest doesn’t use a mmap feature, you don’t need to implement that feature”,不然实现起来就没完没了了🫠🫠

在 Lab 中,munmap 总是在尝试 unmap 一块形如 [start of a map, mid of this map] 或 [mid of a map, end of this map] 或 [start of a map, end of this map] 的区域。总之就是它只在一个 map 区域内 unmap,且不会挖洞。

自然而然地我们会想问,如果传入的参数覆盖了多个 mmap 区域呢?如果覆盖了普通内存呢?如果挖了个洞呢?

我只能说,不要多想🫠🫠🫠🫠

但提出了问题总归是要给解答的,虽然我没有实现这些功能,而且不实现它们也能通过测试。根据 man 手册推测这几种情况的行为如下:

  1. 如果传入的参数覆盖了多个 mmap 区域,把它们都 unmap 掉(潜在的风险是每个区域的 flags 可能不同)
  2. 如果覆盖了普通内存,把那块内存释放掉
  3. 如果挖了个洞,把这块 mmap 区域分割成两块 mmap 区域

回到我们的 Lab. 在实现 sys_munmap 前有两点要注意,第一点是在 unmap 时要检查 map 区域是否被设为 MAP_SHARED,如果是要把内容写回文件;第二点是我们现在使用 lazy allocation,所以虚拟内存空间可能有未映射的虚拟页,因此要修改 uvmunmap 让它在遇到未映射的虚拟页时跳过而非 panic.

先来看看第一点,写回文件的实现如下,如 hint 所言参考 filewrite 就行:

// Write parts of a shared mmap[i] back to file
// theses parts must in mappepd pages and addr must be page-aligned
int mmap_wbk(int i, uint64 addr, int len) {
    struct file *f;
    uint64 fileend, src;
    struct proc *p;
    int pg;

    if (addr % PGSIZE != 0) {
        return -1;
    }

    p = myproc();

    if (i >= NMMAP || !(p->mmap[i].flags & MAP_SHARED) ||
        addr < p->mmap[i].st || addr + len > p->mmap[i].st + p->mmap[i].len) {
        return -1;
    }

    f = p->mmap[i].f;
    ilock(f->ip);
    fileend = p->mmap[i].st - p->mmap[i].off + f->ip->size;
    iunlock(f->ip);

    // Write valid pages back to file
    for (pg = 0; pg * PGSIZE < len; pg++) {
        if (walkaddr(p->pagetable, addr + pg * PGSIZE)) {
            int max = ((MAXOPBLOCKS - 1 - 1 - 2) / 2) * BSIZE;
            int tot = min(PGSIZE, fileend - (addr + pg * PGSIZE));
            int r, written = 0;

            while (written < tot) {
                int n = tot - written;
                if (n > max)
                    n = max;

                begin_op();
                ilock(f->ip);
                src = addr + pg * PGSIZE + written;
                r = writei(f->ip, 1, src, src - p->mmap[i].st + p->mmap[i].off,
                           n);
                iunlock(f->ip);
                end_op();

                if (r != n) {
                    return -1;
                }
                written += r;
            }
        }
    }
    return 0;
}

这里以页为单位写回文件是因为 lazy allocation 导致可能存在未被映射的虚拟页,我们不希望把未映射的页写回文件。另外要注意写回的长度不能超过文件本身长度,也就是说对每一个尝试写回的页,实际写入的长度为:

int tot = min(PGSIZE, fileend - (addr + pg * PGSIZE));

我把这个 mmap_wbk 函数放在 proc.c 中,因为后续在 exit 函数里也要用到它。

然后是第二点,让 uvmunmap 在遇到未映射的虚拟页时跳过而非 panic,我们简单改了一下 uvmunmap:

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) {
            continue;
            // panic("uvmunmap: walk");
        }
        if ((*pte & PTE_V) == 0) {
            continue;
            // 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;
    }
}

最后让我们来看看 sys_munmap:

#define min(a, b) ((a) < (b) ? (a) : (b))
uint64 sys_munmap(void) {
    struct file *f;
    uint64 addr;
    int len;
    struct proc *p;
    int i;

    argaddr(0, &addr);
    argint(1, &len);

    if (addr % PGSIZE != 0) {
        return -1;
    }

    p = myproc();

    // Find mmap corresponding to addr
    for (i = 0; i < NMMAP; i++) {
        if (p->mmap[i].valid && p->mmap[i].st <= addr &&
            addr < p->mmap[i].st + p->mmap[i].len) {
            f = p->mmap[i].f;
            break;
        }
    }
    if (i == NMMAP) {
        // No mmap corresponding to addr
        uvmunmap(p->pagetable, addr, PGROUNDUP(len) / PGSIZE, 1);
        return 0;
    }

    if (p->mmap[i].flags & MAP_SHARED) {
        if (mmap_wbk(i, addr, min(len, p->mmap[i].st + p->mmap[i].len - addr)) <
            0) {
            return -1;
        };
    }

    uvmunmap(p->pagetable, addr, PGROUNDUP(len) / PGSIZE, 1);

    if (addr == p->mmap[i].st && addr + len == p->mmap[i].st + p->mmap[i].len) {
        p->mmap[i].valid = 0;
        fileclose(f);
        return 0;
    } else if (addr == p->mmap[i].st) {
        p->mmap[i].st += PGROUNDUP(len);
        p->mmap[i].len -= PGROUNDUP(len);
        p->mmap[i].off += PGROUNDUP(len);
        return 0;
    } else if (addr + len == p->mmap[i].st + p->mmap[i].len) {
        p->mmap[i].len -= PGROUNDUP(len);
        return 0;
    } else {
        // No need to consider hole punching case in this lab
    }

    return -1;
}

它能通过测试代码,也算是够用了吧🫠🫠

exit 和 fork

有了 munmap 的经验,exit 函数的修改就很简单了。我把关闭 mmap 的代码放在了关闭文件后面:

void exit(int status) {
    // ...
    
    // Close all open files.
    // ...

    // Close all mmaps
    for (int i = 0; i < NMMAP; i++) {
        if (p->mmap[i].valid) {
            struct file *f;
            uint64 addr;

            f = p->mmap[i].f;
            addr = p->mmap[i].st;

            if (p->mmap[i].flags & MAP_SHARED) {
                if (mmap_wbk(i, addr, p->mmap[i].len) < 0) {
                    panic("mmap wbk failed");
                };
            }
            for (int pg = 0; pg * PGSIZE < p->mmap[i].len; pg++) {
                if (walkaddr(p->pagetable, addr + pg * PGSIZE)) {
                    uvmunmap(p->pagetable, addr + pg * PGSIZE, 1, 1);
                }
            }
            p->mmap[i].valid = 0;
            fileclose(f);
        }
    }

    // ...
}

而 fork 的修改也不难:

int fork(void) {
    // ...

    // increment reference counts on open file descriptors.
    // ...

    // Copy mmap
    for (i = 0; i < NMMAP; i++) {
        if (p->mmap[i].valid) {
            np->mmap[i] = p->mmap[i];
            filedup(np->mmap[i].f);
        }
    }

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

    // ...
}

再见了,所有的 6S081

完结撒花,感谢陪伴!Bilibili 干杯 - ( ゜- ゜)つロ[