1 目的
本实验实现mmap和munmap系统调用来更好的控制进程地址空间,可以向数组那样读写文件,写的数据放在buffer cache可以被其他进程所看到。
2 问题分析
代码语言:c复制void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
实现内存映射机制需要维护用户地址和文件偏移量的映射关系vma,为了节省空间,采用lazy allocate方式分配,通过page fault来分配物理页。munmap时只能是从左边界开始或者在右边界结束,不能unmap中间部分。
- lazy分配,通过page fault来分配物理页,使得物理页少于文件大小时也能够可用;
- 定义数据结构VMA来追踪每个进程的mmap,VMA应该包含file;
- 增加代码来导致mmaped区域触发page fault,即创建页表项但不分配物理页;page fault时读取4KB文件内容填充物理页并映射到用户空间,通过readi进行读取;
- munmap时寻找VMA并unmap这个区域,如果unmap掉mmap映射的所有页,那么就减少对文件的引用。如果unmapped page被修改过且MAP_SHARED,filewrite写回文件;
- 修改exit来unmap进程内存映射区域,修改fork来确保子进程会复制mapped区域,需要对文件增加引用;
3 代码实现
3.1 接口定义
MAP_SHARED是进程共享,会将修改的数据写回文件,MAP_PRIVATE不会刷回文件,只在内存中临时保存。PROT_*是读写权限,在分配物理页时会根据这个来设置PTE的flag。vma是维护进程空间和文件偏移的映射关系的,每个进程都有一个vma数组来维护。
代码语言:c复制void* mmap(void *addr,int len,int prot,int flags,int fd,int offset);
int munmap(void *addr,int len);
//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
//proc.h
struct vma{
int valid;
uint64 addr; //user space addr
uint len; //按页向上取整
struct file *f;
uint offset;
uint flags; //MAP_PRIVATE: 不会更新到磁盘 MAP_SHARED: 会刷盘
uint prot; //访问权限
};
struct proc{
//...
struct vma pvma[MAXVMAPERPROC];
}
然后在user/user.h添加接口;
代码语言:c复制void* mmap(void *addr,int len,int prot,int flags,int fd,int offset);
int munmap(void *addr,int len);
再在usys.pl添加entry;
代码语言:c复制entry("mmap");
entry("munmap");
最后添加到系统调用表中。
代码语言:c复制//syscall.h
#define SYS_mmap 22
#define SYS_munmap 23
//syscall.c
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);
[SYS_mmap] sys_mmap,
[SYS_munmap] sys_munmap,
3.2 sys_mmap
mmap实现放在sysfile.c中,首先校验参数,如果在MAP_SHARED且文件不可写且mmap可写,那么就error,因为文件可能也同时被其他进程读,会有影响。然后从进程的pvma数组中选择一个空槽位来分配,此时只需要移动sz,不需要真分配物理内存。
代码语言:c复制uint64
sys_mmap(void){
uint64 addr;
int len,prot,flags,fd,offset;
if(argaddr(0,&addr)<0 || argint(1,&len)<0 || argint(2,&prot)<0
|| argint(3,&flags)<0 || argint(4,&fd)<0 || argint(5,&offset)<0 )
return -1;
struct proc *p=myproc();
struct file *f=p->ofile[fd];
if(f==0)
return -1;
if((flags&MAP_SHARED) && !f->writable && (prot & PROT_WRITE))
return -1;
//addr默认是0,由内核决定映射到进程空间的位置
struct vma *pvma=p->pvma;
for(int i=0;i< MAXVMAPERPROC;i ){
if(!pvma[i].valid){
// printf("mmap: file.ref[%d]n",f->ref);
pvma[i].f=filedup(f);
pvma[i].addr=p->sz;
pvma[i].len=PGROUNDUP(len);
pvma[i].prot=prot;
pvma[i].flags=flags;
pvma[i].offset=offset;
pvma[i].valid=1;
p->sz =len;
// printf("mmap: ref[%d], inode.ref[%d]n",f->ref,f->ip->ref);
return pvma[i].addr;
}
}
return -1;
}
3.3 page fault handler
mmap时是lazy allocate,读写时会发生缺页中断,需要处理。先从stval寄存器中获取缺页地址,然后扫描proc.pvma数组,找到该地址所在的vma;然后根据分配一页内存并根据相对偏移量从文件指定位置读取一页;最后将这一页map到用户页表上。
代码语言:c复制//trap.c
//...
} else if((which_dev = devintr()) != 0){
// ok
} else if(r_scause()==13 || r_scause()==15){
uint64 va=r_stval();
if(mmap_pgfaulthandler(va)!=0){
printf("usertrap(): unexpected scause %p pid=%dn", r_scause(), p->pid);
printf(" sepc=%p stval=%pn", r_sepc(), r_stval());
p->killed = 1;
}
}
//...
int vm_exists(pagetable_t pagetable, uint64 va){
pte_t *pte;
return (pte=walk(pagetable,va,0)) && (*pte & PTE_V);
}
int mmap_pgfaulthandler(uint64 va){
va=PGROUNDDOWN(va);
struct vma *a=0;
//缺页处理
struct proc *p=myproc();
struct vma *pvma=p->pvma;
for(int i=0;i< MAXVMAPERPROC;i ){
if(p->pvma[i].valid && va>=pvma[i].addr && va<pvma[i].addr pvma[i].len){
a=&pvma[i];
break;
}
}
if(a==0)
return -1;
uint64 pa=(uint64)kalloc();
if(pa==0)
return -1;
memset((void*)pa,0,PGSIZE);
int flag=PTE_U;
flag|=a->prot & PROT_READ ? PTE_R:0;
flag|=a->prot & PROT_WRITE ? PTE_W:0;
if(mappages(p->pagetable,va,PGSIZE,pa,flag)!=0){
kfree((void*)pa);
return -1;
}
ilock(a->f->ip);
printf("trap: va[%p]n",va);
if(readi(a->f->ip,0,pa,a->offset va-a->addr,PGSIZE)<=0){
iunlock(a->f->ip);
return -1;
}
iunlock(a->f->ip);
return 0;
}
3.4 sys_munmap
munmap会释放掉映射的内存,如果是MAP_SHARED还会写回文件中。先从pvma数组找到释放的vma,并决定需要释放左边、右边还是全部,释放左边需要移动vma.addr和vma.offset,释放右边减少vma.len即可;然后逐页判断PTE是否有效,有且MAP_SHARED则释放并写回文件;然后uvmunmap页表项映射;最后,如果整个释放,则关闭文件并将vma.valid=0。
代码语言:c复制//sysfile.c
uint64
sys_munmap(void){
uint64 addr;
int len;
if(argaddr(0,&addr)<0 || argint(1,&len)<0)
return -1;
return munmap(addr,len);
}
//vm.c
int munmap(uint64 addr,int length){
struct proc *p = myproc();
struct vma *a = 0;
addr = PGROUNDDOWN(addr);
for(int i = 0; i < MAXVMAPERPROC; i ){
if(p->pvma[i].valid && addr >= p->pvma[i].addr && addr < p->pvma[i].addr p->pvma[i].len){
a = &p->pvma[i];
break;
}
}
if (a == 0) return -1;
uint64 unstart, unlen;
uint64 start = a->addr, offset = a->offset, orilen = a->len;
if(addr == a->addr){
// Unmap at the start
unstart = addr;
unlen = PGROUNDUP(length) < a->len ? PGROUNDUP(length) : a->len;
a->addr = unstart unlen;
a->len = start orilen - a->addr;
a->offset = a->offset unlen;
} else if(addr length >= start orilen){
// Unmap at the end
unstart = start;
unlen = start orilen - unstart;
a->len = unstart-start;
} else{
// Unmap the whole region
unstart = start;
unlen = orilen;
}
for(int i = 0; i < unlen / PGSIZE; i ){
uint64 va = unstart i * PGSIZE;
// May not be alloced due to lazy alloc through page fault.
if(vm_exists(p->pagetable, va)){
if(a->flags & MAP_SHARED){
munmap_writeback(va, PGSIZE, start, offset, a);
}
uvmunmap(p->pagetable, va, 1, 1);
}
}
if(unlen == orilen){
fileclose(a->f);
a->valid = 0;
}
return 0;
}
因为采用了lazy allocate,uvmunmap也需要稍作修改;
代码语言:c复制void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va npages*PGSIZE; a = PGSIZE){
if((pte = walk(pagetable, a, 0)) == 0)
continue; //修改处
if((*pte & PTE_V) == 0)
continue;
if(PTE_FLAGS(*pte) == PTE_V)
continue;
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
释放映射区域需要将数据写回文件,参照filewrite中的逻辑,通过log和inode层的函数写回。
代码语言:c复制int
munmap_writeback(uint64 unstart, uint64 unlen, uint64 start, uint64 offset, struct vma *a)
{
struct file *f = a->f;
uint off = unstart - start offset;
uint size;
ilock(f->ip);
size = f->ip->size;
iunlock(f->ip);
if(off >= size) return -1;
uint n = unlen < size - off ? unlen : size - off;
int r, ret = 0;
int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;
int i = 0;
while(i < n){
int n1 = n - i;
if(n1 > max)
n1 = max;
begin_op();
ilock(f->ip);
r = writei(f->ip, 1, unstart, off i, n1);
iunlock(f->ip);
end_op();
if(r != n1){
// error from writei
break;
}
i = r;
}
ret = (i == n ? n : -1);
return ret;
}
3.5 fork、exit
fork时需要拷贝vma数组,释放时也要重置。
代码语言:c复制//proc.c
//fork()
for(int i=0;i< MAXVMAPERPROC;i ){
if(p->pvma[i].valid){
np->pvma[i]=p->pvma[i];
filedup(np->pvma[i].f);
}
}
//exit()
//释放mmap region
for(int i=0;i< MAXVMAPERPROC;i ){
if(p->pvma[i].valid){
munmap(p->pvma[i].addr,p->pvma[i].len);
p->pvma[i].valid=0;
}
}