内存映射
内存映射
内存映射
内存映射(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
:文件中映射的起始偏移量(必须是页对齐)
二、内存映射的分类
1. 按用途分
- 文件映射:把磁盘上的文件映射到内存,如使用
mmap
实现对大文件的访问。 - 匿名映射:不依赖文件,映射的是一块匿名内存区域,如用于进程间通信。
2. 按共享方式分
- 共享映射(MAP_SHARED)
- 内存的修改会同步到磁盘文件。
- 多个进程共享这块内存,适用于进程间通信。
- 私有映射(MAP_PRIVATE)
- 内存的修改不会影响磁盘文件,系统会在写时进行拷贝(Copy-On-Write)。
- 通常用于只读或临时修改。
三、内存映射的优点
高效 I/O:
省去显式的
read
/write
调用,直接在用户空间访问文件内容。操作系统使用页缓存和按需加载优化访问。
- 节省内存开销:
- 多个进程可共享映射区域,节省物理内存。
- 便于进程间通信(IPC):
- 使用共享映射区域,多个进程可直接读写同一块内存。
- 简化文件处理逻辑:
- 将文件看成一个普通内存数组,访问更自然。
四、内存映射的底层机制
内核会将文件内容加载到页缓存中,并为进程分配虚拟地址空间。当进程访问映射区域时,如果页尚未加载,会触发缺页异常(Page Fault),由内核将相应数据页加载进内存。
页缓存(Page Cache):磁盘文件数据在内存中的副本,属于 文件系统的缓存层。
读写过程如下:
- 页表未建立映射 → 缺页异常
- 内核读取文件内容到物理页
- 建立虚拟地址到物理地址的映射
- 程序访问数据就像访问普通内存一样
五、典型应用场景
应用场景 | 说明 |
---|---|
大文件读写 | 避免反复调用 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/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 进行授权