Lab10 mmap
文件内存映射
在传统的I/0技术中,
- 读取一个文件,文件数据会:磁盘->内核缓冲区->用户缓冲区
- 如果进行了修改,则是用户缓冲区->内核缓冲区->磁盘
也就是说,每次读写都要 切换内核态/用户态 以及 在内核空间和用户空间之间拷贝数据。这对于处理文件读写密集型场景和大文件不友好。
而mmap技术:
- 将 内核缓冲区中的文件数据 映射 到进程的地址空间,就能直接在用户空间修改文件内容,免去了一次拷贝过程。
mmap
实现处理文件内存映射的mmap 与 munmap 系统调用。
实验手册上提供了 mmap 和 munmap 的函数签名:
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
可以假设 addr 始终为零,这意味着内核应决定映射文件的虚拟地址。mmap 返回该地址,或在失败时返回 0xffffffffffffffff。length 是要映射的字节数;它可能与文件的长度不同。prot 指示内存应被映射为可读、可写和/或可执行; 可以假设 prot 为 PROT_READ 或 PROT_WRITE 或两者兼有。flags 可能是 MAP_SHARED,表示对映射内存的修改应写回文件,或 MAP_PRIVATE,表示不应写回。无需实现 flags 中的其他位。fd 是要映射的文件的打开文件描述符。可以假设偏移量为零(它是文件中映射的起始位置)。
int munmap(void* addr, int length);
munmap 移除指定地址范围内的 mmap 映射。如果进程已修改内存且将其映射为 MAP_SHARED,则修改应首先写回文件。munmap 调用可能仅覆盖 mmap 映射区域的一部分,但可以假设它要么从开头取消映射,要么在结尾取消映射,要么取消整个区域的映射(但不会在区域中间留下空洞)。也就是说释放后的区域数目必须是0或1,
注册系统调用
//user/user.h
//System calls
void* mmap(void*, int, int, int, int, int);
int munmap(void*, int);
//user/usys.pl
entry("mmap");
entry("munmap");
//Makefile
$U/_mmaptest\
//kernel/syscall.h
#define SYS_mmap 22
#define SYS_munmap 23
//kernel/syscall.c
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);
[SYS_mmap] sys_mmap,
[SYS_munmap] sys_munmap,
相关数据结构
根据手册,定义一个结构体vma来跟踪进程映射的内存区域。其中相关的标志在fcntl.h中已经定义好了。
//kernel/prco.h
struct vma{
// int valid; //该虚拟内存区域是否被映射
uint64 start; //虚拟内存映射区域开始地址
uint64 sz; //虚拟内存映射区域大小
int prot; //内存被映射为读、写、读写
int flags; //内存的修改是否写回文件
struct file* file; //该虚拟区域映射的文件
uint64 offset; //文件偏移量
};
#define NVMA 16
struct proc {
struct spinlock lock;
...
struct vma vmas[NVMA];
};
实现mmap
//kernel/sysfile.c
uint64
sys_mmap(void)
{
uint64 addr;
int length, prot, flags, fd, offset;
struct file* file;
//传入参数
if( argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0
|| argint(3, &flags) < 0 || argfd(4, &fd, &file) < 0 || argint(5, &offset) < 0)
return 0xffffffffffffffff;
//检查参数
if( length <=0 )
return 0xffffffffffffffff;
if( (!file->readable && (prot & PROT_READ)) //源文件不可读但映射可读
|| (!file->writable && (prot & PROT_WRITE) && (flags == MAP_SHARED) )//源文件不可写但映射可写
)
return 0xffffffffffffffff;
struct proc* p = myproc();
struct vma* v = 0;
length = PGROUNDUP(length);//页面对齐
//遍历进程的vams数组,寻找第一个空闲的vams
for(int i = 0; i < NVMA; ++i){
if( p->vmas[i].valid == 0){
v = &p->vmas[i];
v->valid = 1;
//设置映射区域的属性
v->start = p->sz;//使用进程空间的末尾
p->sz += length;
v->sz = length;
v->prot = prot;
v->flags = flags;
v->file = file;
v->offset = offset;
//给文件增加引用记数
filedup(file);
return v->start;
}
}
printf("没有空闲的映射内存区域\n");
return 0xffffffffffffffff;
}
惰性分配
映射功能也使用惰性分配,实际的物理内存分配延迟到usertrap中:当 mmap 引发页面故障时,分配一页物理内存,将相关文件的 4096 字节的数据读入,并将其映射到用户地址空间。
//kernel/sysfile.c
int vamlazyalloc(uint64 va){
//先找到对应的vma
struct proc* p = myproc();
struct vma* v = 0;
for(int i = 0; i < NVMA; ++i){
v = &p->vmas[i];
if( v->valid == 1 && v->start <= va && va < v->start + v->sz){
break;
}
if( i == NVMA - 1){
printf("vamlazyalloc没有空闲的vma\n");
return -1;
}
}
//分配物理页
void* pa = kalloc();
if( pa == 0)
panic("err_vamlazyalloc_pa");
memset(pa, 0, PGSIZE);
//读入数据
begin_op();
ilock(v->file->ip);
readi(v->file->ip, 0 ,(uint64)pa, v->offset + PGROUNDDOWN(va - v->start),PGSIZE);
iunlock(v->file->ip);
end_op();
//映射
if(mappages(p->pagetable, va, PGSIZE, (uint64)pa, PTE_R | PTE_W | PTE_U) < 0)
panic("err_vamlazyalloc_map");
return 1;
}
//kernel/defs.h
//sysfile.c
int vamlazyalloc(uint64);
//kernel/trap.c
else if((which_dev = devintr()) != 0){
// ok
} else if(r_scause() == 13 || r_scause() == 15){
uint64 va = r_stval();
if( p->sz < va || va > MAXVA )
p->killed = 1;
else
if( vamlazyalloc(va) == -1)
p->killed = 1;
}
同时像lab 5一样,注释掉一部分panic
//kernel/vm.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)
// panic("uvmunmap: walk");
continue;
if((*pte & PTE_V) == 0)
// panic("uvmunmap: not mapped");
continue;
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
//uvmcopy.c
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
//panic("uvmcopy: page not present");
continue;
munmap
释放进程的映射区域。
//kernel/sysfile.c
uint64
sys_munmap(void)
{
uint64 addr;
int length;
struct proc* p = myproc();
struct vma* v = 0;
if( argaddr(0, &addr) < 0 || argint(1, &length) < 0)
return -1;
if( length <= 0)
return -1;
if( p->sz + length > MAXVA)
return -1;
for(int i = 0; i < NVMA; ++i){
v = &p->vmas[i];
if( v->valid == 1 && v->start <= addr && addr < v->start + v->sz){
break;
}
if( i == NVMA - 1){
panic("err_munmap_va2vma");
}
}
//页面对齐
addr = PGROUNDDOWN(addr);
length = PGROUNDUP(length);
//MAP_SHARED需要写回
if(v->flags & MAP_SHARED)
filewrite(v->file, addr, length);
uvmunmap(p->pagetable, addr, length/PGSIZE, 1);
//要么从开头取消映射,要么在结尾处取消映射,要么取消整个区域的映射(但不会在区域中间留下空洞)
//我理解的是要么取消前一部分,要么取消后一部分,要么取消整个区域
if( addr == v->start && length == v->sz ){//取消整个vma
fileclose(v->file);
v->valid = 0;
} else if(addr == v->start){//取消开头一部分
v->start += length;
v->sz -= length;
v->offset += length;
} else if( addr != v->start && addr + length == v->start + v->sz){//取消后一部分
v->sz -= length;
} else{
panic("err_sysmunmap:非法取消映射区域");
}
return 0;
}
fork和exit
fork创建子进程时同样也要复制vma数组。
//kernel/proc.c
//fork()
pid = np->pid;
for(int i = 0; i < NVMA; i++) {
if(p->vmas[i].valid) {
np->vmas[i] = p->vmas[i];
if(p->vmas[i].file)
filedup(p->vmas[i].file);
}
}
np->state = RUNNABLE;
进程退出时也要取消映射,并写盘。
//kernel/proc.c
//eixt()
for(int i = 0; i < NVMA; ++i){
if( p->vmas[i].flags & MAP_SHARED){
filewrite(p->vmas[i].file, p->vmas[i].start, p->vmas[i].sz);
}
uvmunmap(p->pagetable, p->vmas[i].start, p->vmas[i].sz/PGSIZE, 1);
p->vmas[i].valid = 0;
}
begin_op();
运行mmaptest和usertests均能通过
$ mmaptest
mmap_test starting
test mmap f
test mmap f: OK
test mmap private
test mmap private: OK
test mmap read-only
test mmap read-only: OK
test mmap read/write
test mmap read/write: OK
test mmap dirty
test mmap dirty: OK
test not-mapped unmap
test not-mapped unmap: OK
test mmap two files
test mmap two files: OK
$ usertests
...
test forktest: OK
test bigdir: OK
ALL TESTS PASSED

浙公网安备 33010602011771号