MIT 6.S081 2020 Lab10 mmap 个人全流程

零、写在前面

mmap 的目的

  1. 传统 I/O 读写方式开销过大
    1. 传统的文件 I/O 需要在 用户空间 和 内核空间 之间多次拷贝数据。
    2. 大文件处理时,内存的开销比较大。
  2. 映射文件到内存
    1. 将文件的内容直接映射到进程的虚拟地址空间,使得访问文件就像访问普通内存一样高效:
      1. 替代 read/write 等系统调用;
      2. 节省一次内核空间和用户空间的拷贝(零拷贝);
      3. 适合处理大文件或频繁访问的文件数据。
  3. 实现进程间共享内存(IPC)
    1. 使用 MAP_SHARED 标志,多个进程可以映射同一个文件区域,从而共享这段内存,实现快速通信。
  4. 延迟加载/按需加
    1. 当用 mmap 映射大文件时,数据并不会立即加载,而是通过 缺页异常(Page Fault)在访问时才加载,对资源消耗更加友好。
  5. 支持匿名映射
    1. 即映射一块没有文件关联的内存(例如堆或共享内存),常用于运行时分配内存(可替代 malloc)。

mmap 的实现原理(以 Linux 为例,xv6 中实现思路类似但更简化)

  1. 虚拟地址空间管理(VMA)
  • 每个进程都有一个**虚拟内存区域(Virtual Memory Area, VMA)**链表或树,记录当前有哪些区域被映射(包括堆、栈、mmap)。
  • 每次调用 mmap,内核会分配一段空闲的虚拟地址,并创建一个新的 VMA 节点,插入进程的 VMA 列表中。
  1. 页表未立即映射(懒加载)
  • mmap 调用时并不分配物理内存或读取文件数据,而是 延迟到第一次访问该区域 时:
    • CPU 触发缺页异常;
    • 内核捕捉异常,在页表中创建映射;
    • 分配物理页;
    • 若是文件映射,则从文件读取数据填充物理页;
    • 最后更新页表,恢复执行。

这也是为什么 mmap 能够高效地映射超大文件 ------ 因为不会一次性加载全部内容。

  1. 文件共享与写回
  • MAP_SHARED: 修改映射内存会影响原始文件,页被修改(置 Dirty Bit)后,在 munmapmsync 时写回文件;
  • MAP_PRIVATE: 写时拷贝(Copy-on-Write, COW),不会修改原文件(通常用于 fork 时父子进程共享内存);
  1. munmap 实现
  • 查找 VMA 链表中目标地址;
  • 释放对应的物理页;
  • 如果是共享映射且页被修改,则写回文件;
  • 更新页表,清除对应页;
  • 移除 VMA 或裁剪 VMA 区间。

记得切换到 mmap 分支


一、mmap

1.1 说明

mmapmunmap 系统调用允许 UNIX 程序对其地址空间进行详细控制。**它们可以用于在进程之间共享内存、将文件映射到进程的地址空间中,以及用于用户级缺页处理机制,**比如在课堂上讨论的垃圾回收算法。

在这个实验中,你将为 xv6 添加 mmapmunmap,重点是实现内存映射文件功能。

mmap 可以有很多种使用方式,但这个实验只需要支持与文件内存映射相关的一小部分功能:

c 复制代码
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
  • 你可以假设 addr 总是为 0,表示应由内核决定映射文件的虚拟地址。mmap 返回这个地址,或者在失败时返回 0xffffffffffffffff
  • length 是要映射的字节数,可能不同于文件的长度。
  • prot 指示内存应是否可读、可写和/或可执行;你可以假设 protPROT_READPROT_WRITE 或两者都有。
  • flags 将是 MAP_SHARED(表示对映射内存的修改应写回文件)或 MAP_PRIVATE(不写回)。你不需要处理 flags 的其他位。
  • fd 是要映射的文件的打开文件描述符。
  • 你可以假设 offset 为 0(即映射从文件的起始位置开始)。

如果多个进程映射了同一个 MAP_SHARED 文件,它们不共享物理页面也没有问题。

munmap(addr, length) 应移除该地址范围内的 mmap 映射。如果进程修改了这段内存,并且是以 MAP_SHARED 映射的,那么这些修改应写回文件。一个 munmap 调用可能只覆盖一个 mmap 区域的一部分,但你可以假设它只会取消映射开头、结尾,或整个区域(不会在中间打洞)。

你应实现足够的 mmapmunmap 功能以使 mmaptest 测试程序能够工作。mmaptest 没有使用的功能你就不必实现。

官网的一些提示

  1. 先在 UPROGS 中添加 _mmaptest,并实现 mmapmunmap 系统调用,使 user/mmaptest.c 能够编译通过。此时先让 mmapmunmap 返回错误。我们已在 kernel/fcntl.h 中定义了 PROT_READ 等常量。运行 mmaptest,它会在第一次调用 mmap 时失败。
  2. 像懒分配实验那样,懒惰地填充页表,也就是说,mmap 不应该立即分配物理内存或读取文件内容。相反,应在页错误处理代码(usertrap 或其调用函数)中完成这一步。这样做的原因是为了使大文件的 mmap 操作足够快,且可以支持映射大于物理内存的文件。
  3. 跟踪每个进程通过 mmap 映射了什么。定义一个结构体表示虚拟内存区域(VMA,Lecture 15中讲过),记录地址、长度、权限、文件等信息。由于 xv6 内核没有动态内存分配器,可以使用一个固定大小的 VMA 数组,动态分配其中的元素。大小为 16 应该足够。
  4. 实现 mmap:在进程地址空间中找到一个未使用的区域用于映射文件,并将一个 VMA 添加到进程的映射区域表中。VMA 中应包含指向映射文件的 struct file 指针;mmap 应增加文件的引用计数(提示:参见 filedup)。此时运行 mmaptest,第一个 mmap 应成功,但第一次访问映射内存会触发缺页错误并导致程序被杀死。
  5. 添加代码,在访问 mmap 区域时触发页错误后分配物理页、从文件中读取 4096 字节到该页,并将其映射到用户空间。用 readi 读取文件,它接受偏移参数(但你需要加锁/解锁传给 readi 的 inode)。别忘了为该页设置正确的权限。运行 mmaptest,应该会执行到第一个 munmap
  6. 实现 munmap:查找指定地址范围的 VMA,取消映射指定的页面(提示:使用 uvmunmap)。如果这次 munmap 移除了之前 mmap 的所有页面,应减少相关文件结构的引用计数。如果取消映射的页被修改过,且是以 MAP_SHARED 映射的,应将该页写回文件。可参考 filewrite 的实现。
  7. 理想情况下,只有在页面被实际修改后,才写回 MAP_SHARED 页面。RISC-V 页表项中的 D(dirty)位表明该页是否被写过。不过,mmaptest 并不检查未修改页面是否被写回,因此即使不检查 D 位,写回所有页也能通过测试。
  8. 修改 exit,让进程退出时像调用了 munmap 一样取消所有映射区域。此时运行 mmaptestmmap_test 应该能通过,但 fork_test 可能仍然失败。
  9. 修改 fork,确保子进程拥有和父进程一样的映射区域。不要忘了增加每个 VMA 所指 struct file 的引用计数。在子进程页错误处理器中,为页面分配新的物理页是可以的(不与父进程共享)。共享物理页更酷,但实现更复杂。此时运行 mmaptest,应能通过 mmap_testfork_test
  10. 最后运行 usertests,确保所有功能仍然正常工作。

1.2 实现

1.2.1 准备工作

添加系统调用 mmap, munmap

添加系统调用也是常规操作了:

c 复制代码
// kernel/syscall.h
// ...
#define SYS_close  21
#define SYS_mmap   22
#define SYS_munmap 23
c 复制代码
// kernel/syscall.c
// ...
extern uint64 sys_uptime(void);
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);
// ...
[SYS_close]   sys_close,
[SYS_mmap]    sys_mmap,
[SYS_munmap]    sys_munmap,
};
c 复制代码
// kernel/sysfile.c
uint64 sys_mmap(void) 
{
    return 0;
}

uint64 sys_munmap(void)
{
    return 0;
}
c 复制代码
// user/user.h
// ...
void *memcpy(void *, const void *, uint);
void *mmap(void *addr, int length, int prot, int flags, int fd, int offset);
int munmap(void *addr, int length);
c 复制代码
// user/user.pl
// ...
entry("uptime");
entry("mmap");
entry("munmap");

先在makefile 里面添加上 mmaptest 看看能不能跑:

c 复制代码
// ...
$U/_wc\
$U/_zombie\
$U/_mmaptest\

可以跑,接下来进行实现

根据官网提示,我们接下来要定义一个结构体VMA,用来记录 mmap 创建的虚拟内存区域,包括 地址、长度、权限、文件等,然后为进程创建长度 16 的 vma 数组

c 复制代码
// kernel/proc.h
#define NVMA 16

struct VMA{
  uint64 addr;        // address
  uint64 length;      // length
  int prot;           // (PROT_READ, PROT_WRITE)
  int flags;          // MAP_SHARED or MAP_PRIVATE
  struct file *file;  // mapped file 
  uint64 offset;      // offset
};

// Per-process state
struct proc {
  // ...
  struct VMA vmas[NVMA];       // process's table of mapped regions
};
1.2.2 sys_mmap
c 复制代码
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
  • 从 vmas 中找到一个空闲的 VMA
  • 记录 申请对象到 该VMA
  • 增加 file 的引用计数
  • 内存分配为 lazy alloc
c 复制代码
uint64 sys_mmap(void)
{
    // parameters for mmap
    uint64 addr;
    int len, prot, flags, fd, offset;
    struct file* file;
    
    struct VMA* vma = 0;

    // get parameters
    if(argaddr(0, &addr)<0 || argint(1, &len)<0
       || argint(2, &prot)<0 || argint(3, &flags)<0
       || argfd(4, &fd, &file)<0 || argint(5, &offset)<0)
        return -1;

    // is valid ?
    if(len <= 0)
      return -1;        
    if((prot & (PROT_READ|PROT_WRITE|PROT_EXEC)) == 0) // only PROT_READ, PROT_WRITE, PROT_EXEC
        return -1;
    if((prot & PROT_WRITE) && !file->writable && flags == MAP_SHARED) // writable holds when MAP_SHARED
        return -1;
    if((prot & PROT_READ) && !file->readable) // MAP_PRIVATE holds when readable
        return -1;

    struct proc* p = myproc();
    len = PGROUNDUP(len);

    // size overflowed
    if(p->sz+len > MAXVA)
        return -1;

    if(offset<0 || offset%PGSIZE)
        return -1;

    // find a vacant vma
    for(int i = 0; i < NVMA; ++ i) {
        if(p->vmas[i].addr)
            continue;
        vma = &p->vmas[i];
        break;
    }

    // fail to find
    if(!vma)
        return -1;

    if(addr == 0)
        vma->addr = p->sz; // if address not assigned, then use process's sz
    else
        vma->addr = addr; // addr assigned

    vma->length = len;
    vma->prot = prot;
    vma->flags = flags;
    vma->offset = offset;
    vma->file = file;
    p->sz += len;
    
    // file refCnt
    filedup(file);

    return vma->addr;
}
1.2.3 munmap
c 复制代码
munmap(addr, length)
  • 查表找到 要解除映射的 vma
  • 找到后 uvmunmap() 掉
  • 然后 fileclose
  • 如果 是 MAP_SHARED 并且 有修改,那么写回文件(此处可参考 filewrite)
c 复制代码
uint64 sys_munmap(void)
{
    uint64 addr;
    int len;
    struct VMA* vma = 0;
    struct proc* p = myproc();

    if(argaddr(0, &addr) < 0 || argint(1, &len) < 0)
        return -1;

    // check parameter
    if(len <= 0 || addr + len > p->sz)
        return -1;

    addr = PGROUNDDOWN(addr);
    len = PGROUNDUP(len);

    // find vma
    for(int i = 0; i < NVMA; ++ i) {
        if(p->vmas[i].addr && addr >= p->vmas[i].addr
           && addr + len <= p->vmas[i].addr + p->vmas[i].length) {
            vma = &p->vmas[i];
            break;
        }
    }
    
    // addr not valid
    if(!vma || addr != vma->addr)
        return -1;

    // if shared
    if(vma->flags & MAP_SHARED)
        filewrite(vma->file, addr, len);

    // unmap
    uvmunmap(p->pagetable, addr, len/PGSIZE, 1);

    // if unmap completely
    if(len == vma->length) {
        fileclose(vma->file);
        memset(vma, 0, sizeof(*vma));
    } else {
        // otherwise just change addr and length
        vma->addr += len;
        vma->length -= len;
    }

    // if umap the end of process addr, the adjust process size
    if(addr + len == p->sz)
        p->sz -= len;

    return 0;
}
1.2.4 trap handler
  • 因为我们前面只是懒分配,因此要在缺页异常的时候物理分配
  • 如果是文件映射,则从文件读取数据填充物理页;
  • 为 mem 和 va 建立映射
1.2.5 完善 exit
  • 只需添加进程退出时对于 映射区域的 清空逻辑
c 复制代码
void exit(int status)
{
  // ... 
  for (int i = 0; i < 16; ++ i) {
    if(p->vmas[i].length > 0) {
      uvmunmap(p->pagetable, p->vmas[i].addr, p->vmas[i].length / PGSIZE, 1);
      if (p->vmas[i].file) {
        fileclose(p->vmas[i].file);
      }
      p->vmas[i].length = 0;
    }
  } // ...
}
1.2.6 fork
  • 根据官网提示,fork 的时候,copy 父进程的 映射区域 给 子进程
  • 复制vma 的时候,需要增加 file 的引用计数
c 复制代码
int fork(void) {
  // ...
  np->state = RUNNABLE;

  for (int i = 0; i < 16; ++ i) {
    if (p->vmas[i].length > 0) {
      memmove(&np->vmas[i], &p->vmas[i], sizeof(struct VMA));
      if (p->vmas[i].file) {
        filedup(p->vmas[i].file);
      }
    }
  }

  release(&np->lock);

  return pid;
}

到这里我开心的测试了下,然后就释怀的似了

看了下新加的 uvmunmap 的地方,很快就反应过来是lazy alloc 的页面没有被访问,自然有可能是无效的,所以我们 改为 continue 即可

然后再次运行,又似了:

因为 uvmcopy 只在fork那里添加了,原因和前面类似,就是copy 了 没有实际分配的 lazy 页面

那么把 uvmcopy 中的panic 改成 continue 即可

然后就过了

跑一下 usertests:

相关推荐
Jooolin6 小时前
【操作系统】这么多任务,操作系统是怎么忙得过来的?
linux·操作系统·ai编程
望获linux9 小时前
【Linux基础知识系列】第二十八篇-管道与重定向的使用
linux·前端·chrome·操作系统·rtos·嵌入式软件
莱茵不哈哈2 天前
操作系统八股文
c++·操作系统·c·八股文·进程线程
_小猪沉塘3 天前
【Create my OS】5 内核线程
linux·操作系统·unix
huangyuchi.4 天前
【Linux】初见,进程概念
linux·服务器·操作系统·进程·进程管理·pcb·fork
异常君5 天前
用户态与内核态:Java 程序员必懂的两种执行状态
性能优化·操作系统·cpu
Jooolin5 天前
【编程史】Ubuntu到底是啥?它和Linux又是什么关系?
linux·ubuntu·操作系统
c7_ln6 天前
Linux基本指令(包含vim,用户,文件等方面)超详细
linux·操作系统·vim
OpenAnolis小助手6 天前
龙蜥开发者说:我的龙蜥开源之旅 | 第 32 期
开源·操作系统·龙蜥社区·龙蜥开发者说