6.S081 lab10 mmap
mmap
和munmap
系统调用允许 UNIX 程序对其地址空间进行更为细致的控制。它们可用于在进程间共享内存,将文件映射到进程地址空间,并作为用户级page fault
方案的一部分。在本实验室中,我们将在xv6
中添加mmap
和munmap
系统调用,重点是memory-mapped files
。
Lab: mmap
mmap
的 API 如下:
1 | void *mmap(void *addr, size_t length, int prot, int flags, |
在xv6
中,addr
始终为 0,所以由kernel
自行判断应当 map 的地址;prot
表示了 mapped memory 的 R、W、X 权限,flags
为MAP_SHARED
或者MAP_PRIVATE
,前者表示对 mapped memory 的修改应写回文件,后者则不需要;offer
永远为 0,不用处理文件的偏移量;mmap 成功将返回对应内存起始地址;失败返回0xffffffffffffffff
。
munmap(addr, length)
需要将从 addr 开始的长度为length
的内存unmap
。实验指导书保证被munmap
的这段内存位于mmap
内存区间的头部/尾部或者是全部,munmap
不会在中间挖一个洞;当内存是以MAP_SHARED
模式被mmap
时,需要先将修改写回文件。
看完了对于mmap
和munmap
的要求,发现其实测试没有一些比较难的case
,为我们的实现提供了便利。
之后,就可以跟着 hints 完成实验:
首先添加
mmap
和munmap
的系统调用声明,并且在Makefile
中加入_mmaptest
。对 mapped memory 要使用 lazy allocation,就像在之前的实验中那样,这样子使得我们可以在物理内存有限的情况下
mmap
尽可能大的文件。记录
mmap
为每个进程 map 文件的情况,例如地址,长度,权限,对应的文件等等。由于xv6
没有真正的内存分配器,所以我们使用一个定长的数组去存储,16 就足够了。1
2
3
4
5
6
7
8
9
10
11
12
13struct VMA {
int used;
uint64 addr;
uint64 end;
int prot;
int flags;
int offset;
struct file *f;
};
// in struct proc
struct VMA vma[NVMA];
uint64 mmap_start;实现
mmap
,从用户地址空间找到空闲处 map 文件,修改对应的VMA
结构体记录,当对文件mmap
后,应当增加文件的引用计数(filedup
),这样当文件被关闭时,VMA
持有的文件指针才不会失效。最重要的就是找到合适的空闲地址,用于
mmap
。xv6
的用户地址空间如下图:最顶部是
trampoline
和trapframe
,它们占用了两个 page,和stack
之间有很大的空闲地址,我们可以将文件 map 到trapframe
之下,不断向下增长,mmap_start
记录着trapframe
下可用于mmap
的起始地址,初始值为PGROUNDDOWN(MAXVA - (2 * PGSIZE))
。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
35uint64
sys_mmap(void)
{
int length, prot, flags, fd;
struct file *f;
if(argint(1, &length) < 0 || argint(2, &prot) < 0
|| argint(3, &flags) < 0 || argfd(4, &fd, &f) < 0) {
return 0xffffffffffffffff;
}
if (!f->writable && flags == MAP_SHARED && (prot & PROT_WRITE)) {
return 0xffffffffffffffff;
}
// find a vma
struct proc *p = myproc();
struct VMA *v;
for (v = p->vma; v < p->vma + NVMA; v++) {
if(!v->used) {
break;
}
}
if(v == p->vma + NVMA) {
return -1;
}
filedup(f);
v->addr = PGROUNDDOWN(p->mmap_start - length);
v->end = v->addr + length;
p->mmap_start = v->addr;
v->used = 1;
v->f = f;
v->prot = prot;
v->flags = flags;
v->offset = 0;
return v->addr;
}当发生
page fault
时,为其分配一个真实的物理页面,使用readi
将文件内容读入内存,然后将物理页面 map 到用户地址空间,记得正确设置页面的权限。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
39
40
41
42
43
44
45
46
47
48else if (r_scause() == 13 || r_scause() == 15) {
uint64 va = r_stval();
if (va > MAXVA) {
p->killed = 1;
} else {
if(mmap_alloc(p->pagetable, va) < 0) {
p->killed = 1;
}
}
}
int
mmap_alloc(pagetable_t pagetable, uint64 va)
{
char *mem;
struct proc *p = myproc();
struct VMA *v;
// find vma struct
for (v = p->vma; v < p->vma + NVMA; v++) {
if(v->used && va >= v->addr && va < v->end) {
break;
}
}
if (v == p->vma + NVMA) {
return -1;
}
mem = kalloc();
if(mem == 0){
return -1;
}
memset(mem, 0, PGSIZE);
begin_op();
ilock(v->f->ip);
int len;
if((len = readi(v->f->ip, 0, (uint64)mem, va - v->addr, PGSIZE)) < 0) {
iunlock(v->f->ip);
end_op();
return -1;
}
iunlock(v->f->ip);
end_op();
int f = PTE_U | (v->prot << 1);
if(mappages(pagetable, va, PGSIZE, (uint64)mem, f) != 0) {
kfree(mem);
return -1;
}
return 0;
}实现
munmap
,找到对应的VMA
,使用uvmunmap
unmap 对应的内存,当一个mmap
的所有内存都被 unmap 时,需要减少对应文件的引用计数;如果内存被修改过,且是以MAP_SHARED
模式被mmap
,那么需要先将内存内容写回文件。理想态下,我们只应当写回dirty page
,但是测试中不会检查这一点,所以将所有内存写回文件即可了。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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71struct file*
fileundup(struct file *f)
{
acquire(&ftable.lock);
if(f->ref < 1)
panic("filedup");
f->ref--;
release(&ftable.lock);
return f;
}
uint64
sys_munmap(void)
{
uint64 addr;
int length;
if(argaddr(0, &addr) < 0 || argint(1, &length) < 0) {
return -1;
}
return s_munmap(addr, length);
}
uint64
s_munmap(uint64 addr, int length) {
struct proc *p = myproc();
struct VMA *v;
for (v = p->vma; v < p->vma + NVMA; v++) {
if(v->used && v->addr <= addr && addr + length <= v->end) {
break;
}
}
if(v == p->vma + NVMA) {
return -1;
}
uint64 end = addr + length;
uint64 _addr = addr;
while (addr < end) {
// if already load in
if(walkaddr(p->pagetable, addr)) {
if(v->flags == MAP_SHARED && v->f->writable) {
begin_op();
ilock(v->f->ip);
int size = min(end-addr, PGSIZE);
if(writei(v->f->ip, 1, addr, addr - v->addr, size) < size) {
iunlock(v->f->ip);
end_op();
return -1;
}
iunlock(v->f->ip);
end_op();
}
uvmunmap(p->pagetable, addr, 1, 1);
}
addr += PGSIZE;
}
if(_addr == v->addr) {
v->addr += length;
} else if(_addr + length == v->end) {
v->end -= length;
}
if (v->addr == v->end) {
fileundup(v->f);
v->used = 0;
}
return 0;
}实现时遇到两个坑:
- 在
uvmunmap
时,首先要判断该内存是否真的被lazy allocation
了,否则要像lazy
lab 中一样,修改uvmunmap
,我觉得这样实现比较 ugly,因为破坏了uvmunmap
发现错误的功能。 - 最开始实现时,只要
v->flags == MAP_SHARED
就将文件写回,结果在forktest
中父子进程内存内容不一致,查看forktest
代码发现原来创建的只读文件,只要prot
不标志为可写,那么制度文件也是可以用MAP_SHARED
模式进行mmap
的。所以还要加上v->f->writable
或者prot & PROT_WRITE
。
- 在
修改
exit
代码,使得exit
被调用后,unmap 所有被mmap
的内存。1
2
3
4
5
6
7
8struct VMA *v;
for (v = p->vma; v < p->vma + NVMA; v++) {
if(v->used) {
if (s_munmap(v->addr, v->end - v->addr) < 0) {
panic("exit:munmap");
}
}
}修改 fork 代码,使得子进程拥有和父进程相同的
mmap
文件。可以直接为子进程分配新的物理内存用于mmap
,不用共享相同物理页面。1
2
3
4
5
6
7
8
9
10
11
12np->mmap_start = p->mmap_start;
for(i = 0; i < NVMA; i++) {
np->vma[i].addr = p->vma[i].addr;
np->vma[i].end = p->vma[i].end;
np->vma[i].used = p->vma[i].used;
np->vma[i].flags = p->vma[i].flags;
np->vma[i].prot = p->vma[i].prot;
if(p->vma[i].used && p->vma[i].f) {
np->vma[i].f = filedup(p->vma[i].f);
}
}
这样 mmap lab 就完成了,最终代码见GitHub 仓库。
6.S081 lab10 mmap