Lab: mmap
mmap和munmap系统调用允许UNIX程序对地址空间进行细节控制。这可以被用来在多进程之间分享内存,把文件映射到进程地址空间,还有作为一部分用户层页错方案,比如在讲座中谈到的垃圾回收算法。在这个实验中,你将会给xv6添加mmap和munmap到xv6,关注于内存映射文件。
mmap能够用于很多场景,但是这个实验只需要实现文件内存映射的相关特性即可。你可以假设addr一直是0,意味着内核应当决定在什么虚拟地址映射这个文件。如果失败,mmap会返回 0xffffffffffffffff。length是需要映射的字节数;这可能不会和文件长度一样大。prot表示了该内存的权限。你可以假设prot是PROT_READ或者PROT_WRITE或者都有。flags可以是MAP_SHARED,这意味着这个修改应当被写回文件,或者MAP_PRIVATE,意味着不会写回。你不需在flags中实现任何其他位。fd是一个将要映射未见的打开的文件描述符。你可以假设offset是0(文件开始映射的位置)
映射相同MAP_SHARED文件的进程可以不使用相同的物理页。
*munmap(addr,length)应当移除mmap的映射。如果进程已经修改了内存,并且都是MAP_SHARED,这个修改应当先写入到文件。一个munmap调用可能会只覆盖映射过的部分区域,但你可以假设它要么在开始时取消映射,要么在结束时取消映射,或者整个区域(但请不要在中间取消映射)。
你应当你应当实现足够的mmap和munmap的特性,来通过mmaptest的测试程序。如果mmaptestmmaptest不需要使用到某个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
usertests starting
...
ALL TESTS PASSED
$
一些建议:
- 为了使得user/mmaptest.c能够编译,先添加_mmaptest到UPROGS,mmap和munmap系统调用。现在,只需要在mmap和munmap中返回错误。我们已经在kernel/fcntl.h中定义了PROT_READ等。允许mmaptest,这会在mmap调用中返回失败。
- 在page fault的时候,再进行页分配。这一位置,mmap不应当分配物理内存或者读取文件。而且,需要在发生page fault时,在usertrap中进行处理。lazy allocation的原因是确保mmap大文件是迅速的,且mmap的文件可以比物理内存大。
- 追踪mmap位每个进程映射的部分。定义一个在讲座15中描述的于VMA相关的结构,记录mmap创建的虚拟地址范围的地址,长度,许可权限,以及文件等。因为xv6内核中没有一个内存分配器,可以通过声明一个固定长度的VMAs并且从这个数组中进行分配。16大小以及足够。
- 实现mmap:找到一个未使用的虚拟地址范围来映射,并且增加一个VMA到进程映射区域的部分**?????**足够VMA应当包含一个指向映射的struct file的指针。mmap樱花当增加文件的引用计数,因此当文件关闭的时候(提示:看fileduip)这个结构不会消失。允许mmaptest:第一个mmap会成功,但是第一次获取映射的内存会造成page fault并且kill mmaptest。
- 增加代码,该代码用于处理映射空间的page fault时分配页,读入4096字节的相关文件内容到页中,并且把它映射到用户空间。用readi读取文件,这需要用一个偏移参数来读取文件(但是你将必须lock/unlock传入的inode)。不要忘记在页表中设置正确的权限位。允许mmaptest;这会到第一个munmap。
- 实现munmap:找到地址范围内的VMA,并且取消对相应页的映射(提示:使用uvmummap)。如果munmap移除了之前一次映射的所有的页,那么就要降低相关struct file的引用计数。如果一个未映射的页面被修改,并且该文件被映射为MAP_SHARED,则将该页面写回该文件。查看filewrite寻找灵感。
- 理想情况下,你的实现只能在程序修改了MAP_SHARED页的时候才写回。RISC-V PTE中的脏位说明了是否一个页被写回。然而,mmaptest不会检查非脏页是否被写回;因此你可以在写回时,不去管脏位。
- 修改exit来取消映射,就像munmap被调用那样。允许mmaptest;mmap_test应当会通过,但是fork_test可能不会通过。
- 修改fork来确保子程序拥有相同的映射区域。不要忘记增加VMA的struct file的计数。在page fault时,子程序可以自己分配独自的物理空间,即使继承的是一个MAP_SHARED。允许mmaptest,它应当通过mmap_test和fork_test。
- 允许usertests确保所有东西都运行正常。
添加struct VMA
struct VMA{
void *add;
size_t length;
int perm;
struct file* f;
}
实现sys_mmap
先获取参数,并且增加文件的引用计数。
然后找到空闲VMA,写入VMA的信息。
然后就是扩容。
想法一:因为在扩容前,sz的大小并不等于已分配的全部页的总和,可能会小于,但是这少的一部分其实已经被分配了。因为我不了解,如果访问到这块内存的话,是否会报page fault,而且即使报了page fault,在trap里面也不知道如何去辨别它。所以最后决定在mmap时,就把内容写入到内存中,因为是已分配页,所以说并不会导致内存分配。显然这种方法会在munmap时候非常麻烦。
想法二:然后想到好像文档里说了分配的内存大小可以不等于文件的大小。那么起始点就从一个新的页开始,结束点在一个独占的页。然后想munmap是否会处理非整页的内存范围,看了测试文件,munmap的长度都是页的倍数,起始点也是页的起始位置。
添加sys_mmap()
注意当flags是MAP_SHARED时,打开文件的模式不能是只读的。
uint64 sys_mmap(void){
uint64 addr;
int prot,flags,fd;
struct file *f;
uint len,off;
struct proc* p = myproc();
if(argaddr(0, &addr) < 0 || argint(1,(int *)&len) < 0 || argint(2,&prot) < 0 || argint(3,&flags) < 0 || argfd(4,&fd,&f) < 0 || argint(5,(int *)&off) < 0)
return -1;
filedup(f);
struct VMA *v = findEmptyVMA(p);
// No available VMA slot.
if(!v)
return -1;
v->avai = 1;
v->f = f;
v->addr = PGROUNDUP((uint64)p -> sz);
v->perm = prot;
v->flags = flags;
v->off = off;
v->length = len;
p->sz = (uint64 )v->addr + PGROUNDUP((uint64)len);
return (uint64)v->addr;
}
修改trap.c
跟之前的Lazy alloc类似,不过要加上从文件系统读入数据到内存的部分。
else {
uint64 va = r_stval();
uint64 sp = p->trapframe->sp;
// not a page fault
if((r_scause() != 13 && r_scause() != 15) || va >= p->sz || (va < PGROUNDDOWN(sp) && va >= PGROUNDDOWN(sp - PGSIZE))){
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
else{
va = PGROUNDDOWN(va + 1);
char* mem;
// if kalloc fails,kill the proc
if((mem = kalloc()) == 0){
p->killed = 1;
}
else{
memset(mem,0,PGSIZE);
struct VMA* vma = findVMA(va);
if((mappages(p->pagetable, va, PGSIZE, (uint64)mem,PTE_W | PTE_R | PTE_X |PTE_U|PTE_V )) != 0){
kfree(mem);
// if fails,unmap and free the page
uvmunmap(p->pagetable,va,1,0);
}
uint readsz = PGSIZE;
if(va + PGSIZE > vma->addr + (uint64)vma->length){
readsz = vma->addr + (uint64)vma->length - va;
}
struct file *f = vma->f;
// printf("readsz %p\n",readsz);
ilock(f->ip);
readi(f->ip,1,va,vma->off + (uint)(va - vma->addr),readsz);
iunlock(f->ip);
}
}
}
添加sys_munmap
因为测试文件中的munmap区域都是从头开始的整页的操作,所以针对case实现还是比较简单的。
先判断是否是取消全部映射,如果是的话,就要修改VMA以及减少文件的计数。
如果flags是MAP_SHARED,且该页已分配,那么需要把其中的内容写入到文件系统。
如果该页已分配,则清空内容。
清除内存部分是只是清空内容,并不释放页。释放页会导致中空。
最后修改VMA的信息。
uint64 sys_munmap(void){
uint64 addr;
uint len;
if(argaddr(0, &addr) < 0 || argint(1,(int*)&len) <0)
return -1;
struct VMA* vma = findVMA(addr);
// munmap all region
if(addr + (uint64 )len >= vma->addr + (uint64 )vma->length){
if(vma->flags == MAP_SHARED){
begin_op();
ilock(vma->f->ip);
writei(vma->f->ip,1,vma->addr,vma->off,vma->length);
iunlock(vma->f->ip);
end_op();
}
// When the page already allocated,free it.
for(int i = 0;i < PGROUNDUP(len)/PGSIZE;i++){
if(walkaddr(myproc()->pagetable,vma->addr + i * PGSIZE)){
uvmunmap(myproc()->pagetable,vma->addr + i * PGSIZE, 1,0);
}
}
fileclose(vma->f);
vma->avai = 0;
}else{
if(vma->flags == MAP_SHARED){
begin_op();
ilock(vma->f->ip);
writei(vma->f->ip,1,vma->addr,vma->off,len);
iunlock(vma->f->ip);
end_op();
}
// When the page already allocated,free it.
for(int i = 0;i < PGROUNDUP(len)/PGSIZE;i++){
if(walkaddr(myproc()->pagetable,vma->addr + i * PGSIZE)){
uvmunmap(myproc()->pagetable,vma->addr + i * PGSIZE, 1,0);
}
}
vma->addr = vma->addr + len;
vma->off = vma->off + len;
vma->length = vma->length - len;
}
return 0;
}
修改exit
进程退出需要把映射全部取消。
逻辑和sys_munmap类似。
// Munmap all mmap-ed region.
struct VMA* vma = p->Vlist;
for(int i = 0;i < 16;i++){
if(vma->avai){
for(int i = 0;i < PGROUNDUP(vma->length)/PGSIZE;i++){
// If already allocated,unmap it.
if(walkaddr(myproc()->pagetable,vma->addr + i * PGSIZE)){
if(vma->flags == MAP_SHARED){
begin_op();
ilock(vma->f->ip);
writei(vma->f->ip,1,vma->addr + i * PGSIZE,vma->off + i * PGSIZE,PGSIZE);
iunlock(vma->f->ip);
end_op();
}
uvmunmap(myproc()->pagetable,vma->addr + i * PGSIZE, 1,0);
}
}
fileclose(vma->f);
vma->avai = 0;
}
vma++;
}
修改fork()
fork需要把父进程的Vlist复制到子进程中,并且增加打开文件的引用计数。
// Copy the Vlist from parent process.
for(int i = 0;i < 16;i++){
np->Vlist[i].f = p->Vlist[i].f;
np->Vlist[i].addr = p->Vlist[i].addr;
np->Vlist[i].length = p->Vlist[i].length;
np->Vlist[i].avai = p->Vlist[i].avai;
np->Vlist[i].flags = p->Vlist[i].flags;
np->Vlist[i].off = p->Vlist[i].off;
np->Vlist[i].perm = p->Vlist[i].perm;
if(np->Vlist[i].avai)
filedup(np->Vlist[i].f);
}
修改vm.c的相关内容
vm.c中需要修改部分内容来冷处理某些没有被实际分配的页。
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){
// panic("uvmunmap: not mapped");
return;
}
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
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)
return 0;
// panic("uvmcopy: page not present");
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;
}
这个lab跟之前的lazy allocation很像,算是一个小变形。所以不会很难。