内存映射
内存映射
内存映射(Memory Mapping)是一种操作系统提供的机制,它允许把文件或设备中的内容映射到进程的虚拟内存空间中,从而使得程序可以像访问普通内存一样访问文件内容或硬件资源。这种机制广泛用于文件 IO 优化、进程间通信、设备访问等场景。
基本概念
内存映射是指:把一个文件或设备的内容直接映射到进程的虚拟地址空间,之后程序可以像操作内存一样,直接读写文件内容。
在 Linux 和 Unix 系统中,主要通过 mmap() 系统调用实现:
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
addr:建议映射到的内存起始地址(一般为NULL,由系统决定)length:映射的字节数prot:访问权限,如PROT_READ、PROT_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)来完成。
数据存放位置
- 磁盘:文件的实际存储位置。
- 页缓存(Page Cache):操作系统内存中的一部分区域,专门缓存文件数据块,避免频繁磁盘 I/O。
- 物理内存页:页缓存的具体实现单位,每个缓存页对应磁盘上的一段文件数据。
- 虚拟地址空间:进程能看到的线性地址,通过页表映射到物理页。
访问流程
- 进程访问映射区域(虚拟地址)
- 初始时,这些虚拟页没有对应的物理页(页表项无效)。
- 触发缺页异常(Page Fault)
- CPU 发现虚拟地址没有对应的物理页,于是陷入内核。
- 内核处理缺页
- 内核定位到文件偏移位置。
- 检查页缓存中是否已有这部分数据。
- 如果已有 → 直接复用缓存页。
- 如果没有 → 从磁盘读取文件块到内存的页缓存中。
- 建立映射关系
- 内核将页缓存中的物理页与进程的虚拟地址绑定(更新页表)。
- 后续访问
- 进程再次访问同一地址时,会直接命中页缓存中的物理页,就像普通内存访问一样,无需再访问磁盘。
典型应用场景
| 应用场景 | 说明 |
|---|---|
| 大文件读写 | 避免反复调用 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/O | read/write |
简单可靠,易于控制 |
| 访问大文件 / 只读文件 | mmap |
支持随机访问、零拷贝、系统自动优化 |
| 内存共享 / IPC | mmap |
可用于多进程共享内存(匿名或文件映射) |
| 多次频繁小量 I/O | read/write |
mmap 对频繁小数据访问反而不划算 |
| 显式刷盘需求 | write/fsync |
明确控制写入时机更可靠 |
示例代码
#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系统调用映射的文件或匿名内存区域。
简单说,内存映射区域位于堆的上方,栈的下方(高地址空间),用于加载共享库和映射文件。

浙公网安备 33010602011771号