课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.html
Lab 地址:https://pdos.csail.mit.edu/6.S081/2020/labs/mmap.html
我的代码地址:https://github.com/Amroning/MIT6.S081/tree/mmap
xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf
相关翻译:https://xv6.dgs.zone/labs/requirements/lab10.html
参考博客:https://blog.miigon.net/posts/s081-lab10-mmap/
学习笔记记录,如有错误恳请各位大佬指正
Lab10: mmap
仿照Linux实现mmap功能,即将文件映射到进程地址空间,如果进程修改了这部分内存,并且内存标记为映射内存的修改应写回文件,那么释放映射前需要把修改写入源文件。这样与文件交互的时候就可以减少磁盘操作。该实验需要用到很多前面实验的知识点
完整题目要求请去顶部链接查看
mmap(hard)
您应该实现足够的
mmap
和munmap
功能,以使mmaptest
测试程序正常工作。如果mmaptest
不会用到某个mmap
的特性,则不需要实现该特性。
进程所使用的内存空间从低地址向高地址生长(sbrk
调用),范围是stack到trapframe。为了不和进程使用的内存空间冲突,将mmap使用的地址空间映射到trapframe下面的页,从上往下生长。先定义mmap最后一页的地址:
// kernel/memlayout.h
// MMAP 进程映射文件内存最后一个页(开区间)
#define MMAPEND TRAPFRAME
定义一个vma结构体,表示虚拟内存区域,用来记录mmap创建的虚拟内存地址的范围、长度、权限、文件等。再声明一个vma结构体的数组,当mmap映射操作时,就来这个数组获取vma虚拟内存区域:
// kernel/proc.h
// 定义一个虚拟内存区域结构体,用来记录mmap创建的虚拟内存地址的范围、长度、权限、文件等
struct vma {int valid; // 该虚拟内存区域是否已被映射uint64 vastart; // 该虚拟内存区域开始地址uint64 sz; // 该虚拟内存区域大小struct file* f; // 该虚拟内存区域映射的文件int prot; // 该虚拟内存区域权限int flags; // 标记映射内存的修改是否写回文件uint64 offset; // 映射文件的起点
};#define NVMA 16 // VMA数组大小// Per-process state
struct proc {struct spinlock lock;......struct vma vmas[NVMA]; // mmap虚拟内存映射地址数组
};
接下来实现mmap的系统调用。这个实验要做的事挺多,最后再添加系统调用的声明。参考Linux的mmap函数,需要在进程的vmas数组中遍历寻找空闲的vma,遍历的过程中也要计算当前正在使用的所有vma的最低地址,这是为了后面添加新的vma。找到空闲的vma后,设置他的地址为刚才找到的最低地址减去sz(因为mmap的地址是从高到低生长)。然后需要调用filedup
函数将映射文件的引用数+1
调用mmap函数的时候需要注意文件权限问题。如果文件不可读,vma映射为可读,则mmap失败;如果文件不可写,vma映射为可写,并且开启了回盘标志(MAP_SHARED),则mmap失败。据此写出mmap系统调用函数:
// kernel/sysfile.c
// mmap系统调用实现
uint sys_mmap(void) {uint64 addr, sz, offset;int prot, flag, fd;struct file* f;// 读取传入参数if (argaddr(0, &addr) < 0 || argaddr(1, &sz) < 0 || argint(2, &prot) < 0 || argint(3, &flag) < 0 || argfd(4, &fd, &f) < 0 || argaddr(5, &offset) < 0 || sz == 0)return -1;// 以下情况直接返回-1:if ((!f->readable && ((prot & (PROT_READ)))) // 源文件不可读,vma映射为可读|| (!f->writable && (prot & PROT_WRITE) && !(flag & MAP_PRIVATE))) // 源文件不可写 ,vam映射为可写并且设置了将修改写回源文件return -1;sz = PGROUNDUP(sz);struct proc* p = myproc();struct vma* v = 0;uint64 vaend = MMAPEND;// 遍历查询未被使用的vma,并且计算当前已使用的va的最低地址for (int i = 0;i < NVMA;++i) {struct vma* vv = &p->vmas[i];if (vv->valid == 0) { // 若找到了空闲vma就保存下来if (v == 0) {v = &p->vmas[i];v->valid = 1;}}else if (vv->vastart < vaend) {vaend = PGROUNDDOWN(vv->vastart);}}// 没找到空闲的vmaif (v == 0)panic("mmap: no free vma");// 设置vma属性v->vastart = vaend - sz;v->sz = sz;v->f = f;v->prot = prot;v->flags = flag;v->offset = offset;// 增加源文件引用数filedup(v->f);return v->vastart;
}
kernel/fcntl.h
中定义好了相关宏,编译器提示未定义标识符可以不用管:
// kernel/fcntl.h
#ifdef LAB_MMAP
#define PROT_NONE 0x0
#define PROT_READ 0x1
#define PROT_WRITE 0x2
#define PROT_EXEC 0x4#define MAP_SHARED 0x01
#define MAP_PRIVATE 0x02
#endif
映射功能使用写时复制机制实现,即只有在访问的时候才进行磁盘操作。具体原理参考Lab5:
// kernel/sysfile.c
// 通过虚拟地址寻到对应的vma
struct vma* findvma(struct proc* p, uint64 va) {for (int i = 0;i < NVMA;++i) {struct vma* vv = &p->vmas[i];// 如果va地址在某一个vma范围内,则返回这个vmaif (vv->valid == 1 && va >= vv->vastart && va < vv->vastart + vv->sz) {return vv;}}return 0;
}// 给虚拟地址分配物理页并建立映射
int vmaalloc(uint64 va) {struct proc* p = myproc();struct vma* v = findvma(p, va);if (v == 0)return 0;// 分配物理地址void* pa = kalloc();if (pa == 0)panic("vmaalloc:kalloc");memset(pa, 0, PGSIZE);// 从磁盘读取文件begin_op();ilock(v->f->ip);readi(v->f->ip, 0, (uint64)pa, v->offset + PGROUNDDOWN(va - v->vastart), PGSIZE);iunlock(v->f->ip);end_op();// 建立映射if (mappages(p->pagetable, va, PGSIZE, (uint64)pa, PTE_R | PTE_W | PTE_U) < 0)panic("vmaalloc: mappages");return 1;
}
// kernel/trap.c
void
usertrap(void)
{......else if ((which_dev = devintr()) != 0) {// ok}else if (r_scause() == 13 || r_scause() == 15){uint64 va = r_stval(); // 读取当前发生页面错误的地址if (vmaalloc(va) == 0)panic("usertrap: wrong va");}......
}
接下来需要实现另一个系统调用munmap
,释放所有的vma。如果设置了MAP_SHARED,还需要将修改写回磁盘源文件。
munmap
传入的参数为释放映射的地址addr,释放地址的范围大小sz。需要检测释放的区域,取消映射的位置要么在区域起始位置,要么在区域结束位置,要么就是整个区域,但是不能在vma中间“打洞”。页有可能不是完整释放,如果 addr 处于一个页的中间,则那个页的后半部分释放,但是前半部分不释放,此时该页整体不应该被释放:
// kernel/sysfile.c
// 释放vma映射的页
uint64 sys_munmap(void) {uint64 addr, sz;if (argaddr(0, &addr) < 0 || argaddr(1, &sz) < 0 || sz == 0)return -1;struct proc* p = myproc();struct vma* v = findvma(p, addr);if (v == 0)return -1;if (addr > v->vastart && addr + sz < v->vastart + v->sz) // 释放的区域不能在vma中“打洞”return -1;uint64 addr_alinged = addr;if (addr > v->vastart)addr_alinged = PGROUNDUP(addr);int nunmap = sz - (addr_alinged - addr); // 计算要释放的字节数if (nunmap < 0)nunmap = 0;vmaunmap(p->pagetable, addr_alinged, nunmap, v); // 从addr_alinged开始释放nunmap字节数if (addr <= v->vastart && addr + sz > v->vastart) {v->offset += addr + sz - v->vastart;v->vastart = addr + sz;}v->sz -= sz;if (v->sz <= 0) {fileclose(v->f);v->valid = 0;}return 0;
}
vmaunmap
函数实现释放映射功能。释放映射之后,需要更新对应vma的offset、vastart、sz字段。如果释放完了vma的sz大小的范围,则应该关闭文件的引用,释放该vma。
vmaunmap
函数仿照uvmunmap
函数实现, 从传入的参数虚拟地址va开始遍历,查找va + nbytes范围内的每一个页,检查这个页是否被修改过,并且该vma设置了回盘MAP_SHARED,则需要把修改写回磁盘。注意不是每一个页都需要完整的写回,这里需要处理开头页不完整、结尾页不完整以及中间完整页的情况
先加上PTE_D标志位,表示页表被修改过:
// kernel/riscv.h
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_G (1L << 5)
#define PTE_A (1L << 6)
#define PTE_D (1L << 7) // 页表被修改过
实现vmaunmap
函数:
// kernel/vm.c
// 添加必要的头文件
#include "fcntl.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "proc.h"// 释放mmap映射的页,根据PTE_D和MAP_SHARED判断是否将修改写回磁盘
void vmaunmap(pagetable_t pagetable, uint64 va, uint64 nbytes, struct vma* v) {uint64 a;pte_t* pte;for (a = va;a < va + nbytes;a += PGSIZE) {if ((pte = walk(pagetable, a, 0)) == 0) // 读取va对应ptecontinue;if (PTE_FLAGS(*pte) == PTE_V)panic("sys_munmap: not a leaf");if (*pte & PTE_V) {uint64 pa = PTE2PA(*pte);if ((*pte & PTE_D) && (v->flags & MAP_SHARED)) { // 将修改写回磁盘begin_op();ilock(v->f->ip);uint64 aoff = a - v->vastart; // 相对于vma的偏移量if (aoff < 0)writei(v->f->ip, 0, pa + (-aoff), v->offset, PGSIZE + aoff); // 第一页是不满PGSIZE的一个页else if (aoff + PGSIZE > v->sz)writei(v->f->ip, 0, pa, v->offset + aoff, v->sz - aoff); // 最后一页是不满PGSIZE的一个页elsewritei(v->f->ip, 0, pa, v->offset + aoff, PGSIZE);iunlock(v->f->ip);end_op();}kfree((void*)pa);*pte = 0;}}
}
在proc.c中需要添加对vma的处理
首先是初始化进程时,需要初始化一个进程的vmas数组:
// kernel/proc.c
static struct proc*
allocproc(void)
{......// 初始化时清空vmas数组for (int i = 0;i < NVMA;++i)p->vmas[i].valid = 0;return p;
}
释放进程时,要在释放页表前清空vmas数组:
// kernel/proc.c
static void
freeproc(struct proc *p)
{if(p->trapframe)kfree((void*)p->trapframe);p->trapframe = 0;for (int i = 0;i < NVMA;++i) { // 释放页表前把vmas数组清空struct vma* v = &p->vmas[i];vmaunmap(p->pagetable, v->vastart, v->sz, v);}if (p->pagetable)proc_freepagetable(p->pagetable, p->sz);......
}
fork创建子进程时,子进程复制父进程的vmas数组,不复制物理页:
// kernel/proc.c
int
fork(void)
{......// 父进程vmas复制到子进程中,实际内存页和pte不会被复制for (int i = 0;i < NVMA;++i) {struct vma* v = &p->vmas[i];if (v->valid) {np->vmas[i] = *v;filedup(v->f);}}safestrcpy(np->name, p->name, sizeof(p->name));pid = np->pid;np->state = RUNNABLE;release(&np->lock);return pid;
}
主体做好了,现在添加系统调用声明
user.h:
// mmaptest中调用mmap时需要返回char*,这里可以把返回值设置为void*
void* mmap(void* addr, uint sz, int prot, int flag, int fd, uint offset);
int munmap(void* addr, uint sz);
usys.pl:
entry("mmap");
entry("munmap");
syscall.h:
#define SYS_mmap 22
#define SYS_munmap 23
syscall.c:
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);static uint64 (*syscalls[])(void) = {
......
[SYS_mmap] sys_mmap,
[SYS_munmap] sys_munmap,
};
此时可以验证实验是否通过