Lab10: mmap

mmap(hard)

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

获取实验室的xv6源代码并切换到mmap分支:

  1. $ git fetch
  2. $ git checkout mmap
  3. $ make clean

手册页面(运行man 2 mmap)显示了mmap的以下声明:

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

可以通过多种方式调用mmap,但本实验只需要与内存映射文件相关的功能子集。您可以假设addr始终为零,这意味着内核应该决定映射文件的虚拟地址。mmap返回该地址,如果失败则返回0xfffffffffffffffflength是要映射的字节数;它可能与文件的长度不同。prot指示内存是否应映射为可读、可写,以及/或者可执行的;您可以认为protPROT_READPROT_WRITE或两者兼有。flags要么是MAP_SHARED(映射内存的修改应写回文件),要么是MAP_PRIVATE(映射内存的修改不应写回文件)。您不必在flags中实现任何其他位。fd是要映射的文件的打开文件描述符。可以假定offset为零(它是要映射的文件的起点)。

允许进程映射同一个MAP_SHARED文件而不共享物理页面。

munmap(addr, length)应删除指定地址范围内的mmap映射。如果进程修改了内存并将其映射为MAP_SHARED,则应首先将修改写入文件。munmap调用可能只覆盖mmap区域的一部分,但您可以认为它取消映射的位置要么在区域起始位置,要么在区域结束位置,要么就是整个区域(但不会在区域中间“打洞”)。

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

完成后,您应该会看到以下输出:

  1. $ mmaptest
  2. mmap_test starting
  3. test mmap f
  4. test mmap f: OK
  5. test mmap private
  6. test mmap private: OK
  7. test mmap read-only
  8. test mmap read-only: OK
  9. test mmap read/write
  10. test mmap read/write: OK
  11. test mmap dirty
  12. test mmap dirty: OK
  13. test not-mapped unmap
  14. test not-mapped unmap: OK
  15. test mmap two files
  16. test mmap two files: OK
  17. mmap_test: ALL OK
  18. fork_test starting
  19. fork_test OK
  20. mmaptest: all tests succeeded
  21. $ usertests
  22. usertests starting
  23. ...
  24. ALL TESTS PASSED
  25. $

提示:

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

运行usertests以确保一切正常。