【XV6】 mmap
代码:https://github.com/JasenChao/xv6-labs.git
文件映射到进程地址
题目要求实现两个系统调用:mmap
和munmap
。主要功能就是将文件映射到进程的内存中。
题目给出了mmap
和munmap
的声明:
void *mmap(void *addr, size_t len, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t len);
其中:
addr
为映射的地址,在本实验中addr
总是0,也就是由操作系统来决定映射的地址length
是映射的长度prot
对应读写的权限flags
为映射的类型,类型为shared
时最终需要写回外存fd
是文件描述符offset
是偏移量,本实验中总是0
进程的虚拟内存
在proc.h
中定义一个结构体来标记虚拟内存,每个进程都需要有这样的一个结构体数组:
#define VMA_MAX 16
struct VMA{
int valid; //有效位,当值为 0 时表示无效,即为 empty element
uint64 addr; //记录起始地址
int len; //长度
int prot; //权限(read/write)
int flags; //区域类型(shared/private)
int off; //偏移量
struct file* f; //映射的文件
uint64 mapcnt; //(延迟申请)已经映射的页数量
};
struct proc {
...
struct VMA vma[VMA_MAX]; // VMA
uint64 maxaddr; // heap可用最大地址
};
映射的地址从进程空间的高地址向低地址生长,因此增加一个变量maxaddr
用来标记heap中可用的地址。
在进程初始化的时候,需要对应地处理新增的这部分内容,在proc.c
中的allocproc
函数中增加:
for(int i = 0; i < VMA_MAX; i++){
p->vma[i].valid = 0; // 一开始所有的VMA都是无效的
p->vma[i].mapcnt = 0; // 一开始映射的数量为0
}
p->maxaddr = MAXVA - 2 * PGSIZE; // 减去已被使用的trampoline和trapframe的空间
系统调用的具体实现
上面提到flags
为shared
时需要写回,先在file.c
中实现写回的函数:
int
filewriteoff(struct file *f, uint64 addr, int n, int off)
{
int r, ret = 0;
if(f->writable == 0)
return -1;
if(f->type == FD_INODE){
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);
if ((r = writei(f->ip, 1, addr + i, off, n1)) > 0)
off += r;
iunlock(f->ip);
end_op();
if(r != n1){
// error from writei
break;
}
i += r;
}
ret = (i == n ? n : -1);
} else {
panic("my filewrite");
}
return ret;
}
在defs.h
中声明:
int filewriteoff(struct file*, uint64, int n, int off);
在sysfile.c
中实现具体的系统调用函数:
uint64 sys_mmap(void)
{
uint64 addr;
int len , prot , flags , fd , off;
argaddr(0, &addr);
argint(1, &len);
argint(2, &prot);
argint(3, &flags);
argint(4, &fd);
argint(5, &off);
struct proc* p = myproc();
struct file* f = p->ofile[fd];
// 检查权限和类型,MAP_SHARED类型对文件的修改会写回外存
if(flags == MAP_SHARED && f->writable == 0 && (prot & PROT_WRITE))
return -1;
// 找到一块空的VMA
int idx;
for(idx = 0; idx < VMA_MAX; idx++)
if(p->vma[idx].valid == 0)
break;
if(idx == VMA_MAX)
panic("no empty vma field");
struct VMA* vp = &p->vma[idx];
vp->valid = 1;
vp->len = len;
vp->flags = flags;
vp->off = off;
vp->prot = prot;
vp->f = f;
filedup(f); // 保证文件不会被关闭
vp->addr = (p->maxaddr -= len); // len长度的内存分配出来,maxaddr减去len
return vp->addr;
}
uint64 sys_munmap(void)
{
uint64 addr;
int len;
argaddr(0, &addr);
argint(1, &len);
struct proc* p = myproc();
struct VMA* vp = 0;
for(int i = 0; i < VMA_MAX; i++)
if(p->vma[i].addr <= addr && addr < p->vma[i].addr + p->vma[i].len && p->vma[i].valid == 1){
vp = &p->vma[i];
break;
}
if(vp == 0)
panic("munmap no such vma");
// if the page has been mapped
if(walkaddr(p->pagetable , addr) != 0){
// MAP_SHARED类型需要写回
if(vp->flags == MAP_SHARED)
filewriteoff(vp->f, addr, len, addr-vp->addr);
uvmunmap(p->pagetable, addr, len/PGSIZE, 1);
return 0;
}
// 引用计数为0时关闭文件
if(0 == (vp->mapcnt -= len)){
fileclose(vp->f);
vp->valid = 0;
}
return 0;
}
按照之前的方法在以下文件中添加系统调用:
# user/user.h
void *mmap(void *addr, size_t len, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t len);
# user/usys.pl
entry("mmap");
entry("munmap");
# kernel/syscall.h
#define SYS_mmap 22
#define SYS_munmap 23
# kernel/syscall.c
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);
static uint64 (*syscalls[])(void) = {
...
[SYS_mmap] sys_mmap,
[SYS_munmap] sys_munmap,
};
注意到测试程序未进入编译,需要在Makefile
中添加:
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
$U/_init\
$U/_kill\
$U/_ln\
$U/_ls\
$U/_mkdir\
$U/_rm\
$U/_sh\
$U/_stressfs\
$U/_usertests\
$U/_grind\
$U/_wc\
$U/_zombie\
$U/_mmaptest\
缺页中断
mmap
不负责分配物理内存,因此只有在出现缺页中断时需要处理真实的内存分配,类似于之前实验中的写时复制,要在trap.c
中的usertrap
函数中增加对0xd
缺页中断的处理:
} else if(r_scause() == 0xd){
uint64 addr = r_stval();
struct VMA* vp = 0;
// 找到缺页的VMA
for(int i = 0; i < VMA_MAX; i++)
if(p->vma[i].addr <= addr && addr < p->vma[i].addr + p->vma[i].len && p->vma[i].valid == 1){
vp = &p->vma[i];
break;
}
if(vp != 0){
uint64 mem = (uint64)kalloc();
memset((void*)mem, 0, PGSIZE);
if(mappages(p->pagetable, addr, PGSIZE, mem, PTE_U | PTE_V | (vp->prot << 1)) == -1)
panic("pagefault map error");
vp->mapcnt += PGSIZE; //maintain the mapcnt
ilock(vp->f->ip);
readi(vp->f->ip, 0, mem, addr-vp->addr, PGSIZE); //copy a page of the file from the disk
iunlock(vp->f->ip);
}else{
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
这里用到了很多无法调用的结构体,需要在trap.c
中添加对应的头文件:
#include "fs.h"
#include "sleeplock.h"
#include "file.h"
fork & exit
根据提示,fork
需要保证父子进程有一样的内存映射,exit
也需要处理取消映射,在proc.c
中修改这两个函数,fork
中需要多复制一份VMA数组:
np->maxaddr = p->maxaddr;
for(int i = 0; i < VMA_MAX; i++)
if(p->vma[i].valid){
filedup(p->vma[i].f);
memmove(&np->vma[i], &p->vma[i], sizeof(struct VMA));
}
exit
需要取消已有的映射,发现类型为MAP_SHARED
时调用写回函数:
for(int i = 0; i < VMA_MAX; i++){
if(p->vma[i].valid == 1){
struct VMA* vp = &p->vma[i];
for(uint64 addr = vp->addr; addr < vp->addr + vp->len; addr += PGSIZE){
if(walkaddr(p->pagetable, addr) != 0){
if(vp->flags == MAP_SHARED)
filewriteoff(vp->f, addr, PGSIZE, addr-vp->addr);
uvmunmap(p->pagetable, addr, 1, 1);
}
}
fileclose(p->vma[i].f);
p->vma[i].valid = 0;
}
}
这里用到了MAP_SHARED
宏定义,因此要在proc.c
中添加头文件:
#include "fcntl.h"
测试结果
使用make grade
测试,结果如下:
== Test running mmaptest ==
$ make qemu-gdb
(2.4s)
== Test mmaptest: mmap f ==
mmaptest: mmap f: OK
== Test mmaptest: mmap private ==
mmaptest: mmap private: OK
== Test mmaptest: mmap read-only ==
mmaptest: mmap read-only: OK
== Test mmaptest: mmap read/write ==
mmaptest: mmap read/write: OK
== Test mmaptest: mmap dirty ==
mmaptest: mmap dirty: OK
== Test mmaptest: not-mapped unmap ==
mmaptest: not-mapped unmap: OK
== Test mmaptest: two files ==
mmaptest: two files: OK
== Test mmaptest: fork_test ==
mmaptest: fork_test: OK
== Test usertests ==
$ make qemu-gdb
usertests: OK (38.6s)