[mit6.1810]Lab9: mmap

【写在前面】

这是2023-2024春季学期操作系统课程有关xv6 lab部分的实验报告,参考了很多网络资源,解释也不一定正确,仅作为留档,参考需谨慎。

Lab 地址:6.1810 / Fall 2024

参考:

yali-hzy/xv6-labs-2024: MIT 6.1810 assignments

[mit6.s081] 笔记 Lab10: Mmap | 文件内存映射 | Miigon’s blog

实验任务

本实验需要实现 UNIX 的 mmapmunmap 系统调用。此系统调用会把文件映射到用户空间的内存,这样用户可以直接通过内存来修改和访问文件。

实验手册中给出了mmap() 的定义声明:

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

映射描述符为 fd 的文件的前 length 个字节到 addr 开始的位置。并且加上 offset 的偏移量。如果 addr 参数为 0,系统会自动分配一个空闲的内存区域来映射,成功返回这个地址,如果失败则返回 0xffffffffffffffff。实验需要支持 addroffset 都为 0 的情况,不用考虑用户指定内存和文件偏移量。

protflags 是标志位。prot 有可读/可写/可执行选项,规定了能对映射后文件做的操作。实验假设 protPROT_READPROT_WRITE 或两者。

flags 则决定如果在内存映射文件中做了修改,是否要在取消映射时,把这些修改更新到文件中。实验只需要实现 MAP_SHARED(映射内存的修改应写回文件) 和 MAP_PRIVATE (不应写回)两个选项。

手册中unmap() 的定义声明:

1
int munmap(void *addr, size_t length);

意思是取消从 addr 开始的,长度为 length 的文件映射。

实验思路

  1. 添加 mmapmunmap 系统调用,以便 user/mmaptest.c 可以编译。
  2. 根据提示“Define a structure corresponding to the VMA (virtual memory area) described in the “virtual memory for applications” lecture. This should record the address, length, permissions, file, etc…”,定义VMA结构体,包含 mmap 映射的内存区域的各种必要信息,比如开始地址、大小、所映射文件、文件内偏移以及权限等。在 proc 结构体末尾为每个进程加上 16 个 vma 空槽,并添加一个变量用于存储映射 mmap 页的顶部地址。
  3. kernel/sysfile.c中添加mmap的系统调用实现:在16个vma槽中找到空槽,并将一个vam添加到进程的映射区域表中。在最后根据提示使用 filedup()将文件的引用计数增加一。
  4. usertrap()中修改代码处理在内存映射区域中触发缺页错误时,分配一页可用的物理内存,将相关文件的 4096 字节读入该页,并将其映射到用户地址空间。根据提示,可以使用 readi 读取文件,并且正确设置页面的权限。
  5. 为了代码编写方便,将获取可用于mmap映射的虚拟地址空间和更新进程的内存映射顶部指针两个功能封装为了get_free_mmap_vmupdate_mmap_top两个辅助函数。
  6. kernel/sysfile.c中添加munmap的系统调用实现:将一个 vma 所分配的所有页释放,如果未映射的页面已被修改且 flag 映射为 MAP_SHARED ,将已经修改的页写回磁盘。根据提示,mmaptest 不会检查非脏页是否未被写回,因此可以不看 D 位(判断是否被修改的脏位)就写回页面,代码逻辑与filewrite类似。
  7. proc.c 中添加处理进程 vma 的各部分代码。
    • allocproc 初始化进程的时候,将 vma 槽都清空
    • exit 退出程序时,清理所有vma映射,并在需要的时候写回磁盘(与munmap的系统调用中写入的逻辑类似,代码基本一致)
    • fork :拷贝父进程在使用的 vma,但是不拷贝物理页,确保子进程与父进程具有相同的映射区域。
  8. 代码具体实现参照代码注释

实现代码

添加mmapmunmap 系统调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// kernel/syscall.c
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);

[SYS_mmap]    sys_mmap,
[SYS_munmap]  sys_munmap,

// kernel/syscall.h
#define SYS_mmap   22
#define SYS_munmap 23

// user/user.h
#ifdef LAB_MMAP
void *mmap(void *addr, size_t len, int prot, int flags,int fd, off_t offset);
int munmap(void *addr, size_t len);
#endif

// user/usys.pl
entry("mmap");
entry("munmap");

定义vma结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// kernel/proc.h

struct vma {
  uint64 addr; // 起始地址
  unsigned long len; // 长度
  int prot; // 权限标志位
  int flags; // map_shared or map_private
  long int offset; // 偏移量
  struct file *file; // 映射的文件
  int inuse; // vma 结构体是否代表了一个正在使用的文件映射
};

#define NVMA 16

// Per-process state
struct proc {
  struct spinlock lock;
  // ...
  struct VMA vma[NVMA];       // 加上16个vma空槽
  uint64 mmap_top; // 存储映射mmap页的顶部地址
};

sys_mmap()实现

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
// kernel/sysfile.c

uint64
sys_mmap(void)
{
  // 声明系统调用参数变量
  uint64 addr,offset;          
  unsigned long len; 
  int prot, flags, fd; 
  struct proc *p = myproc(); // 获取进程
  struct file* file;

  // 获取参数
  argaddr(0, &addr); 
  argaddr(1, &len); 
  argint(2, &prot);
  argint(3, &flags); 
  argfd(4, &fd, &file); // 同时取得文件和描述符
  argaddr(5, &offset); 
  
  // 检查保护标志与文件权限是否兼容:
  if(((prot & PROT_READ) && !file->readable) || 
     ((prot & PROT_WRITE) && (flags & MAP_SHARED) && !file->writable))
    return -1;  // 权限不匹配,返回错误

  // 如果调用者指定地址为0,由内核自动选择映射地址
  if(addr == 0)
    addr = get_free_mmap_vm(len);  // 获取空闲的映射地址区域

  // 遍历进程的VMA数组,找到空的
  for(int i = 0; i < NVMA; i++){ 
    // 跳过已使用且地址大于新地址的VMA
    if(p->vma[i].in_use == 1 && p->vma[i].addr > addr)
      continue;
    
    // 将i位置之后的VMA向后移动,为新VMA腾出空间
    for(int j = NVMA-1; j > i; j--)
      p->vma[j] = p->vma[j-1];  // 移动VMA条目
    
    // 初始化新的VMA条目
    p->vma[i].addr = addr;
    p->vma[i].len = len;
    p->vma[i].prot = prot;
    p->vma[i].flags = flags;
    p->vma[i].file = file;
    p->vma[i].offset = offset;
    p->vma[i].in_use = 1;
    
    break;
  }

  update_mmap_top();  // 更新进程的地址空间顶部指针
  filedup(file); // 增加引用计数

  return addr;  // 返回映射的起始地址
}

sys_munmap()实现

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// kernel/sysfile.c

uint64
sys_munmap(void)
{
  uint64 addr;
  unsigned long len;
  struct proc *p = myproc(); // 获取进程

  // 获取系统调用参数:起始地址和长度
  argaddr(0, &addr);
  argaddr(1, &len);

  for(int i = 0; i < NVMA; i++){ // 遍历进程的VMA数组
    // 检查当前VMA是否包含要取消映射的区域
    if(p->vma[i].in_use == 1 && p->vma[i].addr <= addr && addr + len <= p->vma[i].addr + p->vma[i].len){
      // 遍历要取消映射的每一页
      for(uint64 j = addr; j < addr + len; j += PGSIZE){
        uint64 pa = walkaddr(p->pagetable, j); // 获取物理地址
        if(pa != 0){
          // 如果是共享映射且可写,需要写回文件
          if(p->vma[i].flags & MAP_SHARED && p->vma[i].prot & PROT_WRITE){
            struct inode *ip = p->vma[i].file->ip;
            begin_op();
            ilock(ip);
            int n = PGSIZE; // 要写入的字节数(不超过文件大小)
            if(p->vma[i].offset + j - p->vma[i].addr + n > p->vma[i].file->ip->size)
              n = p->vma[i].file->ip->size - (p->vma[i].offset + j - p->vma[i].addr);
            // 执行文件写入
            writei(ip, 0, pa, p->vma[i].offset + j - p->vma[i].addr, n);
            iunlock(ip);
            end_op();
          }
          // 取消页表映射并释放物理页
          uvmunmap(p->pagetable, j, 1, 1);
        }
      }

      // 调整VMA元数据
      if(p->vma[i].addr == addr && p->vma[i].len == len){ // 情况1:完全取消映射整个VMA
        fileclose(p->vma[i].file);
        p->vma[i].in_use = 0;
        for(int j = i; j < NVMA-1; j++)
          p->vma[j] = p->vma[j+1];
      } 
      else if(p->vma[i].addr == addr){ // 情况2:从开头取消映射部分区域
        p->vma[i].addr += len;
        p->vma[i].len -= len;
        p->vma[i].offset += len;
      } 
      else if(p->vma[i].addr + p->vma[i].len == addr + len){ // 情况3:从末尾取消映射部分区域
        p->vma[i].len -= len;
      } 
      else { // 情况4:中间区域(不支持)
        panic("munmap: hole in vma not supported");
      }
      break;
    }
  }
  update_mmap_top(); // 更新进程的内存映射信息
  return 0;
}

get_free_mmap_vm()update_mmap_top()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// kernel/sysfile.c
uint64
get_free_mmap_vm(unsigned long len)
{
  struct proc *p = myproc(); // 获取当前进程
  for(int i = 1; i < NVMA; i++){
    if(p->vma[i].in_use == 1){ // 检查当前和前一个VMA是否都有效
      if(p->vma[i-1].addr - PGROUNDUP(p->vma[i].addr + p->vma[i].len) >= len)
        return PGROUNDUP(p->vma[i].addr + p->vma[i].len); // 返回当前VMA结束地址对齐后的位置
    } else
      break; // 遇到未使用的VMA槽位,停止搜索
  }
  return PGROUNDDOWN(p->mmap_top - len); // 如果未找到合适空隙,则从mmap_top向下分配
}

void
update_mmap_top(void) // 获取当前进程
{
  struct proc *p = myproc();
  for(int i = 0; i < NVMA; i++){
    if(p->vma[i].in_use == 1) // 如果VMA正在使用中
      p->mmap_top = p->vma[i].addr; // 更新mmap_top为当前VMA的起始地址
  }
}

trap.c中修改

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 添加头文件
#include "fcntl.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"

// ...
void
usertrap(void)
{
  // ...
  } else { // 如果不是系统调用或设备中断,处理其他异常
    if(r_scause() == 13 || r_scause() == 15){ // 检查是否是缺页异常(13=读取缺页,15=写入缺页)
      for(int i = 0; i < NVMA; i++){ //遍历寻找
        uint64 va = r_stval(); // 获取触发异常的虚拟地址
        struct vma v = p->vma[i];
        // 检查:1. VMA有效 2. 异常地址在该VMA范围内
        if(v.in_use && v.addr <= va && va < v.addr + v.len){
          void *pa = kalloc(); // 分配物理页
          memset(pa, 0, PGSIZE); // 清空新分配的物理页
          if(pa == 0){ 
            printf("usertrap(): kalloc\n");
            goto bad; // 如果出错 跳转到错误处理
          }
          // 从磁盘读取数据
          struct inode *ip = v.file->ip; // 获取VMA关联的文件inode、
          begin_op();
          ilock(ip);
          readi(ip, 0, (uint64)pa, v.offset + PGROUNDDOWN(va - v.addr), PGSIZE);
          iunlock(ip);
          end_op();

          // 根据VMA保护标志添加权限:
          int perm = PTE_U;
          if(v.prot & PROT_READ) perm |= PTE_R;
          if(v.prot & PROT_WRITE) perm |= PTE_W;
          if(v.prot & PROT_EXEC) perm |= PTE_X;
          // 权限验证
          if((r_scause() == 13 && !(perm & PTE_R)) || (r_scause() == 15 && !(perm & PTE_W))){
            kfree(pa);
            goto bad; // 如果出错 跳转到错误处理
          }
          // 建立页表映射
          if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, perm) != 0){
            kfree(pa);
            printf("usertrap(): mappages\n");
            goto bad; // 如果出错 跳转到错误处理
          }
          goto good; // 成功处理缺页,跳转到恢复点
        }
      }
    }
  bad:
    printf("usertrap(): unexpected scause 0x%lx pid=%d\n", r_scause(), p->pid);
    printf("            sepc=0x%lx stval=0x%lx\n", r_sepc(), r_stval());
    setkilled(p);
  }
  good:
  if(killed(p))
    exit(-1);
  // ...
}

proc.c中修改

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 添加头文件
#include "fcntl.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"

// ...

static struct proc*
allocproc(void)
{
  struct proc *p;
  // ...
  p->mmap_top = TRAPFRAME - 2*PGSIZE; // 初始化mmap区域顶部地址
  memset(p->vma, 0, sizeof(p->vma)); // 清空VMA数组

  return p;
}
 // ...

int
fork(void)
{
  int i, pid;
  struct proc *np;
  // ...
  // 复制VMA结构
  for(i = 0; i < NVMA; i++)
    if(p->vma[i].in_use){ // 只复制在使用VMA
      np->vma[i] = p->vma[i];
      filedup(np->vma[i].file);
    }
  np->mmap_top = p->mmap_top; // 复制顶部地址

  // ...    
}

// ...

void
exit(int status)
{
  struct proc *p = myproc();

  if(p == initproc)
    panic("init exiting");

  // 清理所有VMA映射
  for(int i = 0; i < NVMA; i++)
    if(p->vma[i].in_use == 1){
      for(uint64 a = p->vma[i].addr; a < p->vma[i].addr + p->vma[i].len; a += PGSIZE){
        uint64 pa = walkaddr(p->pagetable, a); // 获取物理地址
        if(pa != 0){
          if(p->vma[i].prot & PROT_WRITE && p->vma[i].flags & MAP_SHARED){
            struct inode *ip = p->vma[i].file->ip;
            begin_op();
            ilock(ip);
            int n = PGSIZE; // 写入长度
            if(p->vma[i].offset + PGROUNDDOWN(a - p->vma[i].addr) + PGSIZE > p->vma[i].file->ip->size)
              n = p->vma[i].file->ip->size - (p->vma[i].offset + PGROUNDDOWN(a - p->vma[i].addr));
            writei(ip, 0, pa, p->vma[i].offset + PGROUNDDOWN(a - p->vma[i].addr), n); // 写入文件
            iunlock(ip);
            end_op();
          }
          uvmunmap(p->pagetable, a, 1, 1); // 取消映射并释放物理页
        }
      }
      fileclose(p->vma[i].file);
      p->vma[i].in_use = 0;
    }
  // ...
}

实验结果

… …

2484996991@qq.com
使用 Hugo 构建
主题 StackJimmy 设计