文章

内存映射

内存映射

内存映射

内存映射(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:文件中映射的起始偏移量(必须是页对齐)

二、内存映射的分类

1. 按用途分

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

2. 按共享方式分

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

三、内存映射的优点

  1. 高效 I/O

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

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

  2. 节省内存开销
    • 多个进程可共享映射区域,节省物理内存。
  3. 便于进程间通信(IPC)
    • 使用共享映射区域,多个进程可直接读写同一块内存。
  4. 简化文件处理逻辑
    • 将文件看成一个普通内存数组,访问更自然。

四、内存映射的底层机制

内核会将文件内容加载到页缓存中,并为进程分配虚拟地址空间。当进程访问映射区域时,如果页尚未加载,会触发缺页异常(Page Fault),由内核将相应数据页加载进内存。

页缓存(Page Cache)磁盘文件数据在内存中的副本,属于 文件系统的缓存层

读写过程如下:

  1. 页表未建立映射 → 缺页异常
  2. 内核读取文件内容到物理页
  3. 建立虚拟地址到物理地址的映射
  4. 程序访问数据就像访问普通内存一样

五、典型应用场景

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

六、内存映射 vs 普通文件IO

维度对比表格

对比维度传统文件 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 进行授权