# MIT6.s081-2020 Lab10 Mmap

lab 10 文档 (opens new window)

在操作系统中,进程常需要通过文件存储的方式来持久化数据,然而,文件的读取速度要远小于内存的读取速度,因此,我们需要一种内存映射文件的方法,即将文件映射到进程地址空间,来更加高效的实现对文件数据的读写操作,并且可用于在进程之间共享内存。

# 准备

git fetch
git checkout mmap
make clean
1
2
3

# mmap 和 munmap

引言

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
1

你可以假设 addr将始终为零,这意味着内核应该决定映射文件的虚拟地址。 mmap返回该地址,如果失败则返回 0xfffffffffffffffflength是要映射的字节数;它可能与文件的长度不同。 prot指示内存是否应映射为可读、可写和/或可执行;你可以假设protPROT_READ要么PROT_WRITE 或两者。 flags将是MAP_SHARED,这意味着对映射内存的修改应该写回文件,或者MAP_PRIVATE,这意味着他们不应该。您不必在其中实现任何其他位flags. fd是要映射的文件的打开文件描述符。你可以假设offset为零(它是文件中映射的起点)。映射相同的进程没有MAP_SHARED 文件共享物理页面。

munmap则是上述操作的逆过程,将已经实现内存映射的文件上的脏页重新写回文件磁盘。即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

void munmap(void *addr, size_t length)
1

实现

本小节的目的是在给定在实验环境中实现 mmap()munmap()的系统调用。参考所给的HINT,即可顺利实现。

HINT1:在Makefile中将_mmaptestUPROGS,在顺序流程声明mmapmunmap系统调用函数,以保证测试程序顺利运行。

Makefile

UPROGS=\
//...
        $U/_mmaptest\
1
2
3

user/usys.pl

entry("uptime");
entry("mmap");
entry("munmap");
1
2
3

user/user.h

int uptime(void);
char* mmap(char*, int, int, int, int, int);
int munmap(char *, int);
1
2
3

kernel/syscall.h

#define SYS_close  21
#define SYS_mmap   22
#define SYS_munmap 23
1
2
3

kernel/syscall.c

//...
extern uint64 sys_uptime(void);
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);
static uint64 (*syscalls[])(void) = {
//...
[SYS_close]   sys_close,
[SYS_mmap]    sys_mmap,
[SYS_munmap]  sys_munmap,
};
1
2
3
4
5
6
7
8
9
10
11

HINT2:为每个进程映射的虚拟内存区域设计一个数据结构,该结构包含由创建的虚拟内存范围的地址、长度、权限、文件等。由于 xv6 内核在内核中没有内存分配器,因此可以声明一个固定大小的 VMA 数组并根据需要从该数组进行分配。

kernel/proc.h

#define VMASIZE 16
struct vma {
  struct file* file;
  uint64 addr;
  uint64 length;
  int offset;
  int permission;
  int flag;
  int used;
} ;
struct proc {
//...
  struct inode *cwd; // Current directory 
  char name[16];               // Process name (debugging)
  struct vma vmas[VMASIZE];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

[注]:为了在后续过程中能够正常的使用虚拟内存区域,需要为其声明和初始化。

kernel/defs.h

struct superblock;
struct vma;
1
2

kernel/proc.c

static struct proc*
allocproc(void)
{
//...
  // Set up new context to start executing at forkret,
  // which returns to user space.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;
  memset(&p->vmas, 0, sizeof(p->vmas));
  return p;
}
1
2
3
4
5
6
7
8
9
10
11
12

HINT3:实现mmap核函数,进程的地址空间中找到一个未使用的区域来映射文件,并将 VMA 添加到进程的映射区域表中。VMA 应该包含一个指向struct file对于被映射的文件;mmap应该增加文件的引用计数,以便在文件关闭时结构不会消失(提示:请参阅filedup)。

kernel/sysfile.c

uint64 sys_mmap(void) {
  struct proc* p = myproc();
  struct file* f;
  uint64 addrs;
  int prot, flag, len, offset;
  if(argaddr(0, &addrs) < 0 || argint(1, &len) < 0 || argint(2, &prot) < 0 ||
    argint(3, &flag) < 0 || argfd(4, 0 , &f) < 0 || argint(5, &offset) < 0)
    return -1;
  if(addrs != 0 || offset != 0 || len < 0)
    return -1;
  if(flag == MAP_SHARED && (prot & PROT_WRITE) && !f->writable) 
    return -1;
  if(p->sz + len > MAXVA)
    return -1;
  for(int i = 0; i < VMASIZE; ++i) {
    if(p->vmas[i].used == 0) {
      uint64 addr = p->sz;
      p->sz += len;
      filedup(f);
      p->vmas[i].file = f;
      p->vmas[i].addr = addr;
      p->vmas[i].length = len;
      p->vmas[i].offset = offset;
      p->vmas[i].permission = prot;
      p->vmas[i].flag = flag;
      p->vmas[i].used = 1;
      return addr;
    }
  }
  return -1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

[注]mmap() 映射的页面应该是 lazy alloc 的,以保证在映射大文件时不会阻塞。

kernel/vm.c

void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
//...    
    if((pte = walk(pagetable, a, 0)) == 0)
      panic("uvmunmap: walk");
    if((*pte & PTE_V) == 0)
      //panic("uvmunmap: not mapped");
      continue;
}
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
//...
     if((pte = walk(old, i, 0)) == 0)
      panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      //panic("uvmcopy: page not present");
      continue; 
//...      
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

HINT4:修改usertrap,以处理缺页中断。在缺页中断中读入 对应位置的4096字节 文件放入内存并映射

  } else if((which_dev = devintr()) != 0){
    // ok
  } else if(r_scause() == 13 || r_scause() == 15) { 
    uint64 addr = r_stval();
    struct vma pvma;
    int i;
    for(i = 0; i < VMASIZE; i++) {
      pvma = p->vmas[i];
      if(pvma.used && addr >= pvma.addr && addr < pvma.addr + pvma.length) {
        break;
      }
    }
    if(i != VMASIZE) {
      struct file* f = pvma.file;
      uint64 pg = PGROUNDDOWN(addr);
      uint64 offset = pg - pvma.addr;
      char* mem = kalloc();
      if(mem == 0) {
        p->killed = 1;
      } else {
	memset(mem, 0, PGSIZE);
        ilock(f->ip);
	readi(f->ip, 0, (uint64)mem, offset, PGSIZE);
	iunlock(f->ip);
        int flag = PTE_U;
	if(pvma.permission & PROT_READ) 
	  flag |= PTE_R;
	if(pvma.permission & PROT_WRITE)
	  flag |= PTE_W;
	if(pvma.permission & PROT_EXEC)
	  flag |= PTE_X;
	if(mappages(p->pagetable, pg, PGSIZE, (uint64)mem, flag) != 0) {
	  kfree(mem);
	  p->killed = 1;
	}
      }
    } else {
      p->killed = 1;
    } 
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

HINT5:实现munmap函数,找到地址范围的 VMA 并取消映射指定的页面(提示:使用uvmunmap)。如果munmap删除前一个页面的所有页面mmap,它应该减少相应的引用计数struct file. 如果修改了未映射的页面并且映射了文件MAP_SHARED,将页面写回文件。

kernel/sysfile.c

uint64 sys_munmap(void) {
  uint64 addr;
  int length;
  if(argaddr(0, &addr) < 0 || argint(1, &length) < 0) {
    return -1;
  }
  struct proc* p = myproc();
  int i;
  for(i = 0; i < VMASIZE; ++i) {
    if(p->vmas[i].used && p->vmas[i].length >= length) {
      // 根据提示,munmap的地址范围只能是
      // 1. 起始位置
      if(p->vmas[i].addr == addr) {
        p->vmas[i].addr += length;
        p->vmas[i].length -= length;
        break;
      }
      // 2. 结束位置
      if(addr + length == p->vmas[i].addr + p->vmas[i].length) {
        p->vmas[i].length -= length;
        break;
      }
    }
  }
  if(i == VMASIZE) {
    return -1;
  }
  struct file* f = p->vmas[i].file;
  //uint64 offset = pvma->offset + addr - pvma->addr;
  if(p->vmas[i].flag == MAP_SHARED && (p->vmas[i].permission & PROT_WRITE) != 0)
    filewrite(f, addr, length);
  uvmunmap(p->pagetable, addr, PGROUNDUP(length) / PGSIZE, 1);
  // 当前VMA中全部映射都被取消
  if(p->vmas[i].length == 0) {
    fileclose(f);
    p->vmas[i].used = 0;
  }
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

HINT6:修改exit,使其能够取消映射进程的映射区域,就像调用munmap一样。

kernel/proc.c

void
exit(int status)
{
//...
  for(int i = 0; i < VMASIZE; ++i) {
    if(p->vmas[i].used) {
      if(p->vmas[i].flag == MAP_SHARED && (p->vmas[i].permission & PROT_WRITE) != 0)
        filewrite(p->vmas[i].file, p->vmas[i].addr, p->vmas[i].length);
      uvmunmap(p->pagetable, p->vmas[i].addr, PGROUNDUP(p->vmas[i].length) / PGSIZE, 1);
      fileclose(p->vmas[i].file);
      p->vmas[i].used = 0;
    }
  }
  begin_op();
  iput(p->cwd);
  end_op();
  p->cwd = 0;
//...    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

HINT7:修改fork以确保子级具有与父级相同的映射区域。不要忘记增加 VMA 中struct file的引用计数。

kernel/proc.c

int
fork(void)
{
//...
  np->cwd = idup(p->cwd);
  for(i = 0; i < VMASIZE; ++i) {
    if(p->vmas[i].used) {
      memmove(&np->vmas[i], &p->vmas[i], sizeof(p->vmas[i]));
      filedup(p->vmas[i].file);
    }
  }
  safestrcpy(np->name, p->name, sizeof(p->name));
//...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

结果

启动xv6,运行mmaptest测试程序,即可得到如下通过结果。

image-lab10-1.jpg

运行usertests测试程序,得到如下结果:

image-lab10-2.jpg

运行make grade,可以通过所有的测试用例。

image-lab10-3.jpg

# 总结

总而言之,mmap实验也是虚拟内存的应用,刚开始觉得很难,不知道从何下手,因为要考虑的地方很多,比如映射的地址区域、如何组织管理VMA、映射的长度不为PGSIZE的倍数、父子进程需不需要共享物理内存等等,后来跟着lab的hint一步一步来,发现并没有那么复杂,很多都不需要考虑,然后测试也比较弱。