实现原理 mmap 这次 lab 是要给 xv6 添加 mmap 和 munmap 系统调用。
mmap 的好处在于可以将一个文件直接映射到进程的地址空间中,从而避免了不必要的数据复制,提高了文件操作的效率。与使用 read 和 write 系统调用不同,mmap 操作不需要将文件数据从内核缓冲区复制到用户缓冲区,也不需要将用户缓冲区中的数据复制回内核缓冲区。相反,它通过映射文件的方式,将文件数据直接映射到了进程的地址空间中,因而可以提高文件操作的效率。
同时 mmap 也避免了由于使用 read 和 write 系统调用而造成的在用户空间和内核空间的上下文切换,节省了系统调用的开销。
系统调用声明 mmap 系统调用的函数声明为:
1 void *mmap (void *addr, uint64 len, int prot, int flags, int fd, uint64 offset) ;
addr 为文件在用户地址空间的起始地址,一般传入 0,由内核设置;
len 为要映射的字节数量;
prot 为权限字段,指明该文件是可读(PROT_READ)、可写(PROT_WRITE)或可执行(PROT_EXEC)的;
flags 为标记位,标记映射的模式,MAP_SHARED 模式标识在 munmap 的时候需要把改动写回磁盘,MAP_PRIVATE 模式则不需要;
fd 是文件的描述符;
offset 为文件起始位置到开始映射的位置的偏移量。
munmap 系统调用的函数声明为:
1 int munmap (void *addr, uint64 len) ;
addr 为从哪里开始解除映射;
len 为解除映射的字节数。
代码实现 添加 mmap 和 munmap 系统调用的过程这里就省略了。直接来看实现。
首先,为了能够让用户进程知道关于文件映射的信息,需要在 proc 结构体记录下。新增 vma 结构体,来存储文件映射的相关信息:
1 2 3 4 5 6 7 8 9 10 struct vma { int valid; uint64 addr; uint64 len; int prot; int flags; int fd; struct file *file ; uint64 offset; };
并且在 proc 结构体中添加一个 vma 数组,根据 hint,大小为 16 即可:
1 struct vma vmatable [NVMA ];
接着在 kernel/sysfile.c 中实现 sys_mmap 函数。大致流程如下:
接收 mmap 系统调用传递的参数;
判断参数是否可以满足映射条件:
只读文件在 MAP_PRIVATE 模式下,是可写的;
只读文件在 MAP_SHARED 模式下,是不可写的。
从进程中记录的 vma 中找出一个空闲的 vma,并在进程的 heap 中找出一段可用的内存,将这段内存的起始地址作为系统调用的返回值。注意在这里是不进行内存分配的,只是标记,跟 lazy alloction 是一样的,这样可以让映射比内存空间更大的文件成为可能。为了和进程正在使用的地址空间区分开,选择从 heap 的高位置开始向下扩展来映射文件,即从 TRAPFRAME 开始。
设置 vma 的值;
filedup 对应文件;
mmap should increase the file’s reference count so that the structure doesn’t disappear when the file is closed.
close 系统调用关闭是的一个打开的文件描述符,只是减少该文件的打开引用数,在这里增加一次引用后,就算调用了 close 也不会影响到对已经映射的内存。
返回映射的起始地址;
实现如下:
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 uint64 sys_mmap (void ) { uint64 len, offset; int prot, flags, fd; if (argaddr(1 , &len) < 0 || argint(2 , &prot) < 0 || argint(3 , &flags) < 0 || argint(4 , &fd) < 0 || argaddr(5 , &offset) < 0 ) { return -1 ; } struct proc *p = myproc(); struct file *file = p->ofile[fd]; if ((file->readable && !file->writable) && (prot & PROT_WRITE) && (flags & MAP_SHARED)) { return -1 ; } struct vma *vma = 0 ; int found = 0 ; uint64 addr = TRAPFRAME; for (int i = 0 ; i < 16 ; i++) { if (!p->vmatable[i].valid && !found) { found = 1 ; vma = &p->vmatable[i]; } else if (p->vmatable[i].valid && p->vmatable[i].addr < addr) { addr = p->vmatable[i].addr; } } if (!found) { return -1 ; } addr = addr - len; vma->valid = 1 ; vma->fd = fd; vma->file = file; vma->len = len; vma->offset = offset; vma->prot = prot; vma->flags = flags; vma->addr = addr; filedup(vma->file); return addr; }
完成这一步后,在用户程序中调用 mmap 就会返回一个正确的映射后的起始地址了,但是当进行访问的时候,由于并没有分配内存,就会触发 page fault,所以跟 lazy alloction 一样,在 kernel/trap.c#usertrap 中处理 page fault。
1 2 3 4 5 6 7 8 9 10 } else if ((which_dev = devintr()) != 0 ){ } else if (r_scause() == 13 || r_scause() == 15 ) { uint64 va = r_stval(); if (mmaphandler(va) == -1 ) { p->killed = 1 ; } } else {
kernel/vm.c#mmaphandler 函数接收一个虚拟内存地址(发生 page fault 的地址),来处理 pagefault。
在 mmaphandler 中,我们需要做以下事情:
找出 va 是映射在哪个页中,也就是需要找出对应的 vma;
给 vma 正式分配内存;
根据 vma 中记录的 prot 来设置 PTE 的 flags;
将物理地址和虚拟地址进行映射;
使用 readi 将文件读到刚分配的内存中。在进行操作的时候要开启事务,并且对 inode 上锁。
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 int mmaphandler (uint64 va) { int i; struct proc *p = myproc(); struct vma *vma = 0 ; struct inode *ip ; for (i = 0 ; i < NVMA; i++) { struct vma *v = &p->vmatable[i]; if (v->valid) { if (va >= v->addr && va < (v->addr + v->len * PGSIZE)) { vma = v; break ; } } } if (vma == 0 ) { return -1 ; } uint64 ka = (uint64)kalloc(); if (ka == 0 ) { return -1 ; } memset ((void *) ka, 0 , PGSIZE); va = PGROUNDDOWN(va); pte_t * pte; if ((pte = walk(p->pagetable, va, 0 )) != 0 && (*pte & PTE_V) != 0 ) { kfree((void *) ka); return -1 ; } int flags = PTE_FLAGS(*pte); if (vma->prot & PROT_READ) { flags |= PTE_R; } if (vma->prot & PROT_WRITE) { flags |= PTE_W; } if (vma->prot & PROT_EXEC) { flags |= PTE_X; } if (mappages(p->pagetable, va, PGSIZE, ka, flags | PTE_U) != 0 ) { kfree((void *) ka); return -1 ; } ip = vma->file->ip; begin_op(); ilock(ip); if (readi(ip, 0 , ka, PGROUNDDOWN(vma->offset + (va - vma->addr)), PGSIZE) < 0 ) { return -1 ; } iunlock(ip); end_op(); return 0 ; }
到这里就可以访问我们映射到内存中的文件了。
接下来要实现 munmap 系统调用(kernel/sysfile.c#sys_munmap),注意根据文档,munmap 可以是一部分,但是不会是在中间。
An munmap call might cover only a portion of an mmap-ed region, but you can assume that it will either unmap at the start, or at the end, or the whole region (but not punch a hole in the middle of a region).
在 sys_munmap 函数中我们要处理以下事情:
接收 addr 和 len 参数;
找出 addr 对应的 vma;
判断 vma 是否是 MAP_SHARED 模式,如果是就调用 filewrite 将文件写回磁盘;
取消 munmap 部分的映射;
调整 vma 的长度和起始地址。
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 uint64 sys_munmap (void ) { uint64 addr, len; if (argaddr(0 , &addr) < 0 || argaddr(1 , &len) < 0 ) { return -1 ; } int i; struct vma *vma = 0 ; struct proc *p = myproc(); for (i = 0 ; i < NVMA; i++) { struct vma *v = &p->vmatable[i]; if (v->valid && (v->addr <= addr && addr < (v->addr + len))) { vma = v; } } if (!vma) { return -1 ; } if (vma->flags & MAP_SHARED && vma->file->writable) { filewrite(vma->file, addr, len); } uvmunmap(p->pagetable, addr, len / PGSIZE, 1 ); vma->len -= len; if (vma->len == 0 ) vma->valid = 0 ; else { if (vma->addr == addr) vma->addr += len; } return 0 ; }
注意修改 uvmunmap,否则会报 panic。
当进程退出的时候,即调用 kernel/proc.c#exit,我们需要将它映射的所有文件都 munmap 掉,就像调用 munmap 系统调用。由于我的实现是父子进程并不共享物理内存,所以直接释放掉即可。
1 2 3 4 5 6 7 8 9 10 11 12 int i;for (i = 0 ; i < NVMA; i++) { struct vma *v = &p->vmatable[i]; if (v->valid) { if (v->flags & MAP_SHARED && v->file->writable) { filewrite(v->file, v->addr, v->len); } uvmunmap(p->pagetable, v->addr, v->len/PGSIZE, 1 ); v->valid = 0 ; } }
最后修改 kernel/proc.c#fork,在子进程复制父进程的内存时,可能会复制到没有映射或无效的条目,也要修改 uvmcopy 将 panic 去掉。在 fork 函数中只需要将 vma 复制一份给子进程就可以了。
1 2 3 4 for (i = 0 ; i < NVMA; i++) { np->vmatable[i] = p->vmatable[i]; }
到这里 mmaptest 和 fork test 就都可以通过了。
运行结果
这次的 grader 倒是顺利跑过了。
总结 这个 lab 是对 file system 的进一步深入,不过我感觉跟虚拟内存可能更加相关?难点主要是在 mmap 系统调用,要考虑如何给 vma 找到一块合适的内存空间,想清楚这里之后其它的就比较简单了。page fault 的处理跟 lazy alloction 是一样的。munmap 系统调用就相当于做了一次反操作。