解开我
最神秘的等待
星星坠落
风在吹动
——美丽的神话
完整代码见:6.s081/kernel at mmap · SnowLegend-star/6.s081 (github.com)
这个lab其实是结合了COW、Lazy allocation还有fs三个实验为一体。虽然思维难度不像一般的hard那么大,但胜在代码量极多,需要修改的地方也纷繁复杂,故我完成得磕磕绊绊。由于这个lab的主体部分是重新完成mmap()函数,所以先看看标准库里面mmap()的分析。
头文件:
<unistd.h>
<sys/mman.h>
原型: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offsize);
返回值: 成功则返回映射区起始地址, 失败则返回MAP_FAILED(-1).
参数:
addr: 指定映射的起始地址, 通常设为NULL, 由系统指定.
length: 将文件的多大长度映射到内存.
prot: 映射区的保护方式, 可以是:
PROT_EXEC: 映射区可被执行.
PROT_READ: 映射区可被读取.
PROT_WRITE: 映射区可被写入.
PROT_NONE: 映射区不能存取.
flags: 映射区的特性, 可以是:
MAP_SHARED: 对映射区域的写入数据会复制回文件, 且允许其他映射该文件的进程共享.
MAP_PRIVATE: 对映射区域的写入操作会产生一个映射的复制(copy-on-write), 对此区域所做的修改不会写回原文件.
此外还有其他几个flags不很常用, 具体查看linux C函数说明.
fd: 由open返回的文件描述符, 代表要映射的文件.
offset: 以文件开始处的偏移量, 必须是分页大小的整数倍, 通常为0, 表示从文件头开始映射.
内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,相反,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<---->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。
了解mmap()的工作原理之后,仔细阅读实验说明。
可以通过多种方式调用mmap,但本实验只需要与内存映射文件相关的功能子集。您可以假设addr始终为零,这意味着内核应该决定映射文件的虚拟地址。mmap返回该地址,如果失败则返回0xffffffffffffffff。length是要映射的字节数;它可能与文件的长度不同。prot指示内存是否应映射为可读、可写,以及/或者可执行的;您可以认为prot是PROT_READ或PROT_WRITE或两者兼有。flags要么是MAP_SHARED(映射内存的修改应写回文件),要么是MAP_PRIVATE(映射内存的修改不应写回文件)。您不必在flags中实现任何其他位。fd是要映射的文件的打开文件描述符。可以假定offset为零(它是要映射的文件的起点)。
允许进程映射同一个MAP_SHARED文件而不共享物理页面。
这是什么意思呢?
问了下GPT,回答如下
You
“允许进程映射同一个MAP_SHARED文件而不共享物理页面。”这是什么意思呢
ChatGPT
这句话的意思是,即使多个进程都映射了同一个文件,并且都使用了 MAP_SHARED 标志,但操作系统并不会强制这些进程共享同一个物理页面。换句话说,每个进程会得到自己的一组物理页面来映射这个文件,而不是所有进程共享同一个物理页面。
这种情况可能会发生在操作系统对 MAP_SHARED 映射的处理方式上,通常情况下,操作系统会在内核中维护一张页面表,将不同进程的虚拟地址映射到相同的物理页面上,以实现对文件的共享访问。然而,如果操作系统允许进程映射同一个 MAP_SHARED 文件而不共享物理页面,那么即使多个进程都对文件进行了映射,并且使用了 MAP_SHARED 标志,它们在内存中的映射可能是相互独立的,各自占用不同的物理页面。
这种情况可能会增加系统的内存开销,因为每个进程都会分配自己的物理页面来映射同一个文件,但也有助于提高系统的安全性,因为各个进程之间不会共享物理页面,从而避免了可能的安全隐患。
munmap(addr, length)应删除指定地址范围内的mmap映射。如果进程修改了内存并将其映射为MAP_SHARED,则应首先将修改写入文件。munmap调用可能只覆盖mmap区域的一部分,但您可以认为它取消映射的位置要么在区域起始位置,要么在区域结束位置,要么就是整个区域(但不会在区域中间“打洞”)。
[!TIP|label:YOUR JOB] 您应该实现足够的mmap和munmap功能,以使mmaptest测试程序正常工作。如果mmaptest不会用到某个mmap的特性,则不需要实现该特性。
Hints:
1、首先,向UPROGS添加_mmaptest,以及mmap和munmap系统调用,以便让user/mmaptest.c进行编译。现在,只需从mmap和munmap返回错误。我们在kernel/fcntl.h中为您定义了PROT_READ等。运行mmaptest,它将在第一次mmap调用时失败。
要手动给用户空间添加mmap和munmap的系统调用吗?对的,就像Lab: fs一样。
2、惰性地填写页表,以响应页错误。也就是说,mmap不应该分配物理内存或读取文件。相反,在usertrap中(或由usertrap调用)的页面错误处理代码中执行此操作,就像在lazy page allocation实验中一样。惰性分配的原因是确保大文件的mmap是快速的,并且比物理内存大的文件的mmap是可能的。
处理完trap.c函数后,对uvmcopy和uvmunmap进行painc的修改。这里对usertrap()的修改可以参照Lazy allocation,流程大致如下:
- 获取出错的addr
- 判断addr的合法性
- 找到出错addr属于哪块VMA元素
- 分配pa并设置perm
- 进行addr和pa的映射
- 用readi将映射的文件读入pa中(记得加锁操作)
这里要注意是用readi将文件读入pa中而不是addr,否则会出现以下报错
3、跟踪mmap为每个进程映射的内容。定义与第15课中描述的VMA(虚拟内存区域)对应的结构体,记录mmap创建的虚拟内存范围的地址、长度、权限、文件等。由于xv6内核中没有内存分配器,因此可以声明一个固定大小的VMA数组,并根据需要从该数组进行分配。大小为16应该就足够了。
在什么地方声明这个数组呢?就在proc.h中进行声明。
但是怎么记录这个文件呢?用argfd的方式把读入的fd转换为struct file格式。
一共有64个进程,却只有16个VMA数组,说明不是每个进程都会调用mmap进行映射。不是用含有16个元素的VMA数组来管理所有进程,而是给每个进程都分配一个VMA数组,故每个进程都可以打开的文件上限是16。当前进程结束后记得重置VMA元素。
4、实现mmap:在进程的地址空间中找到一个未使用的区域来映射文件,并将VMA添加到进程的映射区域表中。VMA应该包含指向映射文件对应struct file的指针;mmap应该增加文件的引用计数,以便在文件关闭时结构体不会消失(提示:请参阅filedup)。运行mmaptest:第一次mmap应该成功,但是第一次访问被mmap的内存将导致页面错误并终止mmaptest。
该怎么从进程的地址空间中找一块未使用过的addr呢?遍历pagetable也许可行。也可以直接在进程的地址空间内位于p->sz的位置开始映射,但是如果进程进行了sbrk()会导致下一个VMA元素和当前VMA元素存储的位置不是连续的(从实现角度上没有问题)。
一个新的想法是从虚拟地址的高处开始映射,例如从trapframe-10*PAGESIZE开始映射,但这样会出现bug。
可能是高地址空间位于内核,用户空间不允许访问内核数据。
5、添加代码以导致在mmap的区域中产生页面错误,从而分配一页物理内存,将4096字节的相关文件读入该页面,并将其映射到用户地址空间。使用readi读取文件,它接受一个偏移量参数,在该偏移处读取文件(但必须lock/unlock传递给readi的索引结点)。不要忘记在页面上正确设置权限。运行mmaptest;它应该到达第一个munmap。
这里也提到了在usertrap()中,是用readi读入文件内容到物理页面中,同时读入的字节大小为4096(即PGSIZE)。
6、实现munmap:找到地址范围的VMA并取消映射指定页面(提示:使用uvmunmap)。如果munmap删除了先前mmap的所有页面,它应该减少相应struct file的引用计数。如果未映射的页面已被修改,并且文件已映射到MAP_SHARED,请将页面写回该文件。查看filewrite以获得灵感。
在munmap()的实现中,只有文件的flags是MAP_SHARED才需要重新写入到文件中,让所有共享这个文件的进程都能看到修改。MAP_SHARED类型的文件则不需要。同时,这个文件本身应该是writeable的。
写回文件的过程可以模仿file.c中的filewrite()。
7、理想情况下,您的实现将只写回程序实际修改的MAP_SHARED页面。RISC-V PTE中的脏位(D)表示是否已写入页面。但是,mmaptest不检查非脏页是否没有回写;因此,您可以不用看D位就写回页面。
意思是只要修改了就直接写回,其实可以忽略这个hint。
8、修改exit将进程的已映射区域取消映射,就像调用了munmap一样。运行mmaptest;mmap_test应该通过,但可能不会通过fork_test。
freeproc不是会进行取消映射的处理吗?这里就是在exit()中额外增加对进程VMA数组的释放过程。
9、修改fork以确保子对象具有与父对象相同的映射区域。不要忘记增加VMA的struct file的引用计数。在子进程的页面错误处理程序中,可以分配新的物理页面,而不是与父级共享页面。后者会更酷,但需要更多的实施工作。运行mmaptest;它应该通过mmap_test和fork_test。
是不是和Lab: Lazy alloctaion差不多?naive方案是直接把父进程的VMA数组复制给子进程的VMA。Challenge部分才和Lazy allocation差不多。
proc.h
#define VMASIZE 16
struct VMA{
int valid; //当前VMA的元素是否被使用
uint64 addr;
int length;
int prot;
int flags;
int fd;
int offset;
struct file *f;
};
sys_mmap()
uint64
sys_mmap(){
int fd, length, prot, flags, offset;
uint64 addr;
struct file *f;
struct proc *p=myproc();
int i;
//读入mmap需要的六个参数
if(argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0 ||
argint(3, &flags) < 0 || argfd(4, &fd, &f) < 0 || argint(5, &offset) < 0)
return -1;
// f=p->ofile[fd];
if( !(f->writable) && (prot & PROT_WRITE) && (flags == MAP_SHARED))
return -1;
for(i=0; i < VMASIZE; i++){
if(p->VMA[i].valid==0){ //默认值0代表可用
// p->VMA[i].addr=MMAPADDR + PGSIZE*i*3; //把第i个VMA元素映射到对应的位置
p->VMA[i].addr=p->sz;
p->VMA[i].length=length;
p->VMA[i].prot=prot;
p->VMA[i].flags=flags;
p->VMA[i].fd=fd;
p->VMA[i].offset=offset;
p->VMA[i].f=filedup(f);
p->VMA[i].valid=1;
break;
}
}
if(i == VMASIZE) //当前进程没有空闲的VMA元素了
return -1;
p->sz+=length;
return p->VMA[i].addr;
}
sys_munmap()
uint64
sys_munmap(){
uint64 addr;
int length;
if(argaddr(0, &addr) < 0 || argint(1, &length) < 0)
return -1;
if(addr%PGSIZE || length<0 )
return -1;
return (uint64)munmap_Real(addr, length);
}
由于 exit()也要用到和原始munmap()相同的部分,故抽象出函数munmap_Real()
int munmap_Real(uint64 addr, int length){
int i;
struct VMA *vma=0;
struct proc *p=myproc();
//寻找对应的VMA
for(i=0; i<VMASIZE; i++){
vma=&p->VMA[i];
if(p->VMA[i].valid==1 && addr>= p->VMA[i].addr && addr+length < p->VMA[i].addr + p->VMA[i].length){
break;
}
// vma = p->VMA + i;
// if (vma->valid == 1 && addr >= vma->addr && (addr + length) < (vma->addr + vma->length)) {
// break;
// }
}
if(i>VMASIZE)
return -1;
//将数据写回shared类型的文件中
uint64 begin=addr;
uint64 end=addr+length;
if(vma->flags==MAP_SHARED && vma->f->writable){
uint64 addr_Cur=begin;
while(addr_Cur < end){
int sz=end-addr_Cur>=PGSIZE?PGSIZE:end-addr_Cur; //这里把sz的大小限制为PGSIZE是为了好调用uvmunmap
begin_op();
ilock(vma->f->ip);
if(writei(vma->f->ip, 1, addr_Cur, addr_Cur-vma->addr,sz) != sz)
return -1;
iunlock(vma->f->ip);
end_op();
uvmunmap(p->pagetable, addr_Cur,1,1);
addr_Cur+=PGSIZE;
}
}
//写回结束后,更新vma的内容
if(addr==vma->addr){ //说明addr是mmap的头部
vma->addr+=length;
vma->length-=length;
}
else if(addr+length==vma->addr+vma->length){ //说明释放了vma的尾部
vma->length-=length;
}
//如果vma全部被释放,则要释放对file的引用
if(vma->length ==0 && vma->valid==1){
// fileclose(vma->f);
filedup(vma->f);
vma->valid=0;
vma->addr=0;
vma->f=0;
vma->fd=-1;
vma->flags=0;
vma->length=0;
vma->offset=0;
vma->prot=0;
}
return 0;
}
usertrap()的修改如下
else if(r_scause() == 13 || r_scause()==15){
uint64 pa;
uint64 addr=PGROUNDDOWN(r_stval());
struct VMA *vma=0;
int i;
if(addr >= p->sz || addr < PGROUNDDOWN(p->trapframe->sp))
exit(-1);
//找到出错的VMA
for(i=0;i<VMASIZE;i++)
if(p->VMA[i].addr && p->VMA[i].addr <= addr && p->VMA[i].addr + p->VMA[i].length >addr){
vma=&p->VMA[i];
}
if(i>VMASIZE) //没有找到合适的VMA元素,直接退出
exit(-1);
//找到了vma,说明是mmap导致的缺页错误
if((pa=(uint64)kalloc())==0)
exit(-1);
memset((char*)pa, 0, PGSIZE);
int perm = PTE_U;
if(vma->prot & PROT_READ)
perm |= PTE_R;
if(vma->prot & PROT_WRITE)
perm |= PTE_W;
if(vma->prot & PROT_EXEC)
perm |= PTE_X;
if((mappages(p->pagetable, addr, PGSIZE, (uint64) pa, perm)) < 0){
exit(-1);
}
//将映射的文件读入pa中
ilock(vma->f->ip);
readi(vma->f->ip, 0, pa, addr - vma->addr, PGSIZE);
iunlock(vma->f->ip);
}
exit()
......
p->ofile[fd] = 0;
}
}
//释放进程的VMA
for(int i=0; i<VMASIZE; i++){
if(p->VMA[i].valid==0)
continue;
if(munmap_Real(p->VMA[i].addr, p->VMA[i].length)!=0){
panic("Something wrong with munmap when exit.");
}
}
begin_op();
iput(p->cwd);
fork()
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
//拷贝父进程的VMA给子进程
for(int i=0; i<VMASIZE; i++){
if(p->VMA[i].valid==1){
memmove(&np->VMA[i], &p->VMA[i], sizeof(struct VMA));
filedup(np->VMA[i].f);
}
}
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
np->state = RUNNABLE;
在完成lab的过程中还碰到了其他bug,例如
熟悉的stacktest error,猜测是没判断addr的合法性(即addr下溢到了guard page)。修改下判断即可通过。
下面这段玄学代码也带了了几个令人困惑的bug。
说实话这次lab虽然巨麻烦,不过测试条件是真的宽松。甚至在munmap()中把fileclose()写成了filedup()还是通过了测试。总而言之,涉及fs的lab内容实在是多,看得人头大。