Lab9:mmap
参考文章:
MIT 6.S081 Lab10: mmap
认真分析mmap
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
They can be used to share memory among processes, to map files into process address spaces.
【void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)】
addr:这个lab里可以认定 addr=0,即内核自动决定文件映射到哪个虚拟地址(mmap 返回这个地址,或者错误值)
length:要映射的文件长度(可能和文件总长度不同)
prot:表示这个映射的权限是否是可读/可写或可执行的(you can assume that prot is PROT_READ or PROT_WRITE or both)
flags:MAP_SHARED表示对映射内容的修改要写回文件;MAP_PRIVATE表示不写回
fd:要映射的文件的打开文件描述符
offset:和文件起始的相对位置(You can assume offset is zero:it’s the starting point in the file at which to map)
(It’s OK if processes that map the same MAP_SHARED file do not share physical pages.)
【munmap(addr, length) 在指定地址上去除文件内存映射】
如果内存修改了MAP_SHARED映射的内容,应该写回文件
(munmap可能只覆盖一部分mmap映射的区域,但是在这个lab里可以假设,只在开头,或者结尾,或者整个区域做unmap)
Hints:
- 添加系统调用mmap和munmap
- mmap不需要分配物理页,利用缺页异常的lazy机制,需要时在usertrap中分配即可(原因是mmap一个大文件变快一些,并且mmap一个比物理内存更大的文件是可能的)
- 定义固定数量的VMA (virtual memory area) 在进程中保存mmap信息
- mmap: 找到一块用户进程空间的空闲内存来映射文件,加入一个VMA来记录此信息(需要有一个指针指向被映射的struct file;需要增加文件的引用数ref)
- 加入代码使得mmap区域引入一个缺页异常,分配物理内存,读入4096字节到对应物理页,并且map到对应的用户地址空间的虚拟地址。用readi来读入文件(注意offset,以及对传入readi的inode进行lock/unlock),注意设置页的权限位。
- munmap:找到addr区域对应的VMA,并且unmap对应的页(提示:用uvunmap函数)如果munmap移除了一个mmap映射的全部页,应该减少对struct file的ref;如果一个unmapped page被修改过并且是MAP_SHARED,应该将这个页写回到文件。
Ideally your implementation would only write back MAP_SHARED pages that the program actually modified. The dirty bit (D) in the RISC-V PTE indicates whether a page has been written. However, mmaptest does not check that non-dirty pages are not written back; thus you can get away with writing pages back without looking at D bits.(由于这个lab不检查未修改过的页是否写入,所以只要是MAP_SHARED就写回文件也是可行的,不用单独看D位来判断)
- Modify exit to unmap the process’s mapped regions as if munmap had been called.
- Modify fork to ensure that the child has the same mapped regions as the parent.
Don’t forget to increment the reference count for a VMA’s struct file.
准备工作:
添加系统调用(见参考文章1)
主要工作:
kernel/proc.h 中添加VMA结构体数组(见参考文章1)
sys_mmap():
接收传来的参数,判断参数的合法性;
然后遍历 VMA 数组,找到还没有使用的 vma,将参数信息添加进去。
这里映射的虚拟地址,可以直接填写堆的最高地址,然后让堆继续生长。
// mmap函数: 将文件地址映射到虚拟内存中,返回映射后的地址,同时记录该进程映射到的文件信息。
uint64
sys_mmap(void)
{
uint64 addr;
uint64 mmap_error = 0xffffffffffffffff;
int length, prot, flags, fd, offset;
struct file *file;
// 读入参数:
if(argaddr(0, &addr) || argint(1, &length) || argint(2, &prot) ||
argint(3, &flags) || argfd(4, &fd, &file) || argint(5, &offset)) {
return mmap_error;
}
// error:
// MAP_SHARED(meaning that modifications to the mapped memory should be written back to the file)
// PROT_WRITE(meaning writable)
// 但同时文件权限file->writeable = 0,不可写,出现权限错误,返回-1
if(!file->writable && (prot & PROT_WRITE) && flags == MAP_SHARED)
return mmap_error;
length = PGROUNDUP(length); // 页对齐(向上补齐一页长度)
struct proc *p = myproc();
// error: 进程内存剩余不足
if(p->sz > MAXVA - length)
return mmap_error;
for(int i = 0; i < NVMA; i++) {
// 找到一个未用过的vm_area进行文件映射:
if(!p->vma[i].used) {
p->vma[i].used = 1;
p->vma[i].addr = p->sz; // 直接映射到p->sz虚拟地址
p->vma[i].length = length;
p->vma[i].prot = prot;
p->vma[i].flags = flags;
p->vma[i].fd = fd;
p->vma[i].offset = offset;
p->vma[i].file = file;
filedup(file); // Increment ref count for file
p->sz += length;
return p->vma[i].addr; // 返回文件映射到的虚拟地址(原p->sz)
}
}
panic("not found vm_area");
return mmap_error;
}
usertrap():
在读取或写入相应的虚拟地址时,会存在地址未映射的情况。这时需要将物理地址上的数据读到虚拟地址中,然后重新进行读取或写入操作。
//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void)
{
...
} else if((which_dev = devintr()) != 0){
// ok
} else if(r_scause() == 13 || r_scause() == 15) {
// 【缺页异常】
uint64 va = r_stval();// 出现异常的虚拟地址
// 判断地址是否合法
if(va >= p->sz || va > MAXVA)
p->killed = 1;
else{
struct VMA *vma = 0;
for (int i = 0; i < NVMA; i++) {
// 判断地址是否在某个文件映射的虚拟地址范围内
if (p->vma[i].used == 1 && va >= p->vma[i].addr && va < p->vma[i].addr + p->vma[i].length) {
vma = &p->vma[i];
}
}
if(vma){
// 找到该文件,读取磁盘到物理内存,并将物理地址映射到引发异常的虚拟地址:
va = PGROUNDDOWN(va);
uint64 offset = va - vma->addr;
uint64 mem; // 分配物理页
if((mem = (uint64)kalloc()) == 0)
p->killed = 1;
else{
memset((void*)mem, 0, PGSIZE);
ilock(vma->file->ip);
// [Read data from inode]
readi(vma->file->ip, 0, mem, offset, PGSIZE); // 由inode读磁盘信息到物理内存mem
iunlock(vma->file->ip);
// 设置PTE页表项权限位:
int flag = PTE_U;
if(vma->prot & PROT_READ) flag |= PTE_R;
if(vma->prot & PROT_WRITE) flag |= PTE_W;
if(vma->prot & PROT_EXEC) flag |= PTE_X;
// 【最后建立页表项,将读出的磁盘文件所在的物理地址映射到虚拟地址】:
if(mappages(p->pagetable, va, PGSIZE, mem, flag) != 0) {
kfree((void*)mem);
p->killed = 1;
}
}
}
}
}
else {
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;
}
......
}
此外,还需要注意,由于一些地址并没有进行映射,因此在 walk 的时候,遇到这些地址直接跳过即可:kernel/vm.c
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
...
if((*pte & PTE_V) == 0)
continue;
//panic("uvmunmap: not mapped");
...
}
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
...
if((*pte & PTE_V) == 0)
//panic("uvmcopy: page not present");
continue;
...
munmap():
这部分的逻辑比较简单,主要是取消虚拟地址的映射关系;设置进程 VMA 结构体相应的 vma 为未使用状态。
这部分函数,实验中做了简化,只需要取消地址与传入地址相同的文件的映射即可。
// munmap函数: 取消进程的虚拟地址空间中,文件地址的部分映射。
uint64
sys_munmap(void)
{
uint64 addr;
int length;
if(argaddr(0, &addr) || argint(1, &length))
return -1;
// addr = PGROUNDDOWN(addr);
// length = PGROUNDUP(length);
struct VMA *vma = 0;
struct proc *p = myproc();
for(int i = 0; i < NVMA; i++) {
if (addr == p->vma[i].addr){ // lab中的条件容易处理:vma数据结构中的addr和length自然是页对齐的,只会出现相等的情况。
vma = &p->vma[i];
break;
}
}
if(!vma) // 没有找到对应的文件映射
return 0;
vma->addr += length;
vma->length -= length;
if(vma->flags & MAP_SHARED) // MAP_SHARED必须将修改内容写入文件
filewrite(vma->file, addr, length);
// Write to file f.
// addr is a user virtual address.
uvmunmap(p->pagetable, addr, length/PGSIZE, 1); // 修改:对于没有映射的页不报错,直接continue
if(vma->length == 0) { // 当前映射长度为0,已经全部取消
fileclose(vma->file); // 关闭此进程对文件的映射,即当前文件refcount--
// Close file f. (Decrement ref count, close when reaches 0.)
vma->used = 0; // vma数据结构设置为可用
}
return 0;
}
最后,需要处理一些附带的问题: fork 和 exit 函数。
在进程创建和退出时,需要复制和清空相应的文件映射:
kernel/proc.c
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
...
acquire(&np->lock);
// mmap: fork时要复制文件内存映射信息
for(int i = 0; i < NVMA; i++) {
if(p->vma[i].used){
memmove(&(np->vma[i]), &(p->vma[i]), sizeof(p->vma[i]));
filedup(p->vma[i].file); // refcount++
}
}
np->state = RUNNABLE;
release(&np->lock);
...
return pid;
}
// Exit the current process. Does not return.
// An exited process remains in the zombie state
// until its parent calls wait().
void
exit(int status)
{
struct proc *p = myproc();
...
// mmap: exit时清空进程的文件内存映射信息
for(int i = 0; i < NVMA; i++) {
if(p->vma[i].used) { // 占用状态下的vma,全部清空
if(p->vma[i].flags & MAP_SHARED) // 该写回的全部写回磁盘文件
filewrite(p->vma[i].file, p->vma[i].addr, p->vma[i].length);
fileclose(p->vma[i].file); // refcount--
uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].length/PGSIZE, 1); // 取消虚拟内存映射
p->vma[i].used = 0; // 恢复可用状态
}
}
begin_op();
...
}