MIT 6.S081 lab8:mmap | 内存映射文件
MIT 6.S081 Lab: mmap
mmap
是操作系统中特别重要的一个特性。它允许我们将文件映射到进程的虚拟地址空间,使得我们可以像操作数组一样操作文件。在本次
Lab 中,我们需要在 xv6 中实现一个简化版的 mmap
系统调用。
核心难点在于:懒加载(Lazy Allocation) 的实现以及 VMA(Virtual Memory Area) 的管理。
1. 实验目标
实现 mmap 和 munmap: -
mmap:不立即分配物理内存,只在进程的 proc
结构体中记录一段虚拟地址区间(VMA)。 -
Page Fault:当访问映射地址触发缺页异常时,再分配物理内存并将文件内容读入。
- munmap:解除映射。如果是
MAP_SHARED,需要将修改过的数据写回磁盘。
2. 核心数据结构:VMA
xv6 默认没有 VMA 的概念。我们需要在 kernel/proc.h
中定义它。每个进程可以拥有多个映射区,实验要求至少支持 16 个。
1 | |
3.2 Usertrap (缺页处理)
当用户尝试读写 mmap 的地址时,硬件由于找不到页表项会触发
scause == 13 或 15。此时我们需要在
kernel/trap.c 中进行修复。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// kernel/trap.c -> usertrap()
else if(r_scause() == 13 || r_scause() == 15) {
uint64 va = r_stval();
if(va >= p->sz || va < p->trapframe->sp) { // 基本越界检查
p->killed = 1;
} else {
// 查找该地址属于哪个 VMA
struct vma *v = 0;
for(int i = 0; i < 16; i++) {
if(p->vma[i].valid && va >= p->vma[i].addr && va < p->vma[i].addr + p->vma[i].length) {
v = &p->vma[i];
break;
}
}
if(v) {
uint64 pa = (uint64)kalloc();
if(pa == 0) p->killed = 1;
else {
memset((void*)pa, 0, PGSIZE);
// 读取文件到物理内存
ilock(v->f->ip);
readi(v->f->ip, 0, pa, PGROUNDDOWN(va) - v->addr, PGSIZE);
iunlock(v->f->ip);
// 构建页表映射
int pte_flags = PTE_U | ((v->prot & PROT_READ) ? PTE_R : 0) | ((v->prot & PROT_WRITE) ? PTE_W : 0);
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, pa, pte_flags) != 0) {
kfree((void*)pa);
p->killed = 1;
}
}
} else {
p->killed = 1;
}
}
}
3.3 sys_munmap
解除映射时,如果设置了 MAP_SHARED,必须利用
filewrite 将脏数据同步回磁盘。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// kernel/sysfile.c
uint64
sys_munmap(void)
{
uint64 addr;
int length;
if(argaddr(0, &addr) < 0 || argint(1, &length) < 0) return -1;
struct proc *p = myproc();
struct vma *v = 0;
// 查找对应的 VMA (略) ...
if(v->flags & MAP_SHARED && (v->prot & PROT_WRITE)) {
// 写回磁盘逻辑:需遍历地址区间,检查每页是否已映射(PTE_V)
// 然后调用 filewrite(v->f, addr, length);
}
// 解除映射并释放物理页
uvmunmap(p->pagetable, addr, PGROUNDUP(length)/PGSIZE, 1);
// 如果全部取消映射,释放 VMA 槽位
// v->valid = 0; fileclose(v->f);
return 0;
}
3.4 适配 fork 中的 VMA 复制
在 fork()
时,子进程需要继承父进程的所有映射关系。由于我们实现了懒加载,子进程不需要立即复制父进程的物理内存页,只需要将父进程的
vma 结构体数组完整拷贝一份。
注意: 必须调用
filedup(v->f),因为子进程现在也拥有了对该映射文件的引用。
1 | |
3.5 exit 中自动卸载
果用户程序在调用 munmap
之前就退出了,内核必须负责清理该进程占用的所有 VMA。这包括:
写回脏数据:如果是 MAP_SHARED,同步回磁盘。
释放物理页:解除页表映射。
关闭文件:释放文件引用。
为了复用代码,我们在 kernel/sysfile.c 中封装一个
vma_unmap 函数,供 sys_munmap 和
exit 共同调用。
1 | |
以及对应的 exit 函数 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// kernel/proc.c -> exit()
void
exit(int status)
{
struct proc *p = myproc();
// 在销毁页表之前,清理所有已分配的 VMA
for(int i = 0; i < 16; i++) {
if(p->vma[i].valid) {
vma_unmap(p, &p->vma[i], p->vma[i].addr, p->vma[i].length);
}
}
// ... 后面是原有的进程资源释放逻辑 ...
}
3.6 修改 uvmunmap:
xv6 原生的 uvmunmap 在发现某个虚拟地址没有对应的有效
PTE(页表项)时,会触发 panic。但在 mmap
的懒加载场景下,用户可能映射了 10 页但只访问了 1
页,此时卸载整个区域时,有 9 页是没被映射的。
需要修改 kernel/vm.c 中的 uvmunmap:
1
2
3
4
5
6
7
8
9
10
11
12// kernel/vm.c -> uvmunmap()
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
// ... 循环逻辑 ...
if((pte = walk(pagetable, a, 0)) == 0)
continue; // 修改点:不再 panic,直接跳过
if((*pte & PTE_V) == 0)
continue; // 修改点:不再 panic,直接跳过
// ... 释放物理页逻辑 ...
}