时间及结果
总耗时: 6h27min
解析
系统调用原型
本次lab要求实现mmap和munmap系统调用,系统调用原型如下:
// xv6中没有定义size_t和off_t,故修改为uint64
void *mmap(void *addr, uint64 length, int prot, int flags,
int fd, uint64 offset);
int munmap(void *addr, uint64 length);
// addr: 指定的虚拟地址,此lab中默认为0,即由内核决定映射的虚拟地址;
// length: 映射的字节数;
// prot: 可选 PROT_READ PROT_WRITE,用以表明映射部分的读写权限;
// flags: MAP_SHARED表示映射内存的修改要写回文件,MAP_PRIVATE则无需写回;
// fd: 要映射的文件;
// offset: 文件的偏移量
// mmap系统调用将文件fd从offset开始的length字节映射到虚拟地址addr处
// munmap系统调用取消虚拟地址addr开始的length字节的文件映射
思路
首先需要定义VMA数据结构来保存一段文件映射的相关信息:
// kernel/proc.h
#define MAXMMAP 16
struct VMA{
uint64 addr;
uint64 length;
int prot;
int flags;
struct file *f;
uint64 offset;
int used; // 1 说明该VMA在使用中;0 说明空闲
struct spinlock lock;
};
// 在struct proc中新增VMA数组,支持MAXMMAP个元素
// struct VMA proc_VMA[MAXMMAP];
该数据结构的初始化时机和释放时机与进程proc结构的其它项相同:
// kernel/proc.c: allocproc()
for(int i = 0; i < MAXMMAP; i++) {
p->proc_VMA[i].addr = 0;
p->proc_VMA[i].f = 0;
p->proc_VMA[i].flags = 0;
p->proc_VMA[i].length = 0;
p->proc_VMA[i].offset = 0;
p->proc_VMA[i].prot = 0;
p->proc_VMA[i].used = 0;
}
// kernel/proc.c: freeproc()
for (int i = 0; i < MAXMMAP; i++) {
p->proc_VMA[i].addr = 0;
p->proc_VMA[i].f = 0;
p->proc_VMA[i].flags = 0;
p->proc_VMA[i].length = 0;
p->proc_VMA[i].offset = 0;
p->proc_VMA[i].prot = 0;
p->proc_VMA[i].used = 0;
}
添加系统调用的过程可以参照前面的lab,此处只记录系统调用的实现。
对于mmap系统调用,可以参考lazy allocation中的sbrk系统调用,仅增加myproc()->sz,不实际分配物理内存,而是在缺页故障的处理中分配物理页。
此处实现的mmap系统调用是直接从堆区获取虚拟内存来存放文件映射部分,而没有像linux那样单独划给文件映射区一片内存。
// kernel/sysfile.c
uint64 sys_mmap(void) {
uint64 addr, length, offset;
int prot, flags, fd, i;
struct proc *p = myproc();
struct file *f;
// 获取系统调用参数
if(argaddr(0, &addr) < 0 || argaddr(1, &length) < 0 || argaddr(5, &offset) < 0) {
return -1;
}
if(argint(2, &prot) < 0 || argint(3, &flags) < 0 || argint(4, &fd) < 0) {
return -1;
}
if(addr != 0) {
panic("sys_mmap: addr just can be 0 in my mmap");
}
acquire(&p->lock);
// 如果flags指定映射区的修改要写回文件
// 那么该文件的读写权限和prot参数指定的读写权限必须相容
if(flags & MAP_SHARED) {
if(p->ofile[fd]->readable == 0 && (prot & PROT_READ)) {
release(&p->lock);
return -1;
}
if(p->ofile[fd]->writable == 0 && (prot & PROT_WRITE)) {
release(&p->lock);
return -1;
}
}
// 仅增加p->sz,等缺页故障时再实际分配物理页
addr = p->sz;
p->sz += length;
f = filedup(p->ofile[fd]); // 注意要增加文件的引用
// 找到一个空闲VMA用来存放文件映射的相关信息
for(i = 0; i < MAXMMAP; i++) {
if(p->proc_VMA[i].used == 0) {
p->proc_VMA[i].addr = addr;
p->proc_VMA[i].f = f;
p->proc_VMA[i].flags = flags;
p->proc_VMA[i].length = length;
p->proc_VMA[i].offset = offset;
p->proc_VMA[i].prot = prot;
p->proc_VMA[i].used = 1;
break;
}
}
if(i == MAXMMAP) {
panic("no free VMA for mmap");
}
release(&p->lock);
return addr;
}
接下来需要考虑在缺页故障中为文件映射实际分配物理内存,定义isMmap()函数用来判断一个地址是否是文件映射的页,定义handleMmap()函数为一个地址实际分配物理页并写入相关文件数据
// kernel/trap.c
/**
* @description: 判断传入的地址是否是文件映射的地址
* @param {uint64} addr 虚拟地址
* @return {*} 返回VMA编号,可以从myproc()->proc_VMA中获取相应VMA
* 返回-1说明该地址不是文件映射的地址
*/
int isMmap(uint64 addr) {
struct proc *p = myproc();
uint64 begin, end;
// 遍历当前进程的每个使用中的VMA,找到包含地址addr的那个
for(int i = 0; i < MAXMMAP; i++) {
if(p->proc_VMA[i].used) {
begin = p->proc_VMA[i].addr;
end = p->proc_VMA[i].addr + p->proc_VMA[i].length;
if(p->proc_VMA[i].used == 1 && (addr >= begin && addr < end)) {
return i;
}
}
}
return -1;
}
/**
* @description: 为文件映射的一页分配物理页
* @param {uint64} addr 文件映射的一个虚拟地址
* @param {int} index 这个文件映射对应的VMA编号,是isMmap的返回值
* @return {*}
*/
void handleMmap(uint64 addr, int index) {
struct proc *p = myproc();
uint64 pa = (uint64)kalloc();
int perm = 0, prot, off;
struct inode *ip;
if(pa == 0) { // 物理页不够
panic("handleMmap: no pa");
} else {
memset((void*)pa, 0, PGSIZE);
// 将文件数据读入物理页
ip = p->proc_VMA[index].f->ip;
off = p->proc_VMA[index].offset + (PGROUNDDOWN(addr) - p->proc_VMA[index].addr);
ilock(ip);
begin_op();
readi(ip, 0, pa, off, PGSIZE);
end_op();
iunlock(ip);
// 设置PTE权限
prot = p->proc_VMA[index].prot;
if(prot & PROT_READ) {
perm |= PTE_R;
}
if(prot & PROT_WRITE) {
perm |= PTE_W;
}
if(prot & PROT_EXEC) {
perm |= PTE_X;
}
perm |= PTE_U;
// 设置虚拟地址和物理地址的映射
if((mappages(p->pagetable, PGROUNDDOWN(addr), PGSIZE, pa, perm)) < 0) {
panic("handleMmap: mappages f");
}
}
}
像lazy allocation那样,在usertrap()中处理缺页故障;并且在uvmunmap()和uvmcopy()中跳过处理未定义或未分配的页表项,在walkaddr中为文件映射分配物理页
// kernel/trap.c: usertrap()
else if(scause == 13 || scause == 15) {
int index;
// 是mmap则分配物理页
if((index = isMmap(r_stval())) >= 0) {
handleMmap(r_stval(), index);
} else { // 不是mmap则说明访问了未使用的页,应该杀死进程
printf("is no mmap\n");
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;
}
}
// kernel/vm.c: uvmunmap()
if((pte = walk(pagetable, a, 0)) == 0)
continue;
// panic("uvmunmap: walk");
if((*pte & PTE_V) == 0)
continue;
// panic("uvmunmap: not mapped");
// kernel/vm.c: uvmcopy()
if((pte = walk(old, i, 0)) == 0)
continue;
// panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
continue;
// panic("uvmcopy: page not present");
// kernel/vm.c: walkaddr()
if(pte == 0 || (*pte & PTE_V) == 0) {
int index;
if((index = isMmap(va)) >= 0) {
handleMmap(va, index);
} else {
return 0;
}
}
至此,mmap部分就完成了。
munmap系统调用取消一个文件映射的部分或全部,并将设置了MAP_SHARED的文件映射写回文件中
// kernel/sysfile.c
/**
* @description: 将从addr开始的length取消文件映射
* @param {uint64} addr 虚拟地址
* @param {uint64} length 字节数
* @return {*}
*/
uint64 munmap(uint64 addr, uint64 length) {
struct proc *p = myproc();
int i;
uint64 begin = 0, end = 0;
// 查找包含地址addr的文件映射所对应的VMA
for(i = 0; i < MAXMMAP; i++) {
if(p->proc_VMA[i].used) {
begin = p->proc_VMA[i].addr;
end = p->proc_VMA[i].addr + p->proc_VMA[i].length;
if(addr >= begin && addr < end) {
break;
}
}
}
if (addr < begin || addr >= end) {
panic("sys_munmap: addr not mmap");
}
// flags设置了MAP_SHARED的文件映射应该写回文件中
if(p->proc_VMA[i].flags & MAP_SHARED) {
for(uint64 index = PGROUNDDOWN(addr); index != PGROUNDUP(addr + length); index += PGSIZE) {
pte_t *pte = walk(p->pagetable, index, 0);
// 未分配物理页的文件映射应该跳过
if(pte == 0 || (*pte & PTE_V) == 0) {
continue;
}
begin_op();
if(writei(p->proc_VMA[i].f->ip, 1, index, p->proc_VMA[i].offset + (index - addr), PGSIZE) < 0) {
panic("sys_munmap: write");
}
end_op();
}
}
// 在页表上取消对应映射区域的PTE
uvmunmap(p->pagetable, PGROUNDDOWN(addr), (PGROUNDUP(addr + length) - PGROUNDDOWN(addr)) / PGSIZE, 1);
// 进行munmap的地址从该文件映射的开头开始
if(addr == begin) {
if(addr + length == end) { // 说明munmap的地址范围覆盖了该文件映射的全部,应该释放相应VMA,并减少文件引用
p->proc_VMA[i].used = 0;
struct file *f = p->proc_VMA[i].f;
fileclose(f);
return 0;
} else { // 说明munmap的地址范围只有该文件映射的前半部分
p->proc_VMA[i].addr = addr + length;
p->proc_VMA[i].length -= length;
p->proc_VMA[i].offset += length;
}
} else { // 进行munmap的地址从该文件映射的中间开始,一直到结尾(题目提示此时munmap的地址范围一定会在该文件映射的最后结束)
p->proc_VMA[i].length -= length;
}
return 0;
}
uint64 sys_munmap(void) {
uint64 addr;
uint64 length;
if(argaddr(0, &addr) < 0 || argaddr(1, &length) < 0) {
return -1;
}
return munmap(addr, length);
}
至此,munmap系统调用也实现了。
还需要再处理一下exit()和fork()。
exit()函数中需要对该进程的全部文件映射进行munmap
// kernel/proc.c: exit()
for(int i = 0; i < MAXMMAP; i++) {
if(p->proc_VMA[i].used) {
// 偷懒直接调用了sys_munmap用的munmap函数
munmap(p->proc_VMA[i].addr, p->proc_VMA[i].length);
}
}
fork函数应该让子进程继承父进程的VMA和相应的文件映射,注意文件引用要加1。
文件映射的PTE会在uvmcopy()中继承。
for(int i = 0; i < MAXMMAP; i++) {
np->proc_VMA[i] = p->proc_VMA[i];
if(np->proc_VMA[i].f) filedup(np->proc_VMA[i].f);
}
至此,即可通过mmaptest和usertests。