task
mmap的定义如下
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
-
在这个lab中,addr永远为0,即内核决定用哪个虚拟地址去映射这个文件,mmap返回这个虚拟地址,或者0xffffffffffffffff 表示失败
-
length代表映射的字节数量,不一定要是文件的长度
-
prot决定了这个内存,可读,可写或者可执行
具体的宏为PROT_READ、PROT_WRITE
-
flags如果是MAP_SHARED,意味着对内存的修改要写回到文件
MAP_PRIVATE意味着不用写回文件
-
fd是文件的描述符,你可以假设offset是0
munmap的定义如下
munmap(addr, length)
- munmap应该移出这些地址范围内的映射
- 如果进程修改了内存,并且映射方式是MAP_SHARED,那么修改应该被写回文件
- munmap可能只覆盖了mmap的一部分区间,可以是开始,可以是末尾,也可能是整个,但不会是中间位置
hints
-
首先在Makefile中添加_mmaptest,并且增加mmap和munmap系统调用
在
kernel/fcntl.h
为你定义了PROT_READ
等参数 -
lazy地对待页表,类似于lazy lab。
即mmap并不会直接分配物理内存和读文件,在usertrap中处理页错误时再真正的分配内存
-
你需要记录每个进程通过mmap映射了什么
你可以定义一个和VMA相关的数据结构,去记录地址,长度,权限,文件等
你可以定义一个固定长度的VMA数组,16就足够了
-
实现mmap
-
在进程的地址空间找一块未使用的区域去映射文件
-
并且增加一个VMA到进程映射区域的表中
- VMA应该包含一个指向
struct file
的指针。mmap应该增加这个文件的引用,这一部分可以参考filedup
- VMA应该包含一个指向
-
这时候运行mmaptest,可以发现第一个成功了,但是后面的还是失败了
-
-
增加代码
- 在页错误发生在mapped区域时,分配一个物理页面,从相关的文件中读取4096个字节,并且将其映射到用户地址空间
- 用readi读取文件,它会使用到一个offset参数,同时你需要将inode结点给lock和unlock
- 不要忘记设置这一页的权限位
- 运行mmaptest,这时候会运行到munmap了
-
实现munmap
- 找到这个地址范围内的VMA,unmap指定的页面,使用
uvmunmap
- 如果munmap删除了mmap分配的所有区域,那它应该减少文件的引用次数
- 如果一个页面是MAP_SHARED,并且被修改了,那么应该写回文件(学习
filewrite
)。写回时不需要管pte的dirty位
- 找到这个地址范围内的VMA,unmap指定的页面,使用
-
修改exit函数,使其能够在进程用过mmap的情况下,将没有被munmap都处理掉,至此mmap_test可能可以通过了
-
修改fork保证孩子也有和父节点一样的映射区域,不要忘记了去给VMA文件增加引用数。在页错误发生时,可以分配一个新的物理页面,而不是和父进程共享一个。
思路
基础设置
首先需要修改Makefile并且添加两个系统调用,这个就比较简单了
增加数据结构
我们应该按照hints提示的,创造一个VMA的结构体,并且在进程的proc的结构体中存储一个VMA的数组。这里的思路就比较简单暴力了,就只维护一个数组,需要用的时候就遍历数组去找就行了。
struct vma {
uint64 addr;
int length;
int prot;
int flag;
int fd;
struct file *file;
int valid;
};
struct proc {
struct vma vmas[16];
//.......
}
sys_mmap
开始写mmap
函数
- 这玩意的作用就是只申请一个虚拟地址空间,但是不直接分配物理地址。在申请完之后,就将相关的东西存在proc的VMA数组中去
- 有几个情况需要特判
- 如果文件不可读,那么就不能用
MAP_SHARED
,因为这种模式在之后会写入到磁盘 - 地址不够了,即
p->sz>MAXVA
- 如果文件不可读,那么就不能用
- 注意点
- 只分配虚拟内存,不增加物理内存,就是通过只增加
p->sz
而不真正的映射实现的。包括页表中都没有相关的记录 - 用
argfd
取出文件指针 - 记得用
filedup
增加文件的引用计数
- 只分配虚拟内存,不增加物理内存,就是通过只增加
uint64
sys_mmap(void) {
uint64 addr;
struct file *file;
int length, prot, flags, offest;
uint64 erro = 0xffffffffffffffff;
// 取出参数
if (argaddr(0, &addr) < 0 || argint(1, &length) < 0
|| argint(2, &prot) < 0 || argint(3, &flags) < 0
|| argfd(4, 0, &file) < 0 || argint(5, &offest) < 0) {
return erro;
}
// 权限不对
if (file->writable == 0 && flags == MAP_SHARED && (prot & PROT_WRITE)) {
return erro;
}
// 地址不够了
struct proc *p = myproc();
if (p->sz + length > MAXVA) {
return erro;
}
// 分配地址,找出一个空闲的vma
addr = p->sz;
p->sz += length;
for (int i = 0; i < 16; i++) {
if (p->vmas[i].valid == 0) {
p->vmas[i].valid = 1;
p->vmas[i].addr = addr;
p->vmas[i].flag = flags;
p->vmas[i].length = length;
p->vmas[i].prot = prot;
p->vmas[i].file = file;
filedup(file);
return addr;
}
}
return erro;
}
usertrap
修改usertrap
函数,使得vma相关的地址能够被正确处理
- 首先应该通过
r_scause
捕获异常 - 判断地址的合法性
- 压根不在vma管辖范围内
- 不能超出当前进程的虚拟地址范围,即
p->sz
- 不能低于栈区
- 在进程的vma数组中找不到对应的地址
- 不能超出当前进程的虚拟地址范围,即
- 权限合法性
- 通过
r_scause
可以知道当前是读还是写操作,通过文件的类型,可以确定是否有这个权限
- 通过
- 压根不在vma管辖范围内
kalloc
分配一个物理页面readi
将文件的内容读入物理页面,其中偏移部分通过当前地址和vma记录的addr做差值得到,因为一读就是一个页面,因此还要将这个偏移向PGSIZE舍入- 将映射关系通过
uvmmap
写入到页表
else if (r_scause() == 13 || r_scause() == 15) {
// #ifdef LAB_MMAP
if (mmap_handler(r_stval(), r_scause()) == -1) {
p->killed = -1;
}
// #endif
} else if ((which_dev = devintr()) != 0) {
int mmap_handler(uint64 va, uint64 r_cause) {
struct proc *p = myproc();
// 地址不合法
if (va >= p->sz || va < p->trapframe->sp) {
return -1;
}
// 是否和vma有关系
struct vma *vma = 0;
for (int i = 0; i < 16; i++) {
if (p->vmas[i].valid == 1 && (va >= p->vmas[i].addr && va < p->vmas[i].addr + p->vmas[i].length)) {
vma = &p->vmas[i];
break;
}
}
// 和vma没关系
if (vma == 0) {
return -1;
}
// 和vma是有关的
// 看看权限是否正确
struct file *file = vma->file;
// 不能读,但是你读了
if (!file->readable && r_cause == 13) {
return -1;
}
// 不能写,但是你写了
if (!file->writable && r_cause == 15) {
return -1;
}
// 构建pte的标志位
int pte_flag = PTE_U;
if (vma->prot & PROT_READ) {
pte_flag |= PTE_R;
}
if (vma->prot & PROT_WRITE) {
pte_flag |= PTE_W;
}
if (vma->prot & PROT_EXEC) {
pte_flag |= PTE_X;
}
// 先分配一个物理页面
uint64 pa = (uint64)kalloc();
if (pa == 0) {
return -1;
}
memset((void *)pa, 0, PGSIZE);
// 成功分配物理页面,从文件中读
struct inode *ip = file->ip;
ilock(ip);
// 读取失败
if (readi(ip, 0, pa, PGROUNDDOWN(va - vma->addr), PGSIZE) == 0) {
iunlock(ip);
kfree((void *)pa);
return -1;
}
iunlock(ip);
// 加入映射
if (mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, pa, pte_flag) != 0) {
kfree((void *)pa);
return -1;
}
return 0;
}
munmap
lab对munmap的情况做了简化,只会从头开始unmap,因此在将数据写回磁盘时,直接调用filewrite函数,它在内部会自动调用file的偏移
munmap就是取消某部分虚拟地址的mmap
- 在进程的vma数组找到对应的vma
- 更新vma中的addr和length,如果当前length=0,说明全部被写入了,那么就通过fileclose关闭这个文件,并且将这个vma的valid修改为0。这个关闭的操作最好放到最后,因为我们可能在第3步还要写这个文件
- 如果需要写入,则通过filewrite函数写入磁盘,这个函数的参数很简单,第一个是文件的指针,第二个是起始的虚拟地址,第三个是length
- 将进程的页表给
uvmunmap
掉,因为lab的仁慈,这里的参数传递也很简单,addr全都是PGSIZE的倍数
uint64
sys_munmap(void) {
uint64 addr;
int length;
if (argaddr(0, &addr) < 0 || argint(1, &length) < 0) {
return -1;
}
// 先找到对应vma
struct proc *p = myproc();
struct vma *vma = 0;
for (int i = 0; i < 16; i++) {
if (p->vmas[i].valid == 1 && (addr >= p->vmas[i].addr && addr < p->vmas[i].addr + p->vmas[i].length)) {
vma = &p->vmas[i];
// 头部
if (vma->addr == addr) {
vma->addr += length;
vma->length -= length;
// 尾部
} else if (addr + length == vma->addr + vma->length) {
vma->length -= length;
}
break;
}
}
if (vma == 0) {
return -1;
}
// 如果是shared,需要先写回磁盘
if (vma->flag == MAP_SHARED) {
filewrite(vma->file, addr, length);
}
// 修改页表
uvmunmap(p->pagetable, addr, length / PGSIZE, 1);
//如果map区域为0
if (vma->length == 0) {
fileclose(vma->file);
vma->valid = 0;
}
return 0;
}
这里有几个小补丁要打,分别是uvmunmap
和uvmcopy
,它们在查找页表的时候,如果发现pte无效,会panic,这里直接忽略,因为可能是mmap还没有分配物理地址的区域
exit
这个函数是用于进程死亡时,就所有的mmap区域都给删掉
- 遍历进程的所有vma
- 如果vma有效
- 如果需要写入磁盘,那就写入磁盘
- 更新页表
- 关闭文件
- 设置vma无效
// 将所有的映射区取消
for (int i = 0; i < 16; i++) {
if (p->vmas[i].valid) {
struct vma *vma = &p->vmas[i];
if (vma->flag == MAP_SHARED) {
filewrite(vma->file, vma->addr, vma->length);
}
fileclose(vma->file);
vma->valid = 0;
uvmunmap(p->pagetable, vma->addr, vma->length / PGSIZE, 1);
}
}
fork
这个函数主要用于子进程将父进程的mmap区域都给拷贝过来,要不然子进程一旦访问还没有分配物理地址的mmap区域,在usertrap里就不能正确处理它了
// 将父进程的mmap也拷贝给它
for (int i = 0; i < 16; i++) {
struct vma *vma = &p->vmas[i];
if (vma->valid) {
memmove(&np->vmas[i], vma, sizeof(struct vma));
filedup(vma->file);
}
}