共计 6565 个字符,预计需要花费 17 分钟才能阅读完成。
Lab: mmap (hard)
mmap
和 munmap
零碎调用容许 UNIX 程序对其地址空间进行具体管制。它们可用于在过程之间共享内存,将文件映射到过程地址空间,以及作为用户级页面谬误计划(如课程中探讨的垃圾回收算法)的一部分。在本试验中,你将向 xv6 增加 mmap
和 munmap
,重点关注内存映射文件。
运行 man 2 mmap
可失去手册中 mmap
的申明:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
要求
能够通过多种形式调用 mmap,但此练习只须要与文件内存映射相干的性能子集。
- 您能够假如
addr
将始终为零,这意味着内核应决定映射文件的虚拟地址。mmap
返回该地址,如果失败,则返回0xffffffffffffffff
。 length
是要映射的字节数; 它可能与文件的长度不同。prot
批示内存是否应映射为可读、可写和 / 或可执行; 您能够假如prot
是PROT_READ
或PROT_WRITE
或两者兼而有之。flags
要么是MAP_SHARED
,这意味着对映射内存的批改应该写回文件,要么是MAP_PRIVATE
,这意味着它们不应该写回文件。你不用在flags
中实现任何其余位。fd
是要映射的文件的关上文件描述符。- 您能够假如偏移量为零(文件中要映射的终点)。
对于映射同一 MAP_SHARED
文件的不同过程,禁止共享物理页。
munmap(addr,length)
应该删除指定地址范畴内的 mmap 映射。如果过程批改了内存且为 MAP_SHARED
映射,则应首先将批改写入文件。munmap
调用可能只笼罩已被映射的区域的一部分,但您能够假如它会在开始时、结尾或整个区域勾销映射(但不会在区域两头打一个洞)。
您应该实现足够的
mmap
和munmap
性能,以使mmaptest
测试程序失常工作。无需实现mmaptest
不应用mmap
的性能。
实现后,应会看到以下输入:
$ mmaptest
mmap_test starting
test mmap f
test mmap f: OK
test mmap private
test mmap private: OK
test mmap read-only
test mmap read-only: OK
test mmap read/write
test mmap read/write: OK
test mmap dirty
test mmap dirty: OK
test not-mapped unmap
test not-mapped unmap: OK
test mmap two files
test mmap two files: OK
mmap_test: ALL OK
fork_test starting
fork_test OK
mmaptest: all tests succeeded
$ usertests -q
usertests starting
...
ALL TESTS PASSED
$
提醒
- 首先向
UPROGS
增加_mmaptest
,以及mmap
和munmap
零碎调用,以便使user/mmaptest.c
可能编译。当初,只需从mmap
和munmap
返回谬误。咱们在kernel/fcntl.h
中为您定义了PROT_READ
等。运行mmaptest
,这将在第一次mmap
调用时失败。 - 惰性地填写页表,以响应页面谬误。也就是说,
mmap
不应调配物理内存或读取文件。相同,请在usertrap
中(或由usertrap
调用)的页面错误处理代码中执行此操作,就像在惰性页面调配试验中一样。惰性的起因是确保大文件的mmap
是疾速的,并且大于物理内存的文件的mmap
是可能的。 - 跟踪
mmap
为每个过程映射的内容。定义与第 15 讲中形容的 VMA(虚拟内存区域)对应的构造,记录mmap
创立的虚拟内存范畴的地址、长度、权限、文件等。因为 xv6 内核中没有内存分配器,因而能够申明一个固定大小的 VMA 数组,并依据须要从该数组进行调配。大小为 16 就足够了。 - 实现
mmap
:在过程的地址空间中查找要在其中映射文件的未应用区域,并将 VMA 增加到过程的映射区域表中。VMA 应蕴含指向要映射的文件的struct file
的指针;mmap
应减少文件的援用计数,以便在敞开文件时构造不会隐没(提醒:请参阅filedup
)。运行mmaptest
:第一个mmap
应该胜利,但第一次拜访已映射的内存会导致页面谬误并 killmmaptest
。 - 增加代码,以在拜访已映射区域中导致的页面谬误时,调配一页物理内存,将相干文件的 4096 字节读取到该页面中,并将其映射到用户地址空间。应用
readi
读取文件,它须要一个偏移参数来读取文件(但您必须锁定 / 解锁传递给readi
的 inode)。不要遗记在页面上正确设置权限。运行mmaptest
; 它应该达到第一个 munmap。 - 实现
munmap
:找到地址范畴的 VMA 并勾销映射指定的页面(提醒:应用uvmunmap
)。如果munmap
删除了前一个mmap
的所有页面,它应该缩小相应struct file
的援用计数。如果已批改未映射的页面并且文件已映射MAP_SHARED
,请将该页面写回该文件。查看filewrite
以取得灵感。 - 现实状况下,您的实现只会写回真正被程序修改的
MAP_SHARED
页面。RISC-V PTE 中的脏位(D)批示是否已写入页面。然而,mmaptest
不会查看非脏页面是否没有写回; 因而,您能够在不查看 D 位的状况下从新编写页面。 - 批改
exit
以勾销映射过程的映射区域,就像调用munmap
一样。运行mmaptest
;mmap_test
应该通过,但可能不会通过fork_test
。 - 批改
fork
以确保子级与父级具备雷同的映射区域。不要遗记递增 VMAstruct file
的援用计数。在子级的页面谬误处理程序中,能够调配新的物理页面,而不是与父级共享页面。后者会更酷,但须要更多的工作。运行mmaptest
; 它应该通过mmap_test
和fork_test
。
实现
- 增加
mmap
和munmap
零碎调用:增加零碎调用通用步骤(在用户空间申明零碎调用、增加条目,在内核空间减少零碎调用命令序号以及对应的sys_mmap()
和sys_munmap()
函数) -
在
proc.h
中struct proc
中增加映射区域:首先定义映射区域构造体struct MapArea
及一个过程映射区域的数量:#define MAP_AREA_LENGTH 16 struct MapArea { uint64 address; // 映射开始地址 int length; // 映射区域长度 int prot; int flag; int offset; struct file* fp; };
而后在
struct proc
中增加示意映射区域的数组字段:struct proc { ... struct MapArea mapareas[MAP_AREA_LENGTH]; };
-
实现
sys_mmap()
的性能:uint64 sys_mmap(void) { uint64 addr; int len, prot, flag, fd, offset, i; struct file* fp; struct proc* p = myproc(); argaddr(0, &addr); argint(1, &len); argint(2, &prot); argint(3, &flag); if (argfd(4, &fd, &fp) < 0) return -1; argint(5, &offset); // 判断权限是否抵触 if (!fp->writable && (prot & PROT_WRITE) && (flag & MAP_SHARED)) {printf("file is not writable!\n"); return -1; } // 找闲暇 map area for (i = 0; i < MAP_AREA_LENGTH; ++i) {if (p->mapareas[i].address == 0) break; } if (i == MAP_AREA_LENGTH) {printf("no more map area!\n"); return -1; } // 若未指定地址则须要调配地址 if (!addr) {addr = PGROUNDUP(p->sz); p->sz += PGROUNDUP(len); } // 复制字段 p->mapareas[i].address = addr; p->mapareas[i].length = len; p->mapareas[i].prot = prot; p->mapareas[i].flag = flag; p->mapareas[i].offset = offset; p->mapareas[i].fp = fp; // 文件援用计数 +1 filedup(fp); return addr; }
-
在
usertrap()
中辨认页面谬误并将文件内容写入对应地址空间:首先须要在usertrap()
本来的抉择分支框架中增加页面谬误的分支,在该分支中分配内存并将文件内容写入到指定地位。这里通过map_fill()
函数实现此性能。void usertrap(void) { ... if(r_scause() == 8){ // system call ... } else if (r_scause() == 13 || r_scause() == 15) { // 页面谬误,须要分配内存并写入文件内容 if (map_fill(r_stval()) == 0) {goto error;} } else if((which_dev = devintr()) != 0){// ok} else { error: printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid); printf("sepc=%p stval=%p\n", r_sepc(), r_stval()); setkilled(p); } ... }
map_fill()
函数实现分配内存并写入文件内容的性能:uint64 map_fill(uint64 va) { int idx, perm, prot, offset; struct inode* ip; void* new_mem; struct proc* p = myproc(); // 找到哪个 map area for (idx = 0; idx < MAP_AREA_LENGTH; ++idx) {if (va >= p->mapareas[idx].address && va < p->mapareas[idx].address + p->mapareas[idx].length) break; } if (idx == MAP_AREA_LENGTH) return 0; // 分配内存 if ((new_mem = kalloc()) == 0) {return 0;} // 物理虚拟地址建设映射 prot = p->mapareas[idx].prot; perm = PTE_U; if (prot & PROT_READ) perm |= PTE_R; if (prot & PROT_WRITE) perm |= PTE_W; if (prot & PROT_EXEC) perm |= PTE_X; if (mappages(p->pagetable, va, PGSIZE, (uint64)new_mem, perm) == -1) {kfree(new_mem); return 0; } // 拷贝文件内容 ip = p->mapareas[idx].fp->ip; if (ip == 0) {printf("ip == 0\n"); return 0; } offset = p->mapareas[idx].offset; ilock(ip); if (p->mapareas[idx].length - offset > PGSIZE) {readi(ip, 1, va, offset, PGSIZE); } else {readi(ip, 1, va, offset, p->mapareas[idx].length - offset); } p->mapareas[idx].offset += PGSIZE; iunlock(ip); return (uint64)new_mem; }
-
实现
sys_munmap()
:uint64 sys_munmap(void) { uint64 addr; int length, idx; struct proc* p = myproc(); argaddr(0, &addr); argint(1, &length); // 找到 addr 对应的 map area for (idx = 0; idx < MAP_AREA_LENGTH; ++idx) {if (addr >= p->mapareas[idx].address && addr < p->mapareas[idx].address + p->mapareas[idx].length) break; } if (idx == MAP_AREA_LENGTH) return -1; // MAP_SHARED 写回文件 if (p->mapareas[idx].flag & MAP_SHARED) {filewrite(p->mapareas[idx].fp, addr, length); } // 解除映射 uvmunmap(p->pagetable, addr, PGROUNDUP(length) / PGSIZE, 1); if (PGROUNDUP(length) >= p->mapareas[idx].length) { // 若解除了所有映射,则文件援用计数 -1,并将地址置 0 以标记该区域闲暇 fileclose(p->mapareas[idx].fp); p->mapareas[idx].address = 0; } else { // 若未解除所有映射,则调整映射区域的范畴(地址及长度)p->mapareas[idx].length -= PGROUNDUP(length); p->mapareas[idx].address += PGROUNDUP(length); } return 0; }
-
在
exit()
中解除映射区域的映射:void exit(int status) {struct proc *p = myproc(); if(p == initproc) panic("init exiting"); // 解除 map area 的 mmap for (int i = 0; i < MAP_AREA_LENGTH; ++i) {if (p->mapareas[i].address) { // MAP_SHARED 写回文件 if (p->mapareas[i].flag & MAP_SHARED) {filewrite(p->mapareas[i].fp, p->mapareas[i].address, p->mapareas[i].length); } // 解除映射 uvmunmap(p->pagetable, p->mapareas[i].address, PGROUNDUP(p->mapareas[i].length) / PGSIZE, 1); fileclose(p->mapareas[i].fp); p->mapareas[i].address = 0; } } ... }
-
在
fork()
中复制 map areaint fork(void) { ... pid = np->pid; // 拷贝映射区 for (i = 0; i < MAP_AREA_LENGTH; ++i) {if (p->mapareas[i].address) {np->mapareas[i] = p->mapareas[i]; filedup(np->mapareas[i].fp); // 文件援用计数 +1 } } ... }
问题
- panic: uvmunmap: not mapped
起因:
munmap()
时,实际上可能存在局部文件内容没有拜访、因此没有触发页面谬误、没有建设映射的状况,这时候是对着没有映射过的内存解除映射,uvmunmap()
对这种状况会报错。
我的解决办法是将uvmunmap()
中(*pte & PTE_V) == 0
的状况由原来的panic(...)
改为continue
。碰到没有映射过的内存时,不触发 panic 而是疏忽掉持续运行。
在fork()
中uvmcopy()
时碰过到相似状况也做相似解决。
后果
播种
mmap()
的作用是将(局部)文件内容与一段内存建设映射关系,以减速程序对文件的拜访。mmap()
有两种模式,其中MAP_SHARED
模式不仅能够读文件,还能够将程序对映射内存的改变写回到文件中(如果文件可写)