内存映射
内存映射
内存映射
内存映射(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_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 | 明确控制写入时机更可靠 |
示例代码
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 进行授权