Lab: mmap
mmap
hints
char* mmap(void *addr, int length, int prot, int flags, int fd, int offset);int munmap(void *addr, int length);- mmap() 映射的页面应该是 lazy alloc 的,以保证在映射大文件时不会阻塞
- 每个进程应保持对 mmap() 映射的记录。创建一个符合 VMA 要求的结构体来保存它。每个进程的结构体数组应为16。
- mmap() 的工作:在用户空间地址中寻找未映射的区域来映射文件,并用在进程信息中用结构体保存该映射的信息。该结构体应该保存指向被映射文件struct file 的指针,并且应该增加文件的引用次数
- 处理缺页中断。在缺页中断中读入 对应位置的4096字节 文件放入内存并映射
- munmap() 的工作:取消特定页面的映射,并将MAP_SHARED页面写回磁盘。如果一个映射被完全取消,记得减少相对应的文件引用
- 你可以假设 munmap() 只从区域开头、结尾,或从中间开始到结尾的一整段开始,不会在中间打个洞这样子
- 在本实验中不用考虑是否为脏页
- 修改 exit() 来保证退出前所有的 mmap 已经取消
- 修改 fork() 来确保子进程也拥有正确的映射
实现
首先是注册系统调用。
然后根据提示添加一个存储信息的结构体
//proc.h
struct vma_t{
uint64 addr; //addr
int length; //length
int prot; //prot
int flags; //flags
struct file *f; // fd
int offset; //offset
int used;
};
struct proc {
...
struct vma_t vmas[16];
};
mmap的实现
uint64 sys_mmap(void)
{
struct file *f;
int length, prot, flags, offset;
uint64 addr;
if(argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0 ||
argint(3, &flags) < 0 || argfd(4, 0, &f) < 0 || argint(5, &offset) < 0)
return -1;
// mmap不应分配物理内存或读取文件,文件不可写,但是prot 和 flags设置为写入
if (f->readable && !f->writable && (prot & PROT_WRITE) && (flags & MAP_SHARED))
return -1;
struct proc *p = myproc(); //当前进程
if(p->sz > MAXVA - length)
return -1;
//找一个没用过的
for(int i=0; i<16; ++i)
{
if (p->vmas[i].addr == 0 && p->vmas[i].used == 0)
{
p->vmas[i].used = 1;
p->vmas[i].addr = p->sz;
p->vmas[i].length = length;
p->vmas[i].prot = prot;
p->vmas[i].flags = flags;
p->vmas[i].f = f;
p->vmas[i].offset = offset;
filedup(f); //计数+1
p->sz += length;
return p->vmas[i].addr;
}
}
return -1; //没找到
}
到此为止我们所有的映射都是懒分配的,所以需要一个处理缺页错误:
查询 riscv 的手册,以及实验提示,可以找到 scause 寄存器中储存 13 和 15 代表缺页错误
//trap.c
#include "fcntl.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
void usertrap(void)
{
...
else if((which_dev = devintr()) != 0){
// ok
}
else if(r_scause() == 13 || r_scause() == 15)
{
uint64 va = r_stval(); // 读stval寄存器
if(va >= p->sz || va >MAXVA || PGROUNDUP(va) == PGROUNDDOWN(p->trapframe->sp)) p->killed = 1;
else
{
struct vma_t *vma = 0;
// 找出错的vma
for(int i=0; i<16; ++i)
{
if(p->vmas[i].used == 1 && va >= p->vmas[i].addr && va < p->vmas[i].addr + p->vmas[i].length)
{
vma = &p->vmas[i];
break;
}
}
if(vma)
{
va = PGROUNDDOWN(va);
uint64 offset = va - vma->addr;
uint64 mem = (uint64)kalloc();
if(mem == 0) //分配内存失败
{
p->killed = 1;
}
else
{
memset((void*)mem, 0, PGSIZE);
ilock(vma->f->ip);
readi(vma->f->ip, 0, mem, offset, PGSIZE);
iunlock(vma->f->ip);
//设置flag
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;
}
}
}
}
}
}
这里需要注意,由于测试时会测试地址在栈空间之外等不合法的地方,因此产生读写中断时,需要首先判断地址是否合法。然后判断地址是否在某个文件映射的虚拟地址范围内,如果找到该文件,则读取磁盘,并将地址映射到产生中断的虚拟地址上。
还需要注意,由于一些地址并没有进行映射,因此在 walk 的时候,遇到这些地址直接跳过即可:
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 为未使用状态。
uint64 sys_munmap(void)
{
uint64 addr;
int length;
struct proc *p = myproc();
struct vma_t *vma = 0;
if(argaddr(0, &addr) || argint(1, &length))
return -1;
for(int i=0; i<16;++i)
{
if(addr >= p->vmas[i].addr && addr < p->vmas[i].addr + p->vmas[i].length)
{
vma = &p->vmas[i];
break;
}
}
if(vma == 0) return 0;
if(vma->addr == addr)
{
vma->addr += length;
vma->length -= length;
if (vma->flags & MAP_SHARED)
filewrite(vma->f, addr, length);
uvmunmap(p->pagetable, addr, length/PGSIZE, 1);
if(vma->length == 0)
{
fileclose(vma->f);
vma->used = 0;
}
}
return 0;
}
最后fork/exit,在进程创建和退出时,需要复制和清空相应的文件映射:
int
fork(void){
...
np->state = RUNNABLE;
for(int i = 0; i < VMASIZE; i++) {
if(p->vma[i].used){
memmove(&(np->vma[i]), &(p->vma[i]), sizeof(p->vma[i]));
filedup(p->vma[i].file);
}
}
release(&np->lock);
...
}
void
exit(int status){
...
for(int i = 0; i < VMASIZE; i++) {
if(p->vma[i].used) {
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);
uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].length/PGSIZE, 1);
p->vma[i].used = 0;
}
}
begin_op();
...
}
真难