Lab: mmap
实验准备
切换到mmap分支
$ git fetch
$ git checkout mmap
$ make clean
Background
mmap和munmap系统调用允许UNIX程序对其地址空间进行详细控制。它们可用于在进程之间共享内存,将文件映射到进程地址空间,并作为用户级缺页方案的一部分,例如在讲座中讨论的垃圾收集算法。在这个实验中,您将向xv6添加mmap和munmap,重点是内存映射文件。
以下是mmap、munmap函数声明
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
需求
mmap可以以多种方式调用,但该实验只要求使用其与内存映射文件相关的功能子集。我们可以假设addr始终为零,这意味着内核应该决定映射文件的虚拟地址。mmap返回该地址,如果失败则返回0xffffffffffffffff。length是要映射的字节数;它可能与文件的长度不同。prot指示内存是否应该可读、可写和/或可执行;我们可以假设prot为PROT_READ或PROT_WRITE或两者都是。flags将是MAP_SHARED,表示对映射内存的修改应写回文件,或者是MAP_PRIVATE,表示不写回文件。您不必实现flags中的其他位。fd是要映射的文件的打开文件描述符。我们可以假设偏移量为零(它是映射文件的起始点)。
munmap(addr, length)应在指定的地址范围内删除mmap映射。如果进程修改了内存并将其映射为MAP_SHARED,修改应首先写入文件。munmap调用可以覆盖mmap的部分区域,但您可以假设它要么在开头解除映射,要么在结尾解除映射,要么整个区域都解除映射(但不会在区域中间打洞)。
您应该实现足够的mmap和munmap功能,以使mmaptest测试程序工作。
The solution
首先在kernel/proc.h定义VMA(虚拟内存区域)相对应的结构,用于记录由mmap创建的虚拟内存范围的地址、长度、权限、文件等。
struct VMA{
uint64 addr;//虚拟内存起始地址
int length;//要映射的字节数
int prot;//页表标志位
int flags;//是否要写回文件
int fd;//文件描述符
int offset;//偏移量(映射文件起始点)
struct file *file;//文件结构体
int free_len;//被取消映射的长度
};
由于xv6 的进程是可以映射多个文件,并且xv6内核中没有内存分配器,因此可以在proc结构体中声明一个固定大小的vma数组 VMA vma[16];
然后在kernel/sysfile.c中定义sys_mmap系统调用,这里要做的是检验用户传入的实参是否合法,如果合法就分配一个VMA,懒分配一段虚拟内存,并增加文件引用。
uint64 sys_mmap(void){
uint64 addr;
int length, prot, flags, fd, offset;
struct file *file;
argaddr(0, &addr);
argint(1, &length);
argint(2, &prot);
argint(3, &flags);
if(argfd(4,&fd, &file) < 0)
return -1;
argint(5,&offset);
if(addr < 0 || length < 0 || prot < 0 || offset < 0)
return -1;
if((prot & PROT_READ) && !file->readable)//判断读权限
return -1;
if((flags & MAP_SHARED) && (prot & PROT_WRITE) && !file->writable)//判断写权限
return -1;
length = PGROUNDUP(length);//对齐
struct proc *p = myproc();
if(p->sz + length > MAXVA)//判断是否还有虚拟内存
return -1;
int idx = -1;
for(int i = 0; i < 16; i++)//分配VAM
if(p->vma[i].addr == 0){
idx = i;
break;
}
if(idx == -1)return -1;
p->vma[idx].addr = p->sz;//由内核决定映射文件的虚拟地址
p->vma[idx].length = length;
p->vma[idx].prot = prot;
p->vma[idx].flags = flags;
p->vma[idx].fd = fd;
p->vma[idx].offset = offset;
p->vma[idx].file = file;
p->vma[idx].free_len = 0;
p->sz += length;//lazy分配
filedup(file);//增加文件引用计数
return p->vma[idx].addr;//这里要返回以前的p->sz
}
由于mmap使用了懒分配机制,我们需要在kernel/trap.c中处理由其引起的Load page fault。分配对应的物理内存,并将文件内容读入,最后将虚拟地址映射到物理地址上。
void
usertrap(void)
{
...
else if(r_scause() == 0xd){
uint64 va = r_stval();//出错的虚拟地址
int idx = -1;
for(int i = 0; i < 16; i++)//通过出错地址寻找对应的VAM同时判断其合法性
if(va >= p->vma[i].addr && va < p->vma[i].addr + p->vma[i].length){
idx = i;
break;
}
if(idx == -1)goto err;
char *mem;
if((mem = kalloc()) == 0)//物理内存实在不够
setkilled(p);
else{
memset(mem, 0, PGSIZE);
va = PGROUNDDOWN(va);
struct inode *ip = p->vma[idx].file->ip;
ilock(ip);
readi(ip, 0, (uint64)mem, p->vma[idx].offset + (va - p->vma[idx].addr), PGSIZE);//读取文件内容
iunlock(ip);
int pte_flag = PTE_U;//设置标志位,需要注意的是我们映射的是用户态的页表
if(p->vma[idx].prot & PROT_READ) pte_flag |= PTE_R;
if(p->vma[idx].prot & PROT_WRITE) pte_flag |= PTE_W;
if(p->vma[idx].prot & PROT_EXEC) pte_flag |= PTE_X;
if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, pte_flag) != 0){
kfree(mem);
setkilled(p);
}
}
}
else {
err:
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
setkilled(p);
}
...
}
然后在kernel/sysfile.c中定义sys_munmap系统调用,首先查找地址范围的VMA,然后判断本次取消是否是从上次取消的末位开始(它的取消保证从映射的虚拟首地址开始,但可能分多段进行),然后判断是否需要写回文件,然后才取消映射,最后我们要判断mmap的映射是否被取消完毕,若取消完成则需要减少文件的引用并释放VMA。
uint64 sys_munmap(void){
uint64 addr;
int length;
argaddr(0, &addr);
argint(1, &length);
if(addr < 0 || length < 0)return -1;
struct proc *p = myproc();
int idx = -1;
for(int i = 0; i < 16; i++)//寻找对应VAM
if(addr >= p->vma[i].addr && addr < p->vma[i].addr + p->vma[i].length){
idx = i;
break;
}
if(idx == -1)return -1;
length = PGROUNDUP(length);//对齐
addr = PGROUNDDOWN(addr);
if(p->vma[idx].free_len + p->vma[idx].addr != addr)//必须从上次取消的结尾开始取消
return -1;
if(p->vma[idx].flags & MAP_SHARED)//需要写回
filewrite(p->vma[idx].file, addr, length);
uvmunmap(p->pagetable, addr, length/PGSIZE, 1);//取消映射
if(p->vma[idx].free_len + length == p->vma[idx].length){//若释放了mmap所有映射页面,我们需要减少对于文件的引用并释放VMA
fileclose(p->vma[idx].file);
p->vma[idx].addr = 0;
}
else p->vma[idx].free_len += length;
return 0;
}
我们不仅仅要在调用munmap时取消映射,在进程退出时也需要取消,在kernel/proc,c中修改exit函数定义:
void
exit(int status)
{
...
for(int i = 0; i < 16; i++){
if(p->vma[i].addr){
int len = p->vma[i].length - p->vma[i].free_len;
uint64 addr = p->vma[i].addr + p->vma[i].free_len;
if(p->vma[i].flags & MAP_SHARED)//需要写回
filewrite(p->vma[i].file, addr, len);
uvmunmap(p->pagetable, addr, len/PGSIZE, 1);//释放映射
fileclose(p->vma[i].file);
p->vma[i].addr = 0;
}
}
// Close all open files.
...
}
由于mmaptest中有fork测试,故我们需要在fork子进程时拷贝父进程的VMA并增加对文件的引用
int
fork(void)
{
...
memmove((char*)np->vma,(char*)p->vma,sizeof p->vma);//拷贝VMA
for(int i = 0; i < 16 ;i++)//增加引用
if(np->vma[i].addr)
filedup(np->vma[i].file);
// Copy user memory from parent to child.
...
}
最后需要注意的点是我们在调用munmap、exit、fork时会涉及以下两个函数,它们会检测虚拟页面是否映射到物理页上,由于我们使用lazy分配机制,我们需要忽略这两个panic。
注:理论上来说lazy分配还会导致下述函数中对walk的检测失败,但这种情况只有
- 一级页表项or二级页表项指向空
- 在mmap调用与munmap调用之间对映射的文件不做任何访问操作
的情况下才会发生,而要导致二级页表项为空的话就必须在两个三级表项之间有512个三级表项(对应着512个物理页)的间隔,但在mmaptest中分配的物理页都是紧紧挨着,并且在mmap调用与munmap调用之间有访问操作,故walk对应的panic不会发生。
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
...
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;//忽略由lazy分配而未映射的页面
//panic("uvmunmap: not mapped");
...
}
}
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
...
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;//忽略由lazy分配而未映射的页面
//panic("uvmcopy: page not present");
...
}
...
}