Lab: mmap (hard)

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

运行man 2 mmap可得到手册中mmap的声明:

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

要求

可以通过多种方式调用 mmap,但此练习只需要与文件内存映射相关的功能子集。

  • 您可以假设 addr 将始终为零,这意味着内核应决定映射文件的虚拟地址。mmap 返回该地址,如果失败,则返回0xffffffffffffffff
  • length是要映射的字节数; 它可能与文件的长度不同。
  • prot 指示内存是否应映射为可读、可写和/或可执行; 您可以假设 protPROT_READ PROT_WRITE 或两者兼而有之。
  • flags要么是MAP_SHARED,这意味着对映射内存的修改应该写回文件,要么是MAP_PRIVATE,这意味着它们不应该写回文件。你不必在flags中实现任何其他位。
  • fd 是要映射的文件的打开文件描述符。
  • 您可以假设偏移量为零(文件中要映射的起点)。

对于映射同一MAP_SHARED文件的不同进程,禁止共享物理页。

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

您应该实现足够的 mmapmunmap 功能,以使 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
$ 

提示

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

实现

  1. 添加mmapmunmap系统调用:添加系统调用通用步骤(在用户空间声明系统调用、添加条目,在内核空间增加系统调用命令序号以及对应的sys_mmap()sys_munmap()函数)
  2. proc.hstruct proc中添加映射区域:首先定义映射区域结构体struct MapArea及一个进程映射区域的数量:

    #define MAP_AREA_LENGTH 16
    
    struct MapArea {
        uint64 address;  // 映射开始地址
        int length;  // 映射区域长度
        int prot;  
        int flag;  
        int offset;  
        struct file* fp;  
    };

    然后在struct proc中添加表示映射区域的数组字段:

    struct proc {
        ...
        struct MapArea mapareas[MAP_AREA_LENGTH];
    };
  3. 实现sys_mmap()的功能:

    uint64 sys_mmap(void) {
        uint64 addr;
        int len, prot, flag, fd, offset, i;
        struct file* fp;
        struct proc* p = myproc();
    
        argaddr(0, &addr);
        argint(1, &len);
        argint(2, &prot);
        argint(3, &flag);
        if (argfd(4, &fd, &fp) < 0) return -1;
        argint(5, &offset);
    
        // 判断权限是否冲突
        if (!fp->writable && (prot & PROT_WRITE) && (flag & MAP_SHARED)) {
            printf("file is not writable!\n");
            return -1;
        }
    
        // 找空闲map area
        for (i = 0; i < MAP_AREA_LENGTH; ++i) {
            if (p->mapareas[i].address == 0) break;
        }
        if (i == MAP_AREA_LENGTH) {
            printf("no more map area!\n");
            return -1;
        }
    
        // 若未指定地址则需要分配地址
        if (!addr) {
            addr = PGROUNDUP(p->sz);
            p->sz += PGROUNDUP(len);
        }
    
        // 复制字段
        p->mapareas[i].address = addr;
        p->mapareas[i].length = len;
        p->mapareas[i].prot = prot;
        p->mapareas[i].flag = flag;
        p->mapareas[i].offset = offset;
        p->mapareas[i].fp = fp;
        // 文件引用计数+1
        filedup(fp);
        return addr;
    }
  4. usertrap()中识别页面错误并将文件内容写入对应地址空间:首先需要在usertrap()原本的选择分支框架中添加页面错误的分支,在该分支中分配内存并将文件内容写入到指定位置。这里通过map_fill()函数完成此功能。

    void usertrap(void) {
        ...
        if(r_scause() == 8){
            // system call
            ...
        } else if (r_scause() == 13 || r_scause() == 15) {
            // 页面错误,需要分配内存并写入文件内容
            if (map_fill(r_stval()) == 0) {
                goto error;
            }
        } else if((which_dev = devintr()) != 0){
            // ok
        } else {
    error:
            printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
            printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
            setkilled(p);
        }
        ...
    }

    map_fill()函数完成分配内存并写入文件内容的功能:

    uint64 map_fill(uint64 va) {
        int idx, perm, prot, offset;
        struct inode* ip;
        void* new_mem;
        struct proc* p = myproc();
    
        // 找到哪个map area
        for (idx = 0; idx < MAP_AREA_LENGTH; ++idx) {
            if (va >= p->mapareas[idx].address && 
                va < p->mapareas[idx].address + p->mapareas[idx].length) break;
        }
        if (idx == MAP_AREA_LENGTH) return 0;
    
        // 分配内存
        if ((new_mem = kalloc()) == 0) {
            return 0;
        }
    
        // 物理虚拟地址建立映射
        prot = p->mapareas[idx].prot;
        perm = PTE_U;
        if (prot & PROT_READ) perm |= PTE_R;
        if (prot & PROT_WRITE) perm |= PTE_W;
        if (prot & PROT_EXEC) perm |= PTE_X;
        if (mappages(p->pagetable, va, PGSIZE, (uint64)new_mem, perm) == -1) {
            kfree(new_mem);
            return 0;
        }
    
        // 拷贝文件内容  
        ip = p->mapareas[idx].fp->ip;
        if (ip == 0) {
            printf("ip == 0\n");
            return 0;
        }
        offset = p->mapareas[idx].offset;
        ilock(ip);
        if (p->mapareas[idx].length - offset > PGSIZE) {
            readi(ip, 1, va, offset, PGSIZE);
        } else {
            readi(ip, 1, va, offset, p->mapareas[idx].length - offset);
        }
        p->mapareas[idx].offset += PGSIZE;
        iunlock(ip);
        return (uint64)new_mem;
    }
  5. 实现sys_munmap()

    uint64 sys_munmap(void) {
        uint64 addr;
        int length, idx;
        struct proc* p = myproc();
        argaddr(0, &addr);
        argint(1, &length);
    
        // 找到addr对应的map area
        for (idx = 0; idx < MAP_AREA_LENGTH; ++idx) {
            if (addr >= p->mapareas[idx].address && addr < p->mapareas[idx].address + p->mapareas[idx].length) break;
        }
        if (idx == MAP_AREA_LENGTH) return -1;
    
        // MAP_SHARED写回文件
        if (p->mapareas[idx].flag & MAP_SHARED) {
            filewrite(p->mapareas[idx].fp, addr, length);
        }
    
        // 解除映射
        uvmunmap(p->pagetable, addr, PGROUNDUP(length) / PGSIZE, 1);
        
        if (PGROUNDUP(length) >= p->mapareas[idx].length) {
            // 若解除了所有映射,则文件引用计数-1,并将地址置0以标记该区域空闲
            fileclose(p->mapareas[idx].fp);
            p->mapareas[idx].address = 0;
        } else {
            // 若未解除所有映射,则调整映射区域的范围(地址及长度)
            p->mapareas[idx].length -= PGROUNDUP(length);
            p->mapareas[idx].address += PGROUNDUP(length);
        }
    
        return 0;
    }
  6. exit()中解除映射区域的映射:

    void exit(int status) {
        struct proc *p = myproc();
    
        if(p == initproc)
            panic("init exiting");
    
        // 解除map area的mmap
        for (int i = 0; i < MAP_AREA_LENGTH; ++i) {
            if (p->mapareas[i].address) {
                // MAP_SHARED写回文件
                if (p->mapareas[i].flag & MAP_SHARED) {
                    filewrite(p->mapareas[i].fp, p->mapareas[i].address, p->mapareas[i].length);
                }
                // 解除映射
                uvmunmap(p->pagetable, p->mapareas[i].address, 
                        PGROUNDUP(p->mapareas[i].length) / PGSIZE, 1);
                fileclose(p->mapareas[i].fp);
                p->mapareas[i].address = 0;
            }
        }
        ...
    }
  7. fork()中复制map area

    int fork(void) {
        ...
        pid = np->pid;
    
        // 拷贝映射区
        for (i = 0; i < MAP_AREA_LENGTH; ++i) {
            if (p->mapareas[i].address) {
                np->mapareas[i] = p->mapareas[i];
                filedup(np->mapareas[i].fp); // 文件引用计数+1
            }
        }
        ...
    }

问题

  1. panic: uvmunmap: not mapped
    image.png
    原因:munmap()时,实际上可能存在部分文件内容没有访问、因而没有触发页面错误、没有建立映射的情况,这时候是对着没有映射过的内存解除映射,uvmunmap()对这种情况会报错。
    我的处理方法是将uvmunmap()(*pte & PTE_V) == 0的情况由原来的panic(...)改为continue。碰到没有映射过的内存时,不触发panic而是忽略掉继续运行。
    fork()uvmcopy()时碰过到类似情况也做类似处理。

结果

image.png

收获

  1. mmap()的作用是将(部分)文件内容与一段内存建立映射关系,以加速程序对文件的访问。
  2. mmap()有两种形式,其中MAP_SHARED模式不仅可以读文件,还可以将程序对映射内存的改动写回到文件中(如果文件可写)

Longfar
1 声望4 粉丝