文章

内存映射

内存映射

内存映射

内存映射(Memory Mapping)是一种操作系统提供的机制,它允许把文件或设备中的内容映射到进程的虚拟内存空间中,从而使得程序可以像访问普通内存一样访问文件内容或硬件资源。这种机制广泛用于文件 IO 优化、进程间通信、设备访问等场景

基本概念

内存映射是指:把一个文件或设备的内容直接映射到进程的虚拟地址空间,之后程序可以像操作内存一样,直接读写文件内容。

在 Linux 和 Unix 系统中,主要通过 mmap() 系统调用实现:

1
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
  • addr:建议映射到的内存起始地址(一般为 NULL,由系统决定)
  • length:映射的字节数
  • prot:访问权限,如 PROT_READPROT_WRITE
  • flags:映射类型,如 MAP_SHARED(共享)或 MAP_PRIVATE(私有)
  • fd:要映射的文件描述符
  • offset:文件中映射的起始偏移量(必须是页对齐)

分类

按用途分

  • 文件映射:把磁盘上的文件映射到内存,如使用 mmap 实现对大文件的访问。
  • 匿名映射:不依赖文件,映射的是一块匿名内存区域,如用于进程间通信。

按共享方式分

  • 共享映射(MAP_SHARED)
    • 内存的修改会同步到磁盘文件。
    • 多个进程共享这块内存,适用于进程间通信。
  • 私有映射(MAP_PRIVATE)
    • 内存的修改不会影响磁盘文件,系统会在写时进行拷贝(Copy-On-Write)。
    • 通常用于只读或临时修改。

优点

高效 I/O

  • 省去显式的 read/write 调用,直接在用户空间访问文件内容。

  • 操作系统使用页缓存和按需加载优化访问。

节省内存开销

  • 多个进程可共享映射区域,节省物理内存。

便于进程间通信(IPC)

  • 使用共享映射区域,多个进程可直接读写同一块内存。

简化文件处理逻辑

  • 将文件看成一个普通内存数组,访问更自然。

底层机制

当一个文件被 mmap() 映射时,并不是立刻把整个文件读入内存,而是先在进程的虚拟地址空间中预留一块区域,并与目标文件建立映射关系。真正的数据访问依赖缺页异常(Page Fault)页缓存(Page Cache)来完成。

数据存放位置

  1. 磁盘:文件的实际存储位置。
  2. 页缓存(Page Cache):操作系统内存中的一部分区域,专门缓存文件数据块,避免频繁磁盘 I/O。
  3. 物理内存页:页缓存的具体实现单位,每个缓存页对应磁盘上的一段文件数据。
  4. 虚拟地址空间:进程能看到的线性地址,通过页表映射到物理页。

访问流程

  1. 进程访问映射区域(虚拟地址)
    • 初始时,这些虚拟页没有对应的物理页(页表项无效)。
  2. 触发缺页异常(Page Fault)
    • CPU 发现虚拟地址没有对应的物理页,于是陷入内核。
  3. 内核处理缺页
    • 内核定位到文件偏移位置。
    • 检查页缓存中是否已有这部分数据。
      • 如果已有 → 直接复用缓存页。
      • 如果没有 → 从磁盘读取文件块到内存的页缓存中。
  4. 建立映射关系
    • 内核将页缓存中的物理页与进程的虚拟地址绑定(更新页表)。
  5. 后续访问
    • 进程再次访问同一地址时,会直接命中页缓存中的物理页,就像普通内存访问一样,无需再访问磁盘。

典型应用场景

应用场景说明
大文件读写避免反复调用 read/write,如视频播放器、数据库等
共享内存通信父子进程、多个进程通过映射共享一段内存
执行程序代码程序加载时会把 .text.data 段映射到内存
驱动设备访问操作系统通过内存映射访问 I/O 设备寄存器

对比传统文件 I/O

维度对比表格

对比维度传统文件 I/O (read/write)内存映射 I/O (mmap)
使用方式通过 read()write() 接口显式进行使用 mmap() 映射文件后,通过内存指针访问
数据流路径(读)磁盘 → 内核页缓存 → 用户缓冲区磁盘 → 页缓存 ←→ 用户空间直接访问
数据流路径(写)用户缓冲区 → 内核 → 写入磁盘写内存页 → 标记为脏页 → msync() 或回写
拷贝次数(读)2 次拷贝:磁盘→页缓存,页缓存→用户缓冲区1 次拷贝(磁盘→页缓存);用户直接访问页缓存
系统调用次数每次 I/O 都需要系统调用初始化映射 + 缺页时才触发 page fault(更少)
访问方式顺序读写,使用系统调用支持随机访问,直接通过指针操作
内存使用需要显式缓冲区,占用额外内存共用页缓存,不需要显式用户缓冲区
效率(大文件访问)效率低:频繁 syscall,用户缓冲区拷贝高效:少系统调用,零拷贝,支持随机读取大文件
线程共享性用户缓冲区无法多线程共享多线程可以共享映射内存(用于进程间通信 IPC)
是否自动刷盘手动 write()fsync()自动回写脏页,也可手动 msync()
页缓存利用显式与页缓存交互完全复用页缓存机制
API 灵活性更简单通用,适合各种 I/O 场景需自己处理内存访问边界,不适合小文件或频繁更改
异常控制性出错能立即检查 read()/write() 的返回值出错常通过 SIGSEGV,需注意非法地址访问

适用场景对比

使用场景推荐方式理由
小文件 / 简单顺序 I/Oread/write简单可靠,易于控制
访问大文件 / 只读文件mmap支持随机访问、零拷贝、系统自动优化
内存共享 / IPCmmap可用于多进程共享内存(匿名或文件映射)
多次频繁小量 I/Oread/writemmap 对频繁小数据访问反而不划算
显式刷盘需求write/fsync明确控制写入时机更可靠

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <iostream>

int main() {
    int fd = open("example.txt", O_RDWR);
    size_t length = lseek(fd, 0, SEEK_END);

    char* data = (char*)mmap(nullptr, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (data == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    std::cout << "File content: " << std::string(data, length) << std::endl;

    data[0] = 'H'; // 修改映射区域会影响文件(MAP_SHARED)

    munmap(data, length);
    close(fd);
    return 0;
}

可能的风险和注意事项

  • 安全性:非法地址访问可能导致段错误(SIGSEGV)
  • 同步性:需要调用 msync 来确保数据同步到磁盘
  • 资源泄漏:忘记 munmap 会造成内存泄漏
  • 跨平台问题mmap 是 POSIX 标准,Windows 上需使用 MapViewOfFile 等接口

内存映射区域在哪

进程虚拟地址空间中的用户区:

+-------------------------+
| 栈 Stack                  | <--- 高地址                             |
| ------------------------- |
| 空间 (可能是库)           |
| ------------------------- |
| 堆 Heap                   | <--- malloc/new分配从低地址往高地址扩展 |
| ------------------------- |
| BSS(未初始化全局变量)   |
| ------------------------- |
| Data(已初始化全局/静态) |
| ------------------------- |
| Text(代码区)            | <--- 低地址                             |
+-------------------------+

内存映射(mmap)一般放在用户空间的堆和栈之间,也就是图中“空间 (可能是库)”这一块区域。它通常包含:

  • 动态链接库(共享库)的映射区;
  • 通过 mmap 系统调用映射的文件或匿名内存区域。

简单说,内存映射区域位于堆的上方,栈的下方(高地址空间),用于加载共享库和映射文件。

本文由作者按照 CC BY 4.0 进行授权