实验要求实现mmap和munmap的系统调用,这两个系统调用是把文件映射到内存,即内存映射文件。
实现内存映射文件的作用在于,我们一个进程在对文件进行大量读写的时候可以避免大量的I/O操作,因为文件在内存里面,所以我们对内存进行读写即可。
首先是系统调用的添加。
然后是实现sys_mmap,给每个进程开一个16个vma的数组:
// virtual memory area
struct vma
{
int valid;
uint64 vm_start;// 虚拟地址
uint64 vm_end;// 虚拟地址
int length;// 地址长度
int prot;// 操作文件权限
int flags;
struct file *fd;// 映射文件
int offset;// 从文件offset处开始映射
};
之后完成mmap,把文件映射到内存中,这里分配虚拟地址的时候从高地址往下分配,考虑到sbrk也要分配内存,从低地址往高地址分配,所以我们反过来:
这里的MMAPEND是MAXVA减去两个PGSIZE得到的,也就是TRAMPOLINE和TRAPFRAME,下次分配页面的时候从最低地址开始分配,这样不可避免有外部碎片。
uint64
sys_mmap(void){
uint64 addr;
int length;
int prot;
int flags;
int offset;
int int_fd;
struct file *fd;
// 获取参数
if (argaddr(0,&addr)<0 || argint(1,&length)<0 || argint(2,&prot)<0 ||
argint(3,&flags)<0 || argfd(4,&int_fd,&fd)<0 || argint(5,&offset)<0)
return -1;
// 只读文件 映射为内存可写以及写回标记
if (!fd->writable && (prot&PROT_WRITE) && flags==MAP_SHARED)
return -1;
struct proc *p=myproc();
struct vma *tmp=0;
uint64 va_end=MMAPEND;
for(int i = 0; i < MAXVMA; i++){
if (p->p_vma[i].valid==0){
if (tmp==0){
tmp=&p->p_vma[i];
}
}else if(va_end>p->p_vma[i].vm_start){
va_end=PGROUNDDOWN(p->p_vma[i].vm_start);
}
}
if (!tmp)
return -1;
tmp->valid=1;
tmp->vm_start=va_end-length;
tmp->vm_end=va_end;
tmp->length=length;
tmp->prot=prot;
tmp->flags=flags;
tmp->fd=fd;
tmp->offset=offset;
filedup(fd);
// printf("valid:%d, va_start:%d, va_end:%d\n",p->p_vma[0].valid, p->p_vma[0].vm_start,p->p_vma[0].vm_end);
return tmp->vm_start;
}
然后由于是懒加载,我们在trap.c里面增加对缺页的处理,处理流程就是找到对应的vma,分配一个物理页,把文件内容读到物理页,再将虚拟地址和物理页建立映射:
else if(r_scause()==13 || r_scause()==15){
uint64 va=r_stval();
for (int i = 0; i < MAXVMA; i++){
if (p->p_vma[i].valid && p->p_vma[i].vm_start<=va && va<p->p_vma[i].vm_end){
// 分配一个物理页
uint64 pa=(uint64)kalloc();
if (pa==0){
p->killed=1;
kfree((void*)pa);
break;
}
memset((void *)pa,0,PGSIZE);
// 从文件读内容至物理页中
struct inode *ip=p->p_vma[i].fd->ip;
uint off=va-p->p_vma[i].vm_start;
// printf("off:%d\n",off);
ilock(ip);
readi(ip,0,pa,off,PGSIZE);
iunlock(ip);
// 关联虚拟地址和物理页
int flags=PTE_U;
if (p->p_vma[i].prot&PROT_READ) flags|=PTE_R;
if (p->p_vma[i].prot&PROT_WRITE) flags|=PTE_W;
if (p->p_vma[i].prot&PROT_EXEC) flags|=PTE_X;
if (mappages(p->pagetable,PGROUNDDOWN(va),PGSIZE,pa,flags)<0){
p->killed=1;
kfree((void*)pa);
break;
}
break;
}
}
}
接着实现sys_unmap,主要就是,有MAP_SHARED把物理内存的内容写回缓冲块(缓冲块由日志文件系统自动写回),减少vma的区域范围,如果存在映射把映射给释放了,如果长度是0了还要把文件关闭(也就是把inode的块映射全部删了):
uint64
sys_munmap(void){
uint64 addr;
int length;
if (argaddr(0,&addr)<0 || argint(1,&length))
return -1;
struct proc *p=myproc();
for (int i = 0; i < MAXVMA; i++){
if (p->p_vma[i].valid && (addr==p->p_vma[i].vm_start || addr+length==p->p_vma[i].length)){
if (addr==p->p_vma[i].vm_start)
p->p_vma[i].vm_start+=length;
p->p_vma[i].length-=length;
// 文件内容从用户物理内存写回缓冲块
struct file *fd=p->p_vma[i].fd;
if (p->p_vma[i].flags&MAP_SHARED)
filewrite(fd,addr,length);
// 释放虚拟地址和物理内存的映射
uint64 npages=(PGROUNDUP(addr+length)-PGROUNDDOWN(addr))/PGSIZE;
// printf("npages:%d,start:%x,end:%x,walk:%d\n",npages,addr,addr+length,walkaddr(p->pagetable,addr));
if (walkaddr(p->pagetable,addr))
uvmunmap(p->pagetable,PGROUNDDOWN(addr),npages,1);
if (p->p_vma[i].length==0){
fileclose(fd);
p->p_vma[i].valid=0;
}
return 0;
}
}
return -1;
}
在allocproc里面要把vma的valid清0,fork里边注意要增加文件引用数,在exit里边要清空整个vma区域,前两个很简单,这里给出exit中的实现: