Lab10 Mmap
实现内存映射文件的操作,将文件映射到内存中,从而在与文件交互的时候减少磁盘操作。
添加系统调用
配置mmap
和munmap
系统调用,这里不再赘述。
向进程添加虚拟内存区域
#define NVMA 16
// 虚拟内存区域结构体
struct vm_area
{
int used; // 是否已被使用
uint64 addr; // 起始地址
int len; // 长度
int prot; // 权限
int flags; // 标志位
int vfd; // 对应的文件描述符
struct file *vfile; // 对应文件
int offset; // 文件偏移,本实验中一直为0
};
// Per-process state
struct proc
{
......
struct vm_area vma[NVMA];
};
static struct proc *
allocproc(void)
{
......
memset(&p->vma, 0, sizeof(p->vma)); // 初始化
return p;
}
实现映射关系
uint64
sys_mmap(void)
{
uint64 addr;
int length;
int prot;
int flags;
int vfd;
struct file *vfile;
int offset;
uint64 err = 0xffffffffffffffff;
// 获取系统调用参数
if (argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0 ||
argint(3, &flags) < 0 || argfd(4, &vfd, &vfile) < 0 || argint(5, &offset) < 0)
return err;
// 实验提示中假定addr和offset为0,简化程序可能发生的情况
if (addr != 0 || offset != 0 || length < 0)
return err;
// 文件不可写则不允许拥有PROT_WRITE权限时映射为MAP_SHARED
if (vfile->writable == 0 && (prot & PROT_WRITE) != 0 && flags == MAP_SHARED)
return err;
struct proc *p = myproc();
// 没有足够的虚拟地址空间
if (p->sz + length > MAXVA)
return err;
// 遍历查找未使用的VMA结构体
for (int i = 0; i < NVMA; ++i)
{
if (p->vma[i].used == 0)
{
p->vma[i].used = 1;
p->vma[i].addr = p->sz;
p->vma[i].len = length;
p->vma[i].flags = flags;
p->vma[i].prot = prot;
p->vma[i].vfile = vfile;
p->vma[i].vfd = vfd;
p->vma[i].offset = offset;
// 增加文件的引用计数
filedup(vfile);
p->sz += length;
return p->vma[i].addr;
}
}
return err;
}
识别页错误
void usertrap(void)
{
......
else if ((which_dev = devintr()) != 0)
{
// ok
}
else if (r_scause() == 13 || r_scause() == 15)
{
#ifdef LAB_MMAP
// 读取产生页面故障的虚拟地址,并判断是否位于有效区间
uint64 fault_va = r_stval();
if (PGROUNDUP(p->trapframe->sp) - 1 < fault_va && fault_va < p->sz)
{
if (mmap_handler(r_stval(), r_scause()) != 0)
p->killed = 1;
}
else
p->killed = 1;
#endif
}
......
usertrapret();
}
int mmap_handler(int va, uint64 cause)
{
int i;
struct proc *p = myproc();
// 根据地址查找属于哪一个VMA
for (i = 0; i < NVMA; ++i)
{
if (p->vma[i].used && p->vma[i].addr <= va && va <= p->vma[i].addr + p->vma[i].len - 1)
{
break;
}
}
if (i == NVMA)
return -1;
int pte_flags = PTE_U;
if (p->vma[i].prot & PROT_READ)
pte_flags |= PTE_R;
if (p->vma[i].prot & PROT_WRITE)
pte_flags |= PTE_W;
if (p->vma[i].prot & PROT_EXEC)
pte_flags |= PTE_X;
struct file *vf = p->vma[i].vfile;
// 读导致的页面错误
if (cause == 13 && vf->readable == 0)
return -1;
// 写导致的页面错误
if (cause == 15 && vf->writable == 0)
return -1;
void *pa = kalloc();
if (pa == 0)
return -1;
memset(pa, 0, PGSIZE);
// 读取文件内容
ilock(vf->ip);
// 计算当前页面读取文件的偏移量,实验中p->vma[i].offset总是0
// 要按顺序读读取,例如内存页面A,B和文件块a,b
// 则A读取a,B读取b,而不能A读取b,B读取a
int offset = p->vma[i].offset + PGROUNDDOWN(va - p->vma[i].addr);
int readbytes = readi(vf->ip, 0, (uint64)pa, offset, PGSIZE);
// 什么都没有读到
if (readbytes == 0)
{
iunlock(vf->ip);
kfree(pa);
return -1;
}
iunlock(vf->ip);
// 添加页面映射
if (mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, pte_flags) != 0)
{
kfree(pa);
return -1;
}
return 0;
}
实现解除映射关系
uint64
sys_munmap(void)
{
uint64 addr;
int length;
if (argaddr(0, &addr) < 0 || argint(1, &length) < 0)
return -1;
int i;
struct proc *p = myproc();
for (i = 0; i < NVMA; ++i)
{
if (p->vma[i].used && p->vma[i].len >= length)
{
// 根据提示,munmap的地址范围只能是
// 1. 起始位置
if (p->vma[i].addr == addr)
{
p->vma[i].addr += length;
p->vma[i].len -= length;
break;
}
// 2. 结束位置
if (addr + length == p->vma[i].addr + p->vma[i].len)
{
p->vma[i].len -= length;
break;
}
}
}
if (i == NVMA)
return -1;
// 将MAP_SHARED页面写回文件系统
if (p->vma[i].flags == MAP_SHARED && (p->vma[i].prot & PROT_WRITE) != 0)
{
filewrite(p->vma[i].vfile, addr, length);
}
// 判断此页面是否存在映射
uvmunmap(p->pagetable, addr, length / PGSIZE, 1);
// 当前VMA中全部映射都被取消
if (p->vma[i].len == 0)
{
fileclose(p->vma[i].vfile);
p->vma[i].used = 0;
}
return 0;
}
如果对惰性分配的页面调用了uvmunmap
,或者子进程在fork中调用uvmcopy
复制了父进程惰性分配的页面都会导致panic,因此需要修改uvmunmap
和uvmcopy
检查PTE_V
后不再panic
if((*pte & PTE_V) == 0)
continue;
修改exit
解除已映射区域
void exit(int status)
{
struct proc *p = myproc();
......
// 将进程的已映射区域取消映射
for (int i = 0; i < NVMA; ++i)
{
if (p->vma[i].used)
{
if (p->vma[i].flags == MAP_SHARED && (p->vma[i].prot & PROT_WRITE) != 0)
{
filewrite(p->vma[i].vfile, p->vma[i].addr, p->vma[i].len);
}
fileclose(p->vma[i].vfile);
uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].len / PGSIZE, 1);
p->vma[i].used = 0;
}
}
......
}
修改fork
增加引用计数
int fork(void)
{
......
// increment reference counts on open file descriptors.
for (i = 0; i < NOFILE; i++)
if (p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
// 复制父进程的VMA
for (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].vfile);
}
}
......
return pid;
}
所有的问题都解决了。