【MIT 6.S081】2020, 实验记录(10),Lab: mmap
MIT 6.S081, Lab: mmap
目录
Task: mmap
Lab 介绍
这个实验需要在 xv6 中实现 mmap 功能。mmap 和 munmap 系统调用允许 UNIX 程序对其地址空间进行详细控制。 它们可用于在进程之间共享内存,将文件映射到进程地址空间,以及作为用户级页面错误方案的一部分。在本实验中,我们需要实现 mmap 和 munmap 两个 system call,只关注 mmap 的“将文件映射到进程地址空间”的功能。
mmap 的函数声明如下:
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
我们只关注使用 mmap 来实现内存映射文件的功能,这些参数中:
- addr 参数始终传入 0,所以由 kernel 来决定文件映射后的内存地址即可;
- length 是要映射的字节数,因为调用者可能只想映射文件的一部分而并非全部;
- prot 指示了内存是否被映射为可读、可写 or/and 可执行,这里的 prot 是
PROT_READ
或PROT_WRITE
或 both - flags 指示了在 munmap 时,对映射内存的修改是否要写回到文件中,如果是
MAP_SHARED
表示对映射内存的修改应写回到文件,如果是MAP_PRIVATE
则表示不需要写回。 - fd 是要映射的文件的描述符
- offset:指示了在文件中要映射的起点,在这里假设始终传入 0,即总是从文件开头开始映射
- 返回值是映射的内存地址,
0xffffffffffffffff
代表失败。
这个 system call 将文件映射到一块内存中,并将这个映射信息记录到一个 VMA 结构体中。每个进程维护自己已经 mmap 映射的信息,在代码中就是在 struct proc
结构体中放一个 VMA 数组字段 vma_pool
来存放当前所有 mmap 的信息。
munmap 用来删除一个内存地址范围的 mmap 映射,同时如果有 MAP_SHARED
标志位且进程修改了内存,那么需要写回到文件中。注意,munmap 调用可能只覆盖了 mmap-ed 区域的一部分,但是您可以假设它将在开始或结束时取消对整个区域的映射(但不会在区域的中间打孔)。munmap 函数调用如下:
int munmap(char *addr, int length);
代码实现后,实验要求通过 mmaptest
测试。
Lab 的代码实现如下:
Lab 代码实现
1)增加 mmap 和 munmap 的 system call 声明
在 syscall.h 中增加 system call 号:
将 mmap 和 munmap 加入到 syscall 表中(syscall.c):
在 sysfile.c 中增加 sys_mmap() 和 sys_munmap() 的实现,实现部分先直接 return 0
即可:
uint64
sys_mmap(void)
{
return 0;
}
uint64
sys_munmap(void)
{
return 0;
}
在 user/user.h 中增加 mmap 和 munmap 的用户空间代码的声明:
在 usys.pl 中增加两个系统调用的 entry:
2)Makefile 中增加对 mmaptest 的编译
在 Makefile 中的 UPROUGS 中增加对 mmaptest 的编译:
由此,make qemu 后即可运行 mmaptest
程序,不再报编译错误,接下来需要实现 mmap 和 munmap 函数。
3)使用 VMA 存储 mmap 映射信息
VMA 存储了 mmap-ed 的信息。当一个进程中用户程序调用 mmap 后,kernel 会将已经 mmap 的信息记录到进程的一个 VMA 数组中(一次 mmap 一个 VMA),在实现里,struct proc
中有一个 vma_pool
字段存储了所有的 VMA,是一个固定大小的数组(长度为 16),当我们在 mmap 后需要存储一个 VMA 时,就是从 vma_pool
中找一个空闲的位置并将 VMA 放进去。
VMA 结构体声明(proc.h):
VMA 结构体中字段的含义:
- used:这个 VMA 是否已经使用,用于在
vma_pool
中寻找空闲的位置 - addr:在内存中映射的内存地址
- length:映射的大小
- prot:mmap 参数中的权限
- flags:mmap 参数中的 flags
- offset:mmap 参数中的 offset,恒为 0
- f:所要映射的文件
在 struct proc 结构体中增加 vma_pool
字段:
在进程初始化函数(proc.c 中的 allocproc 函数)中增加对 vma_pool
的初始化:
实现在 vma_pool
中分配和释放 VMA 的逻辑(proc.c),所谓的“分配”其实也就是在 vma 数组中寻找一个未使用的位置:
将以上函数声明写到 defs.h 中:
4)实现 sys_mmap
sys_mmap()
函数的实现逻辑:
- 先解析调用 system call 传入的参数
- 检查权限是否能够 mmap
- 从进程的 vma_pool 的分配一个空的 VMA 位置,并将其填充 mmap 的相关信息
- 把内存分配出去(其实就是增加当前进程的 sz)
5)实现 sys_munmap
sys_munmap()
函数的实现逻辑:
- 先解析调用 system call 的参数
- 在 vma_pool 中找到 munmap 的内存地址所对应的 VMA
- 根据 VMA 的信息,将修改数据回写到文件中
- 更新 VMA 中的信息,同时如果发现当初 mmap 的内存全部被 munmap,那就需要释放 VMA 以及对 file 的引用
int
munmap_impl(uint64 addr, int length)
{
struct proc *p = myproc();
// 在 vma pool 中找到 addr 对应的 vma
struct VMA *vma = 0;
int i;
for (i = 0; i < MAX_VMA_POOL; i++) {
vma = p->vma_pool + i;
if (vma->used == 1 && addr >= vma->addr && (addr + length) < (vma->addr + vma->length)) {
break;
}
}
if (i > MAX_VMA_POOL) {
return -1;
}
// 根据 vma 的信息,将数据回写入文件中
uint64 begin_addr = addr;
uint64 end_addr = addr + length;
if (vma->flags == MAP_SHARED && vma->f->writable) {
uint64 cur_addr = begin_addr;
while (cur_addr < end_addr) {
int sz = end_addr - cur_addr >= PGSIZE? PGSIZE: end_addr - cur_addr;
begin_op();
ilock(vma->f->ip);
if (writei(vma->f->ip, 1, cur_addr, cur_addr - vma->addr, sz) != sz) {
return -1;
}
iunlock(vma->f->ip);
end_op();
uvmunmap(p->pagetable, cur_addr, 1, 1);
cur_addr += PGSIZE;
}
}
// 完成回写后,更新 vma 中的信息
if (addr == vma->addr) { // 说明 addr 是 mmap 内存的头部
vma->addr += length;
vma->length -= length;
} else if (addr + length == vma->addr + vma->length) { // 说明 addr 是 mmap 的尾部
vma->length -= length;
}
// 如果 mmap 的内存全部被 munmmap,那需要释放 vma 以及对 file 的引用
if (vma->length == 0 && vma->used == 1) {
filedup(vma->f);
vma->used = 0;
}
return 0;
}
6)在 page fault handler 中分配物理内存
我们在 mmap 中只是分配了进程的虚拟内存,并未实际分配物理内存,当用户访问被 mmap 的内存时,会产生 page fault,我们需要在 page fault handler 中分配实际的物理内存。
在 trap.c 中的 usertrap 中增加 page fault 的处理:
实现 page_fault_handler()
完成物理内存的分配:
#include "fcntl.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
......
void page_fault_handler() {
struct proc *p = myproc();
// 检查 va 合法性
uint64 va = r_stval();
if (va >= p->sz || va < PGROUNDDOWN(p->trapframe->sp)) {
p->killed = 1;
return;
}
va = PGROUNDDOWN(va);
// 遍历 vma pool,判断 va 是否是被 mmapp 的
struct VMA *vma = 0;
for (int i = 0; i < MAX_VMA_POOL; i++) {
vma = p->vma_pool + i;
if (vma->used == 1 && (va >= vma->addr) && (va < (vma->addr + vma->length))) {
break;
}
}
// 如果 `vma` 不为 0,说明 va 是被 mmap 的,需要为其分配物理内存
if (vma != 0) {
// 先分配物理内存
char *kmem = kalloc();
if (kmem == 0) {
p->killed = 1;
return;
}
memset(kmem, 0, PGSIZE);
// 将 va -> kmem 的映射加入到 pagetable 中
if (mappages(p->pagetable, va, PGSIZE, (uint64) kmem, (vma->prot << 1) | PTE_U) != 0) {
kfree(kmem);
p->killed = 1;
return;
}
// 将映射文件的数据读入 kmem page 中
ilock(vma->f->ip);
readi(vma->f->ip, 0, (uint64) kmem, va - vma->addr, PGSIZE);
iunlock(vma->f->ip);
}
}
因为我们实现了 COW,也就是 mmap 的内存是 lazy allocation 的,这导致了虚拟内存并不一定有对应的物理内存,所以需要修改一下 vm.c 中的 uvmcopy 和 uvmunmap:
7)修改 exit 和 fork
按照 Lab 的要求:
- Modify
exit
to unmap the process’s mapped regions as ifmunmap
had been called. Runmmaptest
;mmap_test
should pass, but probably notfork_test
. - Modify
fork
to ensure that the child has the same mapped regions as the parent. Don’t forget to increment the reference count for a VMA’sstruct file
. In the page fault handler of the child, it is OK to allocate a new physical page instead of sharing a page with the parent. The latter would be cooler, but it would require more implementation work. Runmmaptest
; it should pass bothmmap_test
andfork_test
.
也就是说:
- exit 应当增加释放当前进程全部 mmap-ed 的内存区域
- fork 应当让 child process 拥有与 parent process 相同的 mapped regions
exit 的修改(proc.c 的 exit()
):
fork 的修改(proc.c 的 fork()
):
注意,这里修改 exit 函数时使用了 munmap_impl()
函数,所以应当把这个函数的声明放到 defs.h 中:
Lab 测试
至此,Lab 的测试即可通过:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)