概述
这次实验要求实现Linux中的mmap函数的一个子集,相当于在第五次实验Lazy Allocation中加上了文件的操作。难度比较难定义,因为这个“子集”还是比较模糊的,如果仅仅只针对测试程序,做出一些简化性的假设,难度就不会太大,但如果不做这些假设,难度就会非常高。
内容
为了简化问题,首先做出一些假设:
- 调用mmap的参数length是页宽的倍数,调用munmap的参数addr和length也都是页宽的倍数。
- 单个进程调用mmap映射的虚拟地址空间总大小不超过1G(包括被munmap回收的那些地址空间)。
- 单个进程当前被mmap的虚拟地址段数不超过16。
那么就可以写代码了,首先是vma结构体:
struct vma {
uint64 addr, oaddr; int valid, length, prot, flags;
struct file *fd;
};
其中valid表示当前vma是否已被使用,prot,flags为调用mmap时传入的参数,addr和length为该地址段的当前首地址和长度,oaddr为初始首地址,就是这一段在最开始被映射时的首地址。因为munmap的存在,地址段的当前首地址会随之改变,但在计算文件偏移量的时候,文件的0偏移对应的还是地址段的初始首地址,所以需要专门保存。然后在proc结构体加入两个属性:
struct vma vmas[16];
uint64 mmapsz;
mmapsz为被映射的地址空间大小(包括被munmap回收的那些地址空间,即munmap之后mmapsz不减小),这里定义所有mmap的地址空间为128G到129G之间,即从1L << 37
开始。
接着就是sys_mmap:
uint64 sys_mmap(void) { int length, prot, flags; struct file *fd;
if (argint(1, &length) < 0 || argint(2, &prot) < 0 || argint(3, &flags) || argfd(4, 0, &fd)) return -1;
if (length % PGSIZE != 0 || (fd->writable == 0 && (prot & PROT_WRITE) && flags == MAP_SHARED)) return -1;
struct proc *p = myproc();
uint64 addr = (1L << 37) + p->mmapsz;
for (int i = 0; i < 16; i++) if (!p->vmas[i].valid) {
p->vmas[i].valid = 1; p->vmas[i].addr = p->vmas[i].oaddr = addr;
p->vmas[i].length = length; p->vmas[i].prot = prot;
p->vmas[i].flags = flags; p->vmas[i].fd = fd;
filedup(fd); p->mmapsz += length; return addr;
}
return -1;
}
首先需要判断一下权限,如果映射的内存段可以被写且定义被写的内存需要写回文件,而文件又不能被写,这就矛盾了,直接返回失败。申请地址的时候这里大大简化了,就是直接从1L << 37 + p->mmapsz
开始,申请完p->mmapsz
直接增加。赋值操作很清晰,没啥说的。
然后是对缺页中断的处理,还是一样,这里抽象成了函数,trap.c内的修改和Lazy Allocation实验一模一样,这里仅给出handle_page函数:
uint64 handle_page(uint64 va, struct proc *p) {
if (va < (1L << 37) || va >= (1L << 57)) return -1;
for (int i = 0; i < 16; i++)
if (p->vmas[i].valid && va >= p->vmas[i].addr && va < p->vmas[i].addr + p->vmas[i].length) {
int perm = PTE_U;
if (p->vmas[i].prot & PROT_READ) perm |= PTE_R;
if (p->vmas[i].prot & PROT_WRITE) perm |= PTE_W;
uint64 base = PGROUNDDOWN(va);
char *pa = kalloc(); if (pa == 0) return -1; memset(pa, 0, PGSIZE);
mappages(p->pagetable, base, PGSIZE, (uint64)pa, perm);
begin_op(); ilock(p->vmas[i].fd->ip);
readi(p->vmas[i].fd->ip, 1, base, base - p->vmas[i].oaddr, PGSIZE);
iunlock(p->vmas[i].fd->ip); end_op();
return 0;
}
return -1;
}
地址合法性判断就直接判断当前地址是不是在128G到129G之间不是则返回。接着就是枚举有效的vma,看看当前缺的页落在哪段被映射的地址段内,找到之后就是页表映射,一贯地,页表映射还是需要注意对齐问题,需要对地址向下对齐页宽。另外要注意的是申请来的物理空间一定要初始化为0,因为可能当前在文件中的偏移量已经超过了文件大小,这时会读不出东西,需要自行给读不出东西的位置填充0。然后就是读文件,因为当前inode指针被文件描述符长期持有,而文件描述符又被vma长期持有,所以不用iput。
再接着是sys_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).
即munmap一次只会unmap一段的一部分,同时这一部分要么是这一段的开始到中间,要么是中间到结束,不会是中间到中间挖个洞。这为我们大大降低了工作量。
uint64 sys_munmap(void) {
uint64 addr; int length;
if (argaddr(0, &addr) < 0 || argint(1, &length) < 0) return -1;
if (addr % PGSIZE != 0 || length % PGSIZE != 0) return -1;
struct proc *p = myproc();
for (int i = 0; i < 16; i++) {
if (p->vmas[i].valid
&& (addr + length == p->vmas[i].addr + p->vmas[i].length)
|| addr == p->vmas[i].addr)) {
for (int j = 0; j < length; j += PGSIZE) {
if (walkaddr(p->pagetable, addr + j) == 0) continue;
if (p->vmas[i].flags == MAP_SHARED) {
begin_op(); ilock(p->vmas[i].fd->ip);
writei(p->vmas[i].fd->ip, 1, addr + j, addr + j - p->vmas[i].oaddr, PGSIZE);
iunlock(p->vmas[i].fd->ip); end_op();
}
uvmunmap(p->pagetable, j + addr, 1, 1);
}
if (addr + length == p->vmas[i].addr + p->vmas[i].length)
p->vmas[i].length = addr - p->vmas[i].addr;
else {
p->vmas[i].addr = addr + length;
p->vmas[i].length -= length;
}
if (p->vmas[i].length == 0) {
fileclose(p->vmas[i].fd); p->vmas[i].valid = 0;
}
return 0;
}
}
return 0;
}
首先找到有效的vma,这里的区间覆盖只有两种情况,很好判断,然后是逐页写文件及uvmunmap,如果当前页还没被访问过(没有被handle_page分配物理内存)就跳过,接着更新该段的addr和length,如果该段完全被解映射完了(length==0),就把文件描述符关闭并把vma标记为无效(未使用)。
这次实验的大头完成了,但是和Lazy Allocation实验一样,为了处理缺页对进程相关的代码还需要做一些额外的修改,首先是fork函数,需要把vma和1L << 37
以上的内存复制到子进程:
if(mmapcopy(p->pagetable, np->pagetable, p->mmapsz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->mmapsz = p->mmapsz;
for (int i = 0; i < 16; i++) if (p->vmas[i].valid) {
np->vmas[i] = p->vmas[i]; filedup(np->vmas[i].fd);
}
vm.c新定义了一个mmapcopy函数,内容和uvmcopy差不多,但是修改了遍历范围并把缺页的panic跳掉了:
int mmapcopy(pagetable_t old, pagetable_t new, uint64 sz) {
pte_t *pte;
uint64 pa, i;
uint flags;
char *mem;
for(i = 1L << 37; i < (1L << 37) + sz; i += PGSIZE) {
if((pte = walk(old, i, 0)) == 0) continue;
if((*pte & PTE_V) == 0) 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, i, (i - (1L << 37)) >> PGSHIFT, 1);
return -1;
}
回到proc.c,然后是释放进程需要做的处理:
p->mmapsz = 0;
for (int i = 0; i < 16; i++) p->vmas[i].valid = 0;
这时运行会发现freewalk函数panic:freewalk: leaf
,这是因为freewalk希望所有虚拟地址已经被解绑并释放对应的物理空间了,该函数只负责释放页表。而调用它的uvmfree只解绑了进程中0到p->sz的空间,mmap所用的1L << 37
上面的空间没被解绑,所以我们需要手动解绑,修改proc_freepagetable函数:
void
proc_freepagetable(pagetable_t pagetable, uint64 sz, uint64 mmapsz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmunmap(pagetable, 1L << 37, mmapsz >> PGSHIFT, 1);
uvmfree(pagetable, sz);
}
注意到这里proc_freepagetable的参数多了一个,需要在defs.h里修改函数头部,同时调用这个函数的其他地方也需要修改,包括proc.c和exec.c里的几行代码,这里就不说了。另外和Lazy Allocation实验一样,需要把uvmunmap里关于缺页的panic跳掉,这里也从略。
总结一下,这个实验还是有点麻烦的,但因为只是作为单次实验,实验文档和测试程序的要求也不高,实际上这个实验拓宽来搞,可以做为课设级别的大实验,比如消除mmap和munmap参数是页宽的限制,这样物理内存的分配和释放都需要修改成能处理任意大小空间的形式,那么就得设计链表+淘汰算法或伙伴算法来维护空闲空间;比如地址段超过16个,就得设计内核级的动态分配数组的方式,还是依赖物理内存调度的支持;比如更大量的mmap空间支持,就需要重新考虑如何分配地址空间,可能还需要用到某些数据结构;比如完整的mmap参数支持,代码量就更大了,甚至可以发展成完整的硬盘虚拟内存技术……