实现原理
mmap
这次 lab 是要给 xv6 增加 mmap 和 munmap 体系调用。
mmap 的优点在于能够将一个文件直接映射到进程的地址空间中,从而避免了不必要的数据仿制,进步了文件操作的功率。与运用 read 和 write 体系调用不同,mmap 操作不需要将文件数据从内核缓冲区仿制到用户缓冲区,也不需要将用户缓冲区中的数据仿制回内核缓冲区。相反,它通过映射文件的方法,将文件数据直接映射到了进程的地址空间中,因此能够进步文件操作的功率。
同时 mmap 也避免了因为运用 read 和 write 体系调用而造成的在用户空间和内核空间的上下文切换,节省了体系调用的开销。
体系调用声明
mmap 体系调用的函数声明为:
void *mmap(void *addr, uint64 len, int prot, int flags, int fd, uint64 offset);
- addr 为文件在用户地址空间的开端地址,一般传入 0,由内核设置;
- len 为要映射的字节数量;
- prot 为权限字段,指明该文件是可读(PROT_READ)、可写(PROT_WRITE)或可执行(PROT_EXEC)的;
- flags 为符号位,符号映射的形式,MAP_SHARED 形式标识在 munmap 的时分需要把改动写回磁盘,MAP_PRIVATE 形式则不需要;
- fd 是文件的描述符;
- offset 为文件开端方位到开端映射的方位的偏移量。
munmap 体系调用的函数声明为:
int munmap(void *addr, uint64 len);
- addr 为从哪里开端免除映射;
- len 为免除映射的字节数。
代码实现
增加 mmap 和 munmap 体系调用的进程这儿就省掉了。直接来看实现。
首要,为了能够让用户进程知道关于文件映射的信息,需要在 proc 结构体记载下。新增 vma 结构体,来存储文件映射的相关信息:
struct vma {
int valid; // 该 vma 是否有用
uint64 addr; // 文件在进程地址空间中的开端地址
uint64 len; // 文件映射了多少字节
int prot; // 文件权限
int flags; // 映射形式标识
int fd; // 文件标识符
struct file *file; // 指向对应的文件结构体
uint64 offset; // 文件映射的偏移
};
并且在 proc 结构体中增加一个 vma 数组,依据 hint,大小为 16 即可:
struct vma vmatable[NVMA]; // NVMA 为定义在 kernel/param.h 中的宏
接着在 kernel/sysfile.c 中实现 sys_mmap 函数。大致流程如下:
- 接纳 mmap 体系调用传递的参数;
- 判别参数是否能够满意映射条件:
-
- 只读文件在 MAP_PRIVATE 形式下,是可写的;
- 只读文件在 MAP_SHARED 形式下,是不可写的。
- 从进程中记载的 vma 中找出一个空闲的 vma,并在进程的 heap 中找出一段可用的内存,将这段内存的开端地址作为体系调用的返回值。留意在这儿是不进行内存分配的,仅仅符号,跟 lazy alloction 是一样的,这样能够让映射比内存空间更大的文件成为或许。为了和进程正在运用的地址空间区分隔,挑选从 heap 的高方位开端向下扩展来映射文件,即从 TRAPFRAME 开端。
- 设置 vma 的值;
- filedup 对应文件;
mmap should increase the file’s reference count so that the structure doesn’t disappear when the file is closed.
close 体系调用封闭是的一个翻开的文件描述符,仅仅减少该文件的翻开引用数,在这儿增加一次引用后,就算调用了 close 也不会影响到对已经映射的内存。
- 返回映射的开端地址;
实现如下:
uint64
sys_mmap(void) {
uint64 len, offset;
int prot, flags, fd;
if(argaddr(1, &len) < 0 || argint(2, &prot) < 0 || argint(3, &flags) < 0 || argint(4, &fd) < 0 || argaddr(5, &offset) < 0) {
return -1;
}
struct proc *p = myproc();
struct file *file = p->ofile[fd];
if ((file->readable && !file->writable) && (prot & PROT_WRITE) && (flags & MAP_SHARED)) {
return -1;
}
struct vma *vma = 0;
int found = 0;
uint64 addr = TRAPFRAME;
for(int i = 0; i < 16; i++) {
if(!p->vmatable[i].valid && !found) {
found = 1;
vma = &p->vmatable[i];
} else if (p->vmatable[i].valid && p->vmatable[i].addr < addr) {
addr = p->vmatable[i].addr;
}
}
if (!found) {
return -1;
}
addr = addr - len;
vma->valid = 1;
vma->fd = fd;
vma->file = file;
vma->len = len;
vma->offset = offset;
vma->prot = prot;
vma->flags = flags;
vma->addr = addr;
filedup(vma->file);
return addr;
}
完结这一步后,在用户程序中调用 mmap 就会返回一个正确的映射后的开端地址了,可是当进行拜访的时分,因为并没有分配内存,就会触发 page fault,所以跟 lazy alloction 一样,在 kernel/trap.c#usertrap 中处理 page fault。
// ...
} else if((which_dev = devintr()) != 0){
// ok
} else if(r_scause() == 13 || r_scause() == 15) {
uint64 va = r_stval();
if (mmaphandler(va) == -1) {
p->killed = 1;
}
} else {
// ...
kernel/vm.c#mmaphandler 函数接纳一个虚拟内存地址(产生 page fault 的地址),来处理 pagefault。
在 mmaphandler 中,咱们需要做以下事情:
- 找出 va 是映射在哪个页中,也便是需要找出对应的 vma;
- 给 vma 正式分配内存;
- 依据 vma 中记载的 prot 来设置 PTE 的 flags;
- 将物理地址和虚拟地址进行映射;
- 运用 readi 将文件读到刚分配的内存中。在进行操作的时分要开启事务,并且对 inode 上锁。
int
mmaphandler(uint64 va) {
int i;
struct proc *p = myproc();
struct vma *vma = 0;
struct inode *ip;
for(i = 0; i < NVMA; i++) {
struct vma *v = &p->vmatable[i];
if (v->valid) {
if (va >= v->addr && va < (v->addr + v->len * PGSIZE)) {
vma = v;
break;
}
}
}
if (vma == 0) {
return -1;
}
uint64 ka = (uint64)kalloc();
if (ka == 0) {
return -1;
}
memset((void *) ka, 0, PGSIZE);
va = PGROUNDDOWN(va);
pte_t * pte;
// avoid remap panic.
if ((pte = walk(p->pagetable, va, 0)) != 0 && (*pte & PTE_V) != 0) {
kfree((void *) ka);
return -1;
}
int flags = PTE_FLAGS(*pte);
if (vma->prot & PROT_READ) {
flags |= PTE_R;
}
if (vma->prot & PROT_WRITE) {
flags |= PTE_W;
}
if (vma->prot & PROT_EXEC) {
flags |= PTE_X;
}
if(mappages(p->pagetable, va, PGSIZE, ka, flags | PTE_U) != 0) {
kfree((void *) ka);
return -1;
}
ip = vma->file->ip;
begin_op();
ilock(ip);
if (readi(ip, 0, ka, PGROUNDDOWN(vma->offset + (va - vma->addr)), PGSIZE) < 0) {
return -1;
}
iunlock(ip);
end_op();
return 0;
}
到这儿就能够拜访咱们映射到内存中的文件了。
接下来要实现 munmap 体系调用(kernel/sysfile.c#sys_munmap),留意依据文档,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).
在 sys_munmap 函数中咱们要处理以下事情:
- 接纳 addr 和 len 参数;
- 找出 addr 对应的 vma;
- 判别 vma 是否是 MAP_SHARED 形式,如果是就调用 filewrite 将文件写回磁盘;
- 取消 munmap 部分的映射;
- 调整 vma 的长度和开端地址。
uint64
sys_munmap(void) {
uint64 addr, len;
if(argaddr(0, &addr) < 0 || argaddr(1, &len) < 0) {
return -1;
}
int i;
struct vma *vma = 0;
struct proc *p = myproc();
for (i = 0; i < NVMA; i++) {
struct vma *v = &p->vmatable[i];
if (v->valid && (v->addr <= addr && addr < (v->addr + len))) {
vma = v;
}
}
if (!vma) {
return -1;
}
if (vma->flags & MAP_SHARED && vma->file->writable) {
filewrite(vma->file, addr, len);
}
uvmunmap(p->pagetable, addr, len / PGSIZE, 1);
vma->len -= len;
if(vma->len == 0) vma->valid = 0;
else {
if (vma->addr == addr) vma->addr += len;
}
return 0;
}
留意修正 uvmunmap,否则会报 panic。
当进程退出的时分,即调用 kernel/proc.c#exit,咱们需要将它映射的所有文件都 munmap 掉,就像调用 munmap 体系调用。因为我的实现是父子进程并不同享物理内存,所以直接释放掉即可。
// ... kernel/proc.c#exit
int i;
for(i = 0; i < NVMA; i++) {
struct vma *v = &p->vmatable[i];
if (v->valid) {
if (v->flags & MAP_SHARED && v->file->writable) {
filewrite(v->file, v->addr, v->len);
}
uvmunmap(p->pagetable, v->addr, v->len/PGSIZE, 1);
v->valid = 0;
}
}
最终修正 kernel/proc.c#fork,在子进程仿制父进程的内存时,或许会仿制到没有映射或无效的条目,也要修正 uvmcopy 将 panic 去掉。在 fork 函数中只需要将 vma 仿制一份给子进程就能够了。
// ...kernel/proc.c#fork
for(i = 0; i < NVMA; i++) {
np->vmatable[i] = p->vmatable[i];
}
到这儿 mmaptest 和 fork test 就都能够通过了。
运行成果
这次的 grader 倒是顺利跑过了。
总结
这个 lab 是对 file system 的进一步深化,不过我感觉跟虚拟内存或许愈加相关?难点主要是在 mmap 体系调用,要考虑怎么给 vma 找到一块适宜的内存空间,想清楚这儿之后其它的就比较简单了。page fault 的处理跟 lazy alloction 是一样的。munmap 体系调用就相当于做了一次反操作。