MIT 6.1810 · Lab mmap · 内存映射
Lab: mmap 困难
mmap 和 munmap 系统调用允许 UNIX 程序对其地址空间施加详细控制。在本实验中,你将向 xv6 添加 mmap 和 munmap,重点关注内存映射文件。
$ git fetch
$ git checkout mmap
$ make clean
手册页(运行 man 2 mmap)显示 mmap 的声明:
void *mmap(void *addr, size_t len, int prot, int flags,
int fd, off_t offset);
本实验只需要与内存映射文件相关的子集功能。你可以假设:
- addr 始终为零——内核决定映射文件的虚拟地址
- len 是要映射的字节数;可能与文件长度不同
- prot 是 PROT_READ 或 PROT_WRITE 或两者
- flags 是 MAP_SHARED(修改写回文件)或 MAP_PRIVATE(不写回)
- fd 是要映射的文件的打开文件描述符
- offset 为零
你的实现应该惰性填充页表,响应 page fault。也就是说,mmap 本身不应该分配物理内存或读取文件。相反,在 page fault 处理代码中执行这些操作。原因是确保大文件的 mmap 快速,以及映射大于物理内存的文件是可能的。
munmap 的声明:
int munmap(void *addr, size_t len);
munmap 应该删除指示地址范围中的 mmap 映射(如果有)。如果进程修改了内存并且它映射为 MAP_SHARED,修改应该首先写入文件。
你应该实现足够的 mmap 和 munmap 功能使 mmaptest 测试程序工作。你还必须确保 usertests -q 继续工作。
$ mmaptest
test basic mmap
test basic mmap: 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 lazy access
test lazy access: OK
test mmap two files
test mmap two files: OK
test fork
test fork: OK
test munmap prevents access
usertrap(): unexpected scause 0xd pid=7
sepc=0x924 stval=0xc0001000
test munmap prevents access: OK
test writes to read-only mapped memory
usertrap(): unexpected scause 0xf pid=9
sepc=0xaf4 stval=0xc0000000
test writes to read-only mapped memory: OK
mmaptest: all tests succeeded
$ usertests -q
usertests starting
...
ALL TESTS PASSED
$
以下是一些提示:
- 首先将 _mmaptest 添加到 UPROGS,并添加 mmap 和 munmap 系统调用,以使 user/mmaptest.c 编译。现在,只需从 mmap 和 munmap 返回错误。我们在 kernel/fcntl.h 中为你定义了 PROT_READ 等。运行 mmaptest,它会在第一次 mmap 调用时失败。
- 跟踪 mmap 为每个进程映射了什么。定义一个对应于"应用程序虚拟内存"讲座中描述的 VMA(虚拟内存区域)的结构。这应该记录由 mmap 创建的虚拟内存范围的地址、长度、权限、文件等。由于 xv6 内核没有可变大小的内存分配器,声明一个固定大小的 VMA 数组并根据需要从该数组分配是可以的。大小 16 应该足够。
- 实现 mmap:在进程的地址空间中找到一个未使用的区域来映射文件,并将 VMA 添加到进程的映射区域表中。VMA 应该包含一个指向被映射文件的 struct file 的指针;mmap 应该增加文件的引用计数,以便文件关闭时结构不会消失(提示:参见 filedup)。运行 mmaptest:第一次 mmap 应该成功,但第一次访问 mmap 的内存会导致 page fault 并杀死 mmaptest。
- 添加代码使 mmap 区域中的 page fault 分配一页物理内存,将相关文件的 4096 字节读取到该页中,并将其映射到用户地址空间中。使用 readi 读取文件,它接受文件中读取的偏移量参数(但你必须锁定/解锁传递给 readi 的 inode)。不要忘记正确设置页的权限。运行 mmaptest;它应该到达第一次 munmap。
- 实现 munmap:找到地址范围的 VMA 并取消映射指定的页(提示:使用 uvmunmap)。如果 munmap 删除了前一个 mmap 的所有页,它应该减少相应 struct file 的引用计数。如果已取消映射的页已被修改且文件映射为 MAP_SHARED,将该页写回文件。查看 filewrite 获取灵感。
- 理想情况下,你的实现只会写回程序实际修改的 MAP_SHARED 页。RISC-V PTE 中的脏位(D)指示页是否已被写入。然而,mmaptest 不检查非脏页是否未写回;因此你可以不查看 D 位就写回页。
- 修改 exit 以取消映射进程的映射区域,就像调用了 munmap 一样。运行 mmaptest;所有测试通过 test mmap two files 应该通过,但可能不是 test fork。
- 修改 fork 以确保子进程具有与父进程相同的映射区域。不要忘记增加 VMA 的 struct file 的引用计数。在子进程的 page fault handler 中,分配新物理页而不是与父进程共享页是可以的。运行 mmaptest;它应该通过所有测试。
运行 usertests -q 确保一切仍然工作。
提交实验
时间花费
创建一个新文件 time.txt,放入一个整数,表示你花在实验上的小时数。
答案
如果本实验有问题,将答案写在 answers-mmap.txt 中。
提交
运行 make zipball,上传到 Gradescope。
- 请运行 make grade 确保代码通过所有测试
- 在运行 make zipball 之前提交任何修改的源代码
可选挑战
- 如果两个进程 mmap 同一个文件(如 fork 测试中),共享它们的物理页。你需要物理页的引用计数。
- 你的解决方案可能为从 mmap 文件读取的每个页分配新物理页,即使数据也在内核内存中的缓冲缓存中。修改你的实现使用那些物理内存,而不是分配新页。这需要文件大小与页大小相同(设置 BSIZE 为 4096)。你需要将 mmap 的块固定在缓冲缓存中。
- 移除惰性分配和 mmap 文件实现之间的冗余。(提示:为惰性分配区域创建 VMA。)
- 修改 exec 使用 VMA 处理二进制文件的不同部分,使你可以按需分页的可执行文件。
- 实现页出和页入:当物理内存低时,内核将进程的部分移动到磁盘上。然后,当进程引用分页出的内存时,将其分页进来。