MIT-6.S081-2021 Lab10: mmap
最后一个 lab 了... https://pdos.csail.mit.edu/6.S081/2021/labs/mmap.html
1. 要求
You should implement enough mmap and munmap functionality to make the mmaptest test program work. If mmaptest doesn't use a mmap feature, you don't need to implement that feature.
简单来说,就是实现一个阉割版的 mmap 和 munmap,因为完全体的 mmap 功能较多,这里只实现 memory-mapping a file ,文件映射。
- mmap
mmap
的接口形式为
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr
为文件要映射到的虚拟地址,该 lab 中默认为 0,length
为映射长度,prot
为权限,如 可读/可写,flags
为映射方式,这里只有 2 种情况:
MAP_SHARED
,执行munmap
的时候,将改动写回到文件当中MAP_PRIVATED
,执行munmap
的时候,不写回改动到文件当中
fd
为目标文件描述符,offset
为文件偏移量。
- munmap
munmap
的接口形式为:
int munmap(addr, length)
addr
为解除映射的起始位置,length
为解除映射的长度。
2. 分析
2.1 mmap
mmap 的实现主要注意如下几点:
- 采用 lazy allocation 的方式,首次执行时,只对进程的虚拟内存进行分配(参考 sbrk ),但是不分配物理内存,因为假如文件过大时,效率会比较低下。
- 由于我们是 lazy alloction,所以需要对文件引用计数 +1 ,防止外部 close 文件,导致无法读取内容。
- 由于我们采用 lazy allocation 的方式,因此真正读取文件的时机,应该迁移到触发缺页异常的时候,当 page fault 时,分配物理页,建立映射,并且将文件内容读取到指定页。
- 此处还可以进行一项优化,如当权限为只读的情况下,首先文件的内容已经在内核中有一份了(参考 inode 的 buffer),因此可以将用户进程虚拟页设置为只读,并且映射到该 buffer,需要注意的是,buffer 需要是 page align 的,其次还要用到物理页的引用技术,这里参考 lab-cow。
- 注意权限,比如
fd
打开的文件结构,如果不可写,但是mmap
的prot
设置可写,则是不可行的。 - 根据 hints,我们需要针对
struct proc
建立一个 VMA(virtual memory area) 结构数组,用于管理我们映射了哪些区域,记录权限。 - fork 时,需要注意,将 VMA 结构拷贝过去
- 此处可以进行一项优化,按上述的默认机制,我们触发缺页时会分配内存,并将文件内容写入。由此内存中已经有一份数据了,因此可以参考 copy-on-write 技术,子进程映射对应物理页,设置不可写,当触发写异常时,再分配新的物理页进行页映射。
2.2 munmap
munmap 的实现主要需要注意如下几点:
- 由于页面解除映射可以只解除部分区域,因此我们在 VMA 结构中,需要记录哪些页已经解除映射了
- 当全部区域都解除映射时,减少对应文件引用计数,回收 VMA 。
2.3 VMA
根据上述分析,VMA 结构定义如下:
struct vma { uint64 addr; // map start addr uint64 length; // map area size uint flags; // MAP_SHARED or MAP_PRIVATE uint prot; // permisson rwx struct file* file; //refer file uint8 valid; // vma is valid uint64 dirtyflag; }; // Per-process state struct proc { // some code ... struct vma vma[16]; // virtual memory area by mmap };
获取 vma 接口:
struct vma* getvma(uint64 va) { struct proc* proc = myproc(); struct vma* vma = 0; for (int i = 0; i < 16; i++) { if (proc->vma[i].valid == 0) continue; uint64 addr = proc->vma[i].addr; uint64 length = proc->vma[i].length; if (addr <= va && (va < (addr + length))) { vma = &proc->vma[i]; break; } } return vma; }
3. 实现
3.1 mmap
主要执行初始化 VMA 的工作。对于映射的区域,以页为单位,用 bitmap 的思路管理,方便 munmap 时确定什么时候执行释放。
/* void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); */ uint64 sys_mmap(void) { uint64 addr; // user pointer to array of two integers int length; int prot; int flags; struct file* file; int offset; if(argaddr(0, &addr) < 0) return -1; if(argint(1, &length) < 0) return -1; if(argint(2, &prot) < 0) return -1; if(argint(3, &flags) < 0) return -1; if(argfd(4, 0, &file) < 0) return -1; if(argint(5, &offset) < 0) return -1; if(!file->readable && (prot & PROT_READ)) return -1; if(!file->writable && (prot & PROT_WRITE) && !(flags & MAP_PRIVATE)) return -1; // offset and addr can asume 0 struct proc* proc = myproc(); // step 1 : alloc vma struct vma * vma = 0; for (int i = 0; i < 16; i++) { if (proc->vma[i].addr == 0) { vma = &proc->vma[i]; break; } } if (vma == 0) return -1; vma->length = length; vma->flags = flags; vma->prot = prot; vma->file = file; vma->valid = 1; // step 2 : add heap size uint64 retaddr = PGROUNDUP(proc->sz); proc->sz = retaddr + length; vma->addr = retaddr; // step 3 : add file ref cnt filedup(vma->file); // step 4 : record which page been mapped int pgcnt = length / PGSIZE; int mask = 1; for (int i = 0; i < pgcnt; i++) { vma->dirtyflag |= (mask << i); } return retaddr; }
缺页处理
void usertrap(void) { // some code ... else if (r_scause() == 15 || r_scause() == 13){ // 15 write page fault 13 read page fault uint64 va = r_stval(); if(va >= MAXVA || (va <= PGROUNDDOWN(p->trapframe->sp) && va >= PGROUNDDOWN(p->trapframe->sp) - PGSIZE)){ p->killed = 1; } else { if (pagefault(p->pagetable, va) < 0) p->killed = 1; } } // some code ... }
处理文件读取,需要根据偏移量,确定读取文件的哪一处内容,以页为单位。此外当读取的内容不足一页时,剩下的区域需要清 0 。因为 kalloc
分配的页中写了脏数据。
// only work for mmap int pagefault(pagetable_t pagetable, uint64 fault_va) { // step 1 : check addr is in vma struct vma* vma = getvma(fault_va); if (vma == 0) return -1; // step 2 : check permission /* #define PROT_NONE 0x0 #define PROT_READ 0x1 #define PROT_WRITE 0x2 #define PROT_EXEC 0x4 #define MAP_SHARED 0x01 #define MAP_PRIVATE 0x02 */ if (r_scause() == 13 && (!(vma->prot & PROT_READ) || !(vma->file->readable))) return -1; // not read permisson but excute read if (r_scause() == 15 && (!(vma->prot & PROT_WRITE) || !(vma->file->writable))) return -1; // not write permisson but excute write // step 3 : alloc new page and map it , setup permission flag void* dst_pa = kalloc(); if (dst_pa == 0){ return -1; } uint8 flag = (vma->prot & PROT_READ) ? PTE_R : 0; flag = (vma->prot & PROT_WRITE) ? (flag | PTE_W) : flag; if (mappages(pagetable, PGROUNDDOWN(fault_va), PGSIZE, (uint64)dst_pa, PTE_U | PTE_X | flag) < 0){ kfree(dst_pa); return -1; } // step 4 : load file content to memory uint offset = PGROUNDDOWN(fault_va) - vma->addr; vma->file->off = offset; int read = 0; if ((read = fileread(vma->file, PGROUNDDOWN(fault_va), PGSIZE)) < 0) return -1; // should clear zero if (read < PGSIZE) { uint64 pa = walkaddr(pagetable, PGROUNDDOWN(fault_va)) + read; memset((void*)pa, 0, PGSIZE - read); } return 0; }
3.2 munmap
这里主要根据 flags
确定是否要将改动写回文件,由于前文的 lazy allocation 策略,此处写回执行 walkaddr
时,只会写回改动到的页面。
其次根据 vma.dirtyflag
确定是否已经全部接触映射。
uint64 sys_munmap(void) { uint64 addr; int length; if(argaddr(0, &addr) < 0) return -1; if(argint(1, &length) < 0) return -1; struct vma* vma = getvma(addr); if (vma == 0) return -1; if (length > vma->length || addr < vma->addr) return -1; // step 1 : check strategy int start = ((addr - vma->addr) / PGSIZE); int end = (length % PGSIZE) == 0 ? length / PGSIZE : (length / PGSIZE) + 1; if (vma->flags & MAP_PRIVATE) { // do not write back to file goto finish; } else if (vma->flags & MAP_SHARED) { for (int i = start; i < end; i++) { if (walkaddr(myproc()->pagetable, (vma->addr + PGSIZE * i))) { vma->file->off = PGSIZE * i; filewrite(vma->file, vma->addr, PGSIZE); } } } uint mask = 1; finish: for (int i = start; i < end; i++) { vma->dirtyflag &= ~(mask << i); } printf("dirty flag = %d\n", vma->dirtyflag); if (vma->dirtyflag == 0) { printf("all area unmmap , start recyle\n"); vma->valid = 0; fileclose(vma->file); } return 0; }
3.3 fork
fork 进程时也需要增加一些处理:
int fork(void) { // some code ... // copy mmap area for (i = 0; i < 16; i++) { if(p->vma[i].valid) { np->vma[i].addr = p->vma[i].addr; np->vma[i].dirtyflag = p->vma[i].dirtyflag; np->vma[i].file = p->vma[i].file; np->vma[i].length = p->vma[i].flags; np->vma[i].flags = p->vma[i].flags; np->vma[i].length = p->vma[i].length; np->vma[i].prot = p->vma[i].prot; np->vma[i].valid = 1; filedup(np->vma[i].file); } // some code ... }
其次,uvmcopy 及 uvmunmap 也需要允许 PTE_V 为 0 的 pte,因为前面实现了 lazy allocation 的策略。
int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) { pte_t *pte; uint64 pa, i; uint flags; char *mem; for(i = 0; i < sz; i += PGSIZE){ if((pte = walk(old, i, 0)) == 0) panic("uvmcopy: pte should exist"); if((*pte & PTE_V) == 0) continue; // panic 修改为 continue; pa = PTE2PA(*pte); flags = PTE_FLAGS(*pte); if((mem = kalloc()) == 0) goto err; memmove(mem, (char*)pa, PGSIZE); if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){ kfree(mem); goto err; } } return 0; err: uvmunmap(new, 0, i / PGSIZE, 1); return -1; } void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free) { uint64 a; pte_t *pte; if((va % PGSIZE) != 0) panic("uvmunmap: not aligned"); for(a = va; a < va + npages*PGSIZE; a += PGSIZE){ if((pte = walk(pagetable, a, 0)) == 0) panic("uvmunmap: walk"); if((*pte & PTE_V) == 0) continue; // panic 修改为 continue; if(PTE_FLAGS(*pte) == PTE_V) panic("uvmunmap: not a leaf"); if(do_free){ uint64 pa = PTE2PA(*pte); kfree((void*)pa); } *pte = 0; } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了