xv6-labs-2020.lab9.mmap
lab9 mmap
1. 作业链接
- https://pdos.csail.mit.edu/6.828/2020/labs/mmap.html
2. 实习内容
- 实现系统调用 mmap,munmap
1 | void* mmap(void* addr, size_t length, int prot, int flags, |
- linux 下通过让一下命令查看对 mmap、munmap 的解释
1 | man 2 mmap |
- mmap()
- addr:起始地址
- 如果为 NULL,则内核会选择一个页对齐的地址
- 如果不为 NULL,只是一个 hint,如果不可行则会选择其他地址
- length:申请的空间长度(字节)
- prot:一些保护位
- PROT_EXEC Pages may be executed.
- PROT_READ Pages may be read.
- PROT_WRITE Pages may be written.
- PROT_NONE Pages may not be accessed.
- flag:设置的 flag,表示对这块区域的操作对其它进程的影响
- MAP_SHARED、MAP_SHARED、MAP_ANONYMOUS 等
- fd:建立映射的文件
- offset:被映射对象内容的起点
- 返回值为起始地址,(void*)-1 表示失败
- addr:起始地址
- munmap()
- addr:起始地址
- length:空间长度(字节)
2.1 说明
- 我们在这个实验中只需要实现简单功能即可
- mmap
- addr 可以认为始终为 0
- 表示起始地址由内核决定
- 返回值为起始地址,或者
0xffffffffffffffff
表示失败 - length 表示需要映射内容的长度
- 可能和文件不等长
- prot 表示这块区域是否可读、可写、可执行
- 我们可以假设只能是
PROT_READ
、PROT_WRITE
、二者都有 - flags 只能是
MAP_SHARED
(修改需要写回文件)、MAP_PRIVATE
(不需要写回文件)
- 我们可以假设只能是
- fd 为文件描述符
- offset 为 0
- addr 可以认为始终为 0
- 我们允许 MAP_SHARED 的映射区域不对应相同的物理页
- munmap
- 解除映射
- 如果 flag 为 MAP_SHARED,则需要写回文件
- 解除的区域可以是映射区域的一个子集,但是我们可以假设接触区域只能是如下情况
- 整个区域
- start 和映射区域相同
- end 和映射区域相同
2.2 提示
- 准备工作放在实现里面
- mmap 本身不分配物理页,通过 page fault 分配物理页
- usertrap 就行了,mmap 只会被用户态调用分配
- 保证快速映射一块大区域,允许映射比实际物理内存更大的范围
- 每个进程保存一个数据结构 VMA(virtual memory area),记录被 mmap
映射的区域
- 简单的可以直接开一个定长的数组,16 就足够了
- VMA 需要保存如下内容
- address, length, permissions, file 等
- mmap 的实现
- 首先在进程的空闲内存区与中找到一块区域分配空间
- 在 VMA 中找到一个空闲区域用于保存记录
- VMA 中需要有一个
struct file*
指针,需要把 file 的引用计数+1(避免释放)- 可以看
filedup
的实现
- 可以看
- 注意权限的检查,有 read-only 的测试样例
- 如果把只读区域映射为可写的而且是 MAP_SHARED 则直接报错
- MAP_PRIVATE 不会写
- 此时由于 lazy 的结果会导致 page fault
- 实现 page fault 时读取文件,分配物理页
- 捕捉 page fault
- 通过 readi 读取文件中与访问地址相近的 4096 bytes,并建立映射关系
- readi 的时候需要给 inode 加锁
- 正确设置权限位
- 此时能够正确到达第一个 munmap 测试
- 实现 munmap
- 遍历 VMA,找到对应的 VMA,释放对应的空间即可
- 如果是 MAP_SHARED 的话,需要将修改的内容写回文件
- 参考 filewrite 实现
- 如果是释放了整个映射区域的话,需要将文件的引用计数-1
- 理论上,我们应该只写回真正被修改的页,也就是说 dirty 位设置为 1 的页
- 但是这里没有检查,因此我们不做要求
- exit() 退出的时候,释放所有的 VMA 中没有解除映射的块
- fork() 的时候,子进程拥有和父进程一样的 VMA,允许在子进程遇到 page
fault 的时候分配一个不和父进程一样的物理页
- 更好的方式是和父进程共用一个物理页,但是这里不要求
2.3 实现
(1) 添加系统调用
Makefile
1 | UPROGS=\ |
- 添加系统调用
user/usys.pl
1 | entry("mmap"); |
user/user.h
1 | void* mmap(void*, int, int, int, int, int); |
kernel/sysfile.c
1 | uint64 sys_mmap (void){ |
kernel/syscall.h
1 |
kernel/syscall.c
1 | extern uint64 sys_mmap(void); |
(2) VMA
- 添加数据结构 VMA
1 | // kernel/proc.h |
- 在 proc 中添加 VMA
1 | // kernel/proc.h |
(3) mmap
- 按照提示的说明实现 mmap()
- 找到一个空闲区域,写入 VMA,修改当前进程的 sz
- 注意上面提到的一个权限问题
1 | // kernel/sysfile.c |
- 由于这里并没有分配内存,因此会触发 usertrap()
(4) usertrap
- 在 usertrap 中处理 page fault
- 和 lab4-lazy 比较像
1 | // kernel/trap.c:usertrap |
- 因为头文件的包含问题,我们把 map_mmap() 的实现放在了 kernel/sysfile.c 里面
- kernel/defs.h 里添加函数声明
1 | // kernel/defs.h |
- kernel/sysfile.c 中添加实现
- 遍历 VMA,找到对应的文件
- 申请空间
- 建立映射
- 从文件中读入内存
- 一些个问题
- 申请空间是按页申请的,因此起始地址必须时页对齐的,申请长度必须是页的整数倍
1 | // kernel/sysfile.c |
(5) munmap
- 遍历 VMA,找到对应的映射
- 判断是否是从 start 开始释放,如果是,接着判断是否需要释放整个文件
- 注意如果要释放只做标记,之后再释放,否则会出问题(不能提前释放文件)
- 如果是 MAP_SHARED,则在释放之前需要进行写操作
1 | // kernel/sysfile.c |
- 和 filewrite() 一样的实现,只是增加一个文件的 offset
- 我们只实现了 FINODE 的写操作
1 | // kernel/sysfile.c |
(6) 其他细节
- 在 exit() 退出的时候,需要释放映射的文件区域
- 注意文件计数需要+1
1 | // kernel/proc/c:exit() |
- 在 fork 的时候,将父进程的映射内容拷贝到子进程
- 注意文件计数需要+1
1 | // kernel/proc/c:fork() |
- 因为 lazy 分配的原因,我们需要做一些特殊处理
- uvmunmap()
- 如果没有建立映射,则不需要释放
- 如果建立了映射,但是没有读入内存,则不需要调用 kfree()
1 | // kernel/vm.c:uvmunmap() |
- uvmcopy()
- 如果尚未分配物理页,这是允许的
1 | // kernel/vm.c:uvmcopy() |
3. 实验结果
1 | == Test running mmaptest == |
4. 遇到的困难以及收获
- 这个 lab
做起来很舒服,一方面提示给得很完整,另一方面我对文件系统的理解也更加深入了
- 这个 lab 可以说是 lab4-lazy 的延伸,lazy 机制缺失在很多地方都很有用
- 做完这个 lab,感觉自己对 xv6 -riscv 文件系统了解更深入了
5. 对课程或 lab 的意见和建议
- 建议提供一些关于 lab 的 debug 功能的指导
6. 参考文献
- https://pdos.csail.mit.edu/6.828/2019/xv6/book-riscv-rev1.pdf