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;
}
}