课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.html
Lab 地址:https://pdos.csail.mit.edu/6.S081/2020/labs/mmap.html
我的代码地址:https://github.com/Amroning/MIT6.S081/tree/mmap
xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf
相关翻译:https://xv6.dgs.zone/labs/requirements/lab10.html
参考博客:https://blog.miigon.net/posts/s081-lab10-mmap/
学习笔记记录,如有错误恳请各位大佬指正
Lab10: mmap
仿照Linux实现mmap功能,即将文件映射到进程地址空间,如果进程修改了这部分内存,并且内存标记为映射内存的修改应写回文件,那么释放映射前需要把修改写入源文件。这样与文件交互的时候就可以减少磁盘操作。该实验需要用到很多前面实验的知识点
完整题目要求请去顶部链接查看
mmap(hard)
您应该实现足够的
mmap
和munmap
功能,以使mmaptest
测试程序正常工作。如果mmaptest
不会用到某个mmap
的特性,则不需要实现该特性。
进程所使用的内存空间从低地址向高地址生长(sbrk
调用),范围是stack到trapframe。为了不和进程使用的内存空间冲突,将mmap使用的地址空间映射到trapframe下面的页,从上往下生长。先定义mmap最后一页的地址:
// kernel/memlayout.h
// MMAP 进程映射文件内存最后一个页(开区间)
#define MMAPEND TRAPFRAME
定义一个vma结构体,表示虚拟内存区域,用来记录mmap创建的虚拟内存地址的范围、长度、权限、文件等。再声明一个vma结构体的数组,当mmap映射操作时,就来这个数组获取vma虚拟内存区域:
// kernel/proc.h
// 定义一个虚拟内存区域结构体,用来记录mmap创建的虚拟内存地址的范围、长度、权限、文件等
struct vma {
int valid; // 该虚拟内存区域是否已被映射
uint64 vastart; // 该虚拟内存区域开始地址
uint64 sz; // 该虚拟内存区域大小
struct file* f; // 该虚拟内存区域映射的文件
int prot; // 该虚拟内存区域权限
int flags; // 标记映射内存的修改是否写回文件
uint64 offset; // 映射文件的起点
};
#define NVMA 16 // VMA数组大小
// Per-process state
struct proc {
struct spinlock lock;
......
struct vma vmas[NVMA]; // mmap虚拟内存映射地址数组
};
接下来实现mmap的系统调用。这个实验要做的事挺多,最后再添加系统调用的声明。参考Linux的mmap函数,需要在进程的vmas数组中遍历寻找空闲的vma,遍历的过程中也要计算当前正在使用的所有vma的最低地址,这是为了后面添加新的vma。找到空闲的vma后,设置他的地址为刚才找到的最低地址减去sz(因为mmap的地址是从高到低生长)。然后需要调用filedup
函数将映射文件的引用数+1
调用mmap函数的时候需要注意文件权限问题。如果文件不可读,vma映射为可读,则mmap失败;如果文件不可写,vma映射为可写,并且开启了回盘标志(MAP_SHARED),则mmap失败。据此写出mmap系统调用函数:
// kernel/sysfile.c
// mmap系统调用实现
uint sys_mmap(void) {
uint64 addr, sz, offset;
int prot, flag, fd;
struct file* f;
// 读取传入参数
if (argaddr(0, &addr) < 0 || argaddr(1, &sz) < 0 || argint(2, &prot) < 0 || argint(3, &flag) < 0 || argfd(4, &fd, &f) < 0 || argaddr(5, &offset) < 0 || sz == 0)
return -1;
// 以下情况直接返回-1:
if ((!f->readable && ((prot & (PROT_READ)))) // 源文件不可读,vma映射为可读
|| (!f->writable && (prot & PROT_WRITE) && !(flag & MAP_PRIVATE))) // 源文件不可写 ,vam映射为可写并且设置了将修改写回源文件
return -1;
sz = PGROUNDUP(sz);
struct proc* p = myproc();
struct vma* v = 0;
uint64 vaend = MMAPEND;
// 遍历查询未被使用的vma,并且计算当前已使用的va的最低地址
for (int i = 0;i < NVMA;++i) {
struct vma* vv = &p->vmas[i];
if (vv->valid == 0) { // 若找到了空闲vma就保存下来
if (v == 0) {
v = &p->vmas[i];
v->valid = 1;
}
}
else if (vv->vastart < vaend) {
vaend = PGROUNDDOWN(vv->vastart);
}
}
// 没找到空闲的vma
if (v == 0)
panic("mmap: no free vma");
// 设置vma属性
v->vastart = vaend - sz;
v->sz = sz;
v->f = f;
v->prot = prot;
v->flags = flag;
v->offset = offset;
// 增加源文件引用数
filedup(v->f);
return v->vastart;
}
kernel/fcntl.h
中定义好了相关宏,编译器提示未定义标识符可以不用管:
// kernel/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
映射功能使用写时复制机制实现,即只有在访问的时候才进行磁盘操作。具体原理参考Lab5:
// kernel/sysfile.c
// 通过虚拟地址寻到对应的vma
struct vma* findvma(struct proc* p, uint64 va) {
for (int i = 0;i < NVMA;++i) {
struct vma* vv = &p->vmas[i];
// 如果va地址在某一个vma范围内,则返回这个vma
if (vv->valid == 1 && va >= vv->vastart && va < vv->vastart + vv->sz) {
return vv;
}
}
return 0;
}
// 给虚拟地址分配物理页并建立映射
int vmaalloc(uint64 va) {
struct proc* p = myproc();
struct vma* v = findvma(p, va);
if (v == 0)
return 0;
// 分配物理地址
void* pa = kalloc();
if (pa == 0)
panic("vmaalloc:kalloc");
memset(pa, 0, PGSIZE);
// 从磁盘读取文件
begin_op();
ilock(v->f->ip);
readi(v->f->ip, 0, (uint64)pa, v->offset + PGROUNDDOWN(va - v->vastart), PGSIZE);
iunlock(v->f->ip);
end_op();
// 建立映射
if (mappages(p->pagetable, va, PGSIZE, (uint64)pa, PTE_R | PTE_W | PTE_U) < 0)
panic("vmaalloc: mappages");
return 1;
}
// kernel/trap.c
void
usertrap(void)
{
......
else if ((which_dev = devintr()) != 0) {
// ok
}
else if (r_scause() == 13 || r_scause() == 15){
uint64 va = r_stval(); // 读取当前发生页面错误的地址
if (vmaalloc(va) == 0)
panic("usertrap: wrong va");
}
......
}
接下来需要实现另一个系统调用munmap
,释放所有的vma。如果设置了MAP_SHARED,还需要将修改写回磁盘源文件。
munmap
传入的参数为释放映射的地址addr,释放地址的范围大小sz。需要检测释放的区域,取消映射的位置要么在区域起始位置,要么在区域结束位置,要么就是整个区域,但是不能在vma中间“打洞”。页有可能不是完整释放,如果 addr 处于一个页的中间,则那个页的后半部分释放,但是前半部分不释放,此时该页整体不应该被释放:
// kernel/sysfile.c
// 释放vma映射的页
uint64 sys_munmap(void) {
uint64 addr, sz;
if (argaddr(0, &addr) < 0 || argaddr(1, &sz) < 0 || sz == 0)
return -1;
struct proc* p = myproc();
struct vma* v = findvma(p, addr);
if (v == 0)
return -1;
if (addr > v->vastart && addr + sz < v->vastart + v->sz) // 释放的区域不能在vma中“打洞”
return -1;
uint64 addr_alinged = addr;
if (addr > v->vastart)
addr_alinged = PGROUNDUP(addr);
int nunmap = sz - (addr_alinged - addr); // 计算要释放的字节数
if (nunmap < 0)
nunmap = 0;
vmaunmap(p->pagetable, addr_alinged, nunmap, v); // 从addr_alinged开始释放nunmap字节数
if (addr <= v->vastart && addr + sz > v->vastart) {
v->offset += addr + sz - v->vastart;
v->vastart = addr + sz;
}
v->sz -= sz;
if (v->sz <= 0) {
fileclose(v->f);
v->valid = 0;
}
return 0;
}
vmaunmap
函数实现释放映射功能。释放映射之后,需要更新对应vma的offset、vastart、sz字段。如果释放完了vma的sz大小的范围,则应该关闭文件的引用,释放该vma。
vmaunmap
函数仿照uvmunmap
函数实现, 从传入的参数虚拟地址va开始遍历,查找va + nbytes范围内的每一个页,检查这个页是否被修改过,并且该vma设置了回盘MAP_SHARED,则需要把修改写回磁盘。注意不是每一个页都需要完整的写回,这里需要处理开头页不完整、结尾页不完整以及中间完整页的情况
先加上PTE_D标志位,表示页表被修改过:
// kernel/riscv.h
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_G (1L << 5)
#define PTE_A (1L << 6)
#define PTE_D (1L << 7) // 页表被修改过
实现vmaunmap
函数:
// kernel/vm.c
// 添加必要的头文件
#include "fcntl.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "proc.h"
// 释放mmap映射的页,根据PTE_D和MAP_SHARED判断是否将修改写回磁盘
void vmaunmap(pagetable_t pagetable, uint64 va, uint64 nbytes, struct vma* v) {
uint64 a;
pte_t* pte;
for (a = va;a < va + nbytes;a += PGSIZE) {
if ((pte = walk(pagetable, a, 0)) == 0) // 读取va对应pte
continue;
if (PTE_FLAGS(*pte) == PTE_V)
panic("sys_munmap: not a leaf");
if (*pte & PTE_V) {
uint64 pa = PTE2PA(*pte);
if ((*pte & PTE_D) && (v->flags & MAP_SHARED)) { // 将修改写回磁盘
begin_op();
ilock(v->f->ip);
uint64 aoff = a - v->vastart; // 相对于vma的偏移量
if (aoff < 0)
writei(v->f->ip, 0, pa + (-aoff), v->offset, PGSIZE + aoff); // 第一页是不满PGSIZE的一个页
else if (aoff + PGSIZE > v->sz)
writei(v->f->ip, 0, pa, v->offset + aoff, v->sz - aoff); // 最后一页是不满PGSIZE的一个页
else
writei(v->f->ip, 0, pa, v->offset + aoff, PGSIZE);
iunlock(v->f->ip);
end_op();
}
kfree((void*)pa);
*pte = 0;
}
}
}
在proc.c中需要添加对vma的处理
首先是初始化进程时,需要初始化一个进程的vmas数组:
// kernel/proc.c
static struct proc*
allocproc(void)
{
......
// 初始化时清空vmas数组
for (int i = 0;i < NVMA;++i)
p->vmas[i].valid = 0;
return p;
}
释放进程时,要在释放页表前清空vmas数组:
// kernel/proc.c
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
for (int i = 0;i < NVMA;++i) { // 释放页表前把vmas数组清空
struct vma* v = &p->vmas[i];
vmaunmap(p->pagetable, v->vastart, v->sz, v);
}
if (p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
......
}
fork创建子进程时,子进程复制父进程的vmas数组,不复制物理页:
// kernel/proc.c
int
fork(void)
{
......
// 父进程vmas复制到子进程中,实际内存页和pte不会被复制
for (int i = 0;i < NVMA;++i) {
struct vma* v = &p->vmas[i];
if (v->valid) {
np->vmas[i] = *v;
filedup(v->f);
}
}
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
np->state = RUNNABLE;
release(&np->lock);
return pid;
}
主体做好了,现在添加系统调用声明
user.h:
// mmaptest中调用mmap时需要返回char*,这里可以把返回值设置为void*
void* mmap(void* addr, uint sz, int prot, int flag, int fd, uint offset);
int munmap(void* addr, uint sz);
usys.pl:
entry("mmap");
entry("munmap");
syscall.h:
#define SYS_mmap 22
#define SYS_munmap 23
syscall.c:
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);
static uint64 (*syscalls[])(void) = {
......
[SYS_mmap] sys_mmap,
[SYS_munmap] sys_munmap,
};
此时可以验证实验是否通过
标签:uint64,sz,addr,映射,Mit6,mmap,vma,Lab10 From: https://www.cnblogs.com/Amroning/p/18555179