mmap
简介
切换到 mmap 分支。
实现 mmap
以及 munmap
系统调用,mmap
的函数声明如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
mmap的用处很多,当前实验仅关注内存映射文件的功能。
addr
参数表示映射的虚拟地址的起始,也可以指定为0,此时内核将会决定映射文件的起始虚拟地址,mmap
成功后会返回该地址,失败则返回0xffffffffffffffff
。length
参数表示要映射的字节,可以与文件的长度不同prot
参数表示内存是否映射为可读、可写、可读写、可执行等,可以假设prot
为PROT_READ
或者PROT_WRITE
或者两者flags
参数只能为MAP_SHARED
或者MAP_PRIVATE
,前者表示对于映射内存的修改需要写回内存,后者表示不应该写回fd
表示要映射的已打开文件的描述符offset
参数表示从文件的哪里开始映射,可以假设该参数为0
munmap(addr, length)
应删除指定地址范围内的mmap
映射,如果进程修改了内存且映射方式为MAP_SHARED
,则应首先将修改写回到文件。munmap
可能只覆盖 mmap
区域的一部分,可以假设它会在开始、结束或整个区域取消映射,而不会在中间打洞。
目标:实现 mmap
和 munmap
函数并通过 mmaptest
测试
提示
kernel/fcntl.h
中定义了PROT_READ
相关的宏mmap
不应分配物理内存或读取文件,而应该在映射文件发生读写时通过page fault
的页错误处理代码中实现页面的申请及内容修改,这样做的原因是保证大文件的mmap
速度,大于物理内存的文件mmap
也是可行的- 定义一个
struct vma
,用于记录mmap
创建的虚拟内存的地址、长度、权限以及对应的文件等信息。由于xv6内核中没有内存分配器,因此可以声明固定大小的vma
数组并根据需要从该数组中分配vma
,大小 16 就可以了。 - 实现mmap:在进程的地址空间中找到一块未使用的区域来映射文件,并将
vma
添加到进程的映射区域表中。vma
应该包含一个指向被映射文件的文件指针(struct file
),mmap
还需要增加该文件的引用计数,以防止当文件被关闭时,该结构体失效。 - 添加页错误的处理代码,当mmap区域发生page fault时,分配一个4KB的页,并从文件中读取4KB填充到该页,需要记录文件的读取位置。
RISC-V PTE
中的dirty bit (D)
标记了当前该页是否被修改过- Modify
exit
to unmap the process’s mapped regions as if munmap had been called. - Modify
fork
to ensure that the child has the same mapped regions as the parent. Don’t forget to increment the reference count for a VMA’s struct file. In the page fault handler of the child, it is OK to allocate a new physical page instead of sharing a page with the parent.
实验代码
- kernel/param.h
#define NVMA 16 // 每个进程最大的VMA区域数量
- kernel/proc.h
struct vma {
int valid;
uint64 addr; //起始虚拟地址
int length; //映射区域的长度
int prot;
int flags;
struct file *mapfile;
};
struct proc {
// ...
// mmap lab
struct vma vmas[NVMA]; // Process virtual memory area
};
- kernel/proc.c
需要注意,exit
退出的时候由于mmap
映射的vma
区域有的地方没有被写过,因此没有触发 page fault
,所以这些虚拟地址没有关联物理地址。如果不改动 uvmunmap
函数,则在解除该虚拟地址和物理地址的映射时,由于该虚拟地址对应的页框没有分配物理地址,PTE_V
标志为0,会导致panic
。所以,在exit
中添加了虚拟地址是否关联物理地址的判断
#include "fcntl.h"
// 创建进程时初始化vma区域
static struct proc*
allocproc(void)
{
struct proc *p;
// ...
found:
p->pid = allocpid();
// 新增代码
for (int i = 0; i < NVMA; i++) {
p->vmas[i].valid = 0;
}
// ...
return p;
}
// 创建子进程时拷贝mmap区域
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// ...
np->sz = p->sz;
// 拷贝vma区域到子进程
for (int i = 0; i < NVMA; i++) {
if (p->vmas[i].valid) {
np->vmas[i].valid = 1;
np->vmas[i].addr = p->vmas[i].addr;
np->vmas[i].length = p->vmas[i].length;
np->vmas[i].prot = p->vmas[i].prot;
np->vmas[i].flags = p->vmas[i].flags;
np->vmas[i].mapfile = p->vmas[i].mapfile;
filedup(np->vmas[i].mapfile); // 增加引用计数
}
}
// ...其他代码
}
// exit退出时,解除当前进程所有的mmap,需要注意有的vma没有被写过,没有触发page fault,因此需要跳过
void
exit(int status)
{
struct proc *p = myproc();
pte_t *pte;
// munmap所有映射
for (int i = 0; i < NVMA; i++) {
if (p->vmas[i].valid) {
if (p->vmas[i].flags & MAP_SHARED) {
// 如果是 MAP_SHARED,将修改写回
filewrite(p->vmas[i].mapfile, p->vmas[i].addr, p->vmas[i].length);
}
// 解除虚拟地址和物理页的映射
uint64 a;
uint64 addr = p->vmas[i].addr;
for(a = addr; a < addr + p->vmas[i].length; a += PGSIZE){
if((pte = walk(p->pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
if((*pte & PTE_V) == 0)
continue;
// 关联了物理页,执行uvmunmap
uvmunmap(p->pagetable, a, 1, 1);
}
// uvmunmap(p->pagetable, p->vmas[i].addr, p->vmas[i].length/PGSIZE, 1);
// 引用计数减一
fileclose(p->vmas[i].mapfile);
}
}
}
- makefile
$U/_mmaptest\
- user/usys.pl
entry("mmap");
entry("munmap");
- user/user.h
void *mmap(void *addr, uint length, int prot, int flags,
int fd, uint offset);
int munmap(void *addr, uint len);
- 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,
};
- kernel/sysfile.c
同exit
函数,在sys_unmap
函数中也需要添加同样的虚拟地址是否关联物理地址的判断
uint64
sys_mmap(void)
{
uint64 addr;
int length, prot, flags, fd, i;
struct proc* p;
argint(1, &length);
argint(2, &prot);
argint(3, &flags);
argint(4, &fd);
if (length < 0 || prot < 0 || flags < 0 || fd < 0)
return -1;
p = myproc();
struct file *mapfile = p->ofile[fd];
if ((!mapfile->writable)&&(prot&PROT_WRITE)&&(!(flags&MAP_PRIVATE)))
return -1;
for (i = 0; i < NVMA; i++) {
// 分配一个新的vma区域
if (p->vmas[i].valid == 0) {
p->vmas[i].valid = 1;
p->vmas[i].addr = addr = p->sz;
p->vmas[i].length = length;
p->vmas[i].prot = prot;
p->vmas[i].flags = flags;
p->vmas[i].mapfile = mapfile;
filedup(p->ofile[fd]); // 文件的引用计数加一
break;
}
}
// have no vma slot
if (i == NVMA) {
return -1;
}
p->sz += length; // lazy mapping
return addr;
}
uint64
sys_munmap(void)
{
uint64 addr;
int length, i;
struct proc* p;
pte_t *pte;
argaddr(0, &addr);
argint(1, &length);
if (addr < 0 || length < 0)
return -1;
p = myproc();
for (i = 0; i < NVMA; i++) {
if (p->vmas[i].valid == 1) {
if (p->vmas[i].addr <= addr && (p->vmas[i].addr + p->vmas[i].length) > addr)
break;
}
}
// have no vma slot
if (i == NVMA) {
return -1;
}
struct vma *vmap = &p->vmas[i];
if (vmap->flags & MAP_SHARED) {
filewrite(vmap->mapfile, addr, length);
}
// 同理,需要判断虚拟地址是否关联物理地址
if (vmap->addr == addr && vmap->length == length) {
// unmap the whole vma
// 先判断哪些虚拟地址已经关联了物理页,因为有的va可能还没被写,没有触发缺页中断
uint64 a;
int i = length / PGSIZE;
for(a = addr; a < addr + i*PGSIZE; a += PGSIZE){
if((pte = walk(p->pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
if((*pte & PTE_V) == 0)
continue;
// 关联了物理页,执行uvmunmap
uvmunmap(p->pagetable, a, 1, 1);
}
fileclose(vmap->mapfile);
vmap->valid = 0;
} else if (vmap->addr == addr) {
// unmap from the beginning
// 同理,需要判断是否关联物理地址
uint64 a;
int i = length / PGSIZE;
for(a = addr; a < addr + i*PGSIZE; a += PGSIZE){
if((pte = walk(p->pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
if((*pte & PTE_V) == 0)
continue;
// 关联了物理页,执行uvmunmap
uvmunmap(p->pagetable, a, 1, 1);
}
vmap->addr += length;
vmap->length -= length;
} else if (vmap->addr + vmap->length == addr + length){
// unmap from the end
uint64 a;
int i = length / PGSIZE;
for(a = addr; a < addr + i*PGSIZE; a += PGSIZE){
if((pte = walk(p->pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
if((*pte & PTE_V) == 0)
continue;
// 关联了物理页,执行uvmunmap
uvmunmap(p->pagetable, a, 1, 1);
}
vmap->length -= length;
}
return 0;
}
- kernel/trap.c
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "fcntl.h"
void
usertrap(void)
{
// ...
if(r_scause() == 8){
// ...
} else if((which_dev = devintr()) != 0){
// ok
} else if (r_scause() == 13 || r_scause() == 15) {
// page fault (write or read)
uint64 va = r_stval();
if (va >= p->sz || va < p->trapframe->sp) {
// 如果进程读写的虚拟地址不在堆栈之间,报错,因为mmap映射的区域为堆和栈中间未使用的区域
p->killed = 1;
} else {
int i;
// check if the pagefault page is in one virtual memory area
for (i = 0; i < NVMA; i++) {
if (p->vmas[i].valid) {
if (p->vmas[i].addr <= va && (p->vmas[i].addr + p->vmas[i].length) > va)
break;
}
}
if (i == NVMA) {
// not in any vma
p->killed = 1;
} else {
// allocate page
uint64 ka = (uint64) kalloc();
if (ka == 0){
p->killed = 1;
} else {
// printf("access va %d\n", va);
// printf("sz : %d\n", p->sz);
// printf("addr : %d\n", p->vmas[i].addr);
memset((void *)ka, 0, PGSIZE);
va = PGROUNDDOWN(va);
ilock(p->vmas[i].mapfile->ip);
readi(p->vmas[i].mapfile->ip, 0, ka, va - p->vmas[i].addr, PGSIZE);
iunlock(p->vmas[i].mapfile->ip);
uint64 pm = PTE_U;
if (p->vmas[i].prot & PROT_READ)
pm |= PTE_R;
if (p->vmas[i].prot & PROT_WRITE)
pm |= PTE_W;
if(mappages(p->pagetable, va, PGSIZE, ka, pm) != 0) {
kfree((void *)ka);
p->killed = 1;
}
}
}
}
} 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;
}
// ...
}
此时可以通过 mmaptest
的前半部分,而forktest
会失败。这是因为forktest
中父进程会fork
一个子进程验证mmap区域
是否复制,验证完成子进程退出后,父进程通过wait
等待子进程退出信号,并在收到信号后清理子进程的所有资源。此时会调用freeproc
清理子进程,该函数会通过proc_freepagetable
函数从虚拟地址 0 到 p->sz(sz记录了子进程已分配的虚拟地址的最大值(要求vm连续分配))
,将这两个值作为参数调用 uvmunmap
。
因此,这里假定了从 0 到 p->sz 范围内的va都关联了pa
,否则会由于前面所述原因而panic
。而我们在前面实现为mmap区域分配va的逻辑借助了p->sz
,即每次从 p->sz的位置开始往后分配 length 长度,然后 p->sz += length
,这就导致xv6原先保证的 0 到 p->sz 区域va都关联 pa不成立了。所以,我们要不另外选择 mmap 区域的va分配算法
,要不更改 uvmunmap 函数
,使得该函数在检测到 va 没有关联 pa
时,不要panic
,而是跳过(这里时间原因,选择这种,正规来说应该重新选择va分配算法)
同理,还有创建子进程时使用的 uvmcopy
函数,因此我们都需要进行修改
- 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");
if((*pte & PTE_V) == 0)
continue; //当检测到 va 没有关联 pa时,跳过,用于处理父进程释放子进程资源的情况(freeproc函数的调用链)
//panic("uvmunmap: not mapped");
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
char *mem;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
continue;
//panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
if((mem = kalloc()) == 0)
goto err;
memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
kfree(mem);
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
实验结果
-
mmaptest
-
usertests -q