Lab地址:pdos.csail.mit.edu/6.828/2021/…
git代码地址:github.com/cardchoosen…
Lab: mmap
实现mmap和munmap系统调用,首先了解一下这两个系统调用。
- mmap
mmap系统调用的主要功能是将文件或设备的内容映射到进程的虚拟地址空间,进程可以像访问内存一样对文件或设备进行读写操作,从而避免了频繁的read和write系统调用,提高了 I/O 效率。它也可用于创建匿名映射,实现进程间通信。
函数原型:
// addr:指定映射的起始地址。通常设置为NULL,让系统自动选择合适的地址。
// length:要映射的内存区域的长度,以字节为单位。
// prot:内存区域的保护标志,常用的标志有:
// PROT_READ:可读。
// PROT_WRITE:可写。
// PROT_EXEC:可执行。
// PROT_NONE:不可访问。
// flags:控制映射的类型和行为,常见的标志有:
// MAP_SHARED:对映射区域的修改会反映到文件中,并且会被其他映射同一文件的进程看到。
// MAP_PRIVATE:对映射区域的修改是私有的,不会反映到文件中。
// MAP_ANONYMOUS:创建匿名映射,不与任何文件关联。
// fd:文件描述符,指定要映射的文件或设备。如果使用MAP_ANONYMOUS标志,则此参数通常被忽略,可设为 -1。
// offset:文件中的偏移量,表示从文件的哪个位置开始映射,必须是页大小的整数倍。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 成功时,返回映射区域的起始地址。
// 失败时,返回MAP_FAILED(通常被定义为(void *)-1),并设置errno以指示错误类型。
2. Munmap
munmap系统调用用于解除之前通过mmap创建的内存映射,释放映射区域所占用的虚拟地址空间,并将修改后的内容写回文件(如果使用了MAP_SHARED标志)。
函数原型:
// addr:要解除映射的内存区域的起始地址,该地址必须是之前mmap调用返回的地址。
// length:要解除映射的内存区域的长度,必须与mmap调用时指定的长度一致。
int munmap(void *addr, size_t length);
// 成功时,返回 0。
// 失败时,返回 -1,并设置errno以指示错误类型。
Lab给出的提示:
- 首先将 “_mmaptest” 添加到 UPROGS 中,并添加 mmap 和 munmap 系统调用,以便使 user/mmaptest.c 能够编译。目前,只需从 mmap 和 munmap 返回错误。我们在 kernel/fcntl.h 中为你定义了 PROT_READ 等。运行 mmaptest,它将在第一次 mmap 调用时失败。
- 在响应页面错误时懒惰地填充页表。也就是说,mmap 不应分配物理内存或读取文件。相反,在 usertrap 中的页面错误处理代码中(或由其调用的代码中)进行此操作,就像在延迟页面分配实验中一样。懒惰的原因是确保对大文件的 mmap 快速,并且对大于物理内存的文件进行 mmap 是可能的。
- 跟踪每个进程的 mmap 映射了什么。定义一个与讲座 15 中描述的 VMA(虚拟内存区域)相对应的结构,记录由 mmap 创建的虚拟内存范围的地址、长度、权限、文件等。由于 xv6 内核在内核中没有内存分配器,因此可以声明一个固定大小的 VMA 数组,并根据需要从该数组中进行分配。大小为 16 应该足够了。
- 实现 mmap:在进程的地址空间中找到一个未使用的区域来映射文件,并将一个 VMA 添加到进程的映射区域表中。VMA 应包含一个指向正在映射的文件的结构体文件的指针;mmap 应增加文件的引用计数,以便在文件关闭时该结构不会消失(提示:参见 filedup)。运行 mmaptest:第一次 mmap 应该成功,但对 mmap 后的内存的第一次访问将导致页面错误并终止 mmaptest。
- 添加代码,使 mmap 区域中的页面错误分配一页物理内存,将相关文件的 4096 字节读入该页面,并将其映射到用户地址空间。使用 readi 读取文件,它接受一个偏移量参数,用于在文件中读取(但你必须锁定 / 解锁传递给 readi 的 inode)。不要忘记正确设置页面的权限。运行 mmaptest;它应该到达第一次 munmap。
- 实现 munmap:找到地址范围的 VMA 并取消映射指定的页面(提示:使用 uvmunmap)。如果 munmap 移除了先前 mmap 的所有页面,它应减少相应结构体文件的引用计数。如果一个未映射的页面已被修改且文件被映射为 MAP_SHARED,则将该页面写回文件。从 filewrite 中获取灵感。
- 理想情况下,你的实现只会写回程序实际修改的 MAP_SHARED 页面。RISC-V PTE 中的脏位(D)指示页面是否已被写入。然而,mmaptest 不会检查非脏页面是否未被写回;因此,你可以在不查看 D 位的情况下写回页面。
- 修改 exit,以如同调用了 munmap 一样取消映射进程的映射区域。运行 mmaptest;mmap_test 应该通过,但可能不是 fork_test。
- 修改 fork,以确保子进程具有与父进程相同的映射区域。不要忘记增加 VMA 的结构体文件的引用计数。在子进程的页面错误处理程序中,可以分配一个新的物理页面,而不是与父进程共享页面。后者会更酷,但需要更多的实现工作。运行 mmaptest;它应该通过 mmap_test 和 fork_test。
这一个Lab比较复杂,先一步一步走,设置Makefile,完成系统调用的添加和一些宏定义。
// Makefile
UPROGS=\
...
$U/_mmaptest\
// user/user.h
void *mmap(void *addr, uint length, int prot, int flags, int fd, uint offset);
int munmap(void *addr, uint length);
// user/usys.pl
entry("mmap");
entry("munmap");
// kernel/syscall.h
#define SYS_mmap 22
#define SYS_munmap 23
// kernel/syscall.c
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);
static uint64 (*syscalls[])(void) = {
...
[SYS_mmap] sys_mmap,
[SYS_munmap] sys_munmap,
};
// kernel/riscv.h
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_G (1L << 5) // global mapping
#define PTE_A (1L << 6) // accessed
#define PTE_D (1L << 7) // dirty
mmap需要在用户的地址空间中找到一片空闲区域用于映射mmap页,xv6对用户地址空间的分配中,heap的范围是从stack到trapframe,进程本身使用的内存空间是从低地址往高地址生长,所以为了避免mmap使用的地址空间与进程已使用空间发生冲突,将mmap映射的文件map到尽可能高的地址,也就是trapframe下方,mmap多个文件时,往下生长。
// kernel/memlayout.h
// User memory layout.
// Address zero first:
// text
// original data and bss
// fixed-size stack
// expandable heap
// ...
// TRAPFRAME (p->trapframe, used by the trampoline)
// TRAMPOLINE (the same page as in the kernel)
#define TRAPFRAME (TRAMPOLINE - PGSIZE)
// MMAP 所能使用的最后一个页+1
#define MMAPEND TRAPFRAME
在proc.h中增加vma结构体的定义,vma中包含mmap映射到内存区域必要信息,如开始地址、大小、映射到文件、文件内偏移、权限等。
同时为proc结构末尾增加16个vma槽位。
struct vma {
int valid;
uint64 vastart;
uint64 sz;
struct file *f;
int prot;
int flags;
uint64 offset;
};
#define NVMA 16
// Per-process state
struct proc {
...
struct vma vmas[NVMA]; // virtual memory areas
};
在defs.h中添加一些声明
struct vma;
// file.c
...
int vmatrylazytouch(uint64 va);
// vm.c
...
void vmaunmap(pagetable_t pagetable, uint64 va, uint64 nbytes, struct vma *v);
mmap参数中的保护标志prot和映射类型flag已在fcntl.h中帮我们声明好了
#ifdef LAB_MMAP
#define PROT_NONE 0x0
#define PROT_READ 0x1
#define PROT_WRITE 0x2
#define PROT_EXEC 0x4
#define MAP_SHARED 0x01
#define MAP_PRIVATE 0x02
#endif
到这里我们的前置准备工作都已经完成,接下来正式实现mmap系统调用sys_mmap,函数的功能是在进程的16个vma槽中找到可用的槽,并且计算所有vma中使用的最低的虚拟地址(保证映射的地址空间向下生长),作为新的vma的结尾地址vaend,将当前文件映射到该最低地址的下方,vastart = vaend - sz。