21、内存映射文件:mmap原理、使用场景、与malloc的对比
内存映射文件,英文叫 mmap。说实话,很多嵌入式工程师听到这个词就觉得是Linux内核才用的东西,跟自己没关系。我以前也这么想,直到有一次做视频采集项目,内存拷贝把CPU吃光了……嗯,从那以后,我见到大块数据传输就会想:能不能用mmap?
今天咱们就把mmap的原理、用法、坑,还有它跟malloc的区别,一次性讲透。
mmap是什么?一句话说清楚
mmap的全称是 memory map,也就是内存映射。它能把一个文件或者设备的一部分,直接映射到进程的虚拟地址空间里。
说白了就是:你打开一个文件,然后mmap一下,之后读写这块内存,就等于在读写文件。不需要read、write,不需要中间缓冲区,数据直接在内存和文件之间“对齐”。
我个人的理解是:mmap让文件“住进”了内存里。你操作内存,文件也跟着变。
mmap的核心原理
咱们从底层看两眼。mmap做的事情其实很巧妙:
- 它不立即把文件内容全部读进内存
- 它只是在进程的页表里建立映射关系
- 真正访问某个页时,触发缺页中断,内核才把对应磁盘块加载到物理内存
这个机制叫 按需分页。你想想看,一个1GB的文件,你只读前4KB,mmap就只加载4KB,省了多少内存和时间。
关键区别:malloc分配内存时,内存是立即分配的(至少虚拟地址是立即分配的)。而mmap是“懒加载”的,你碰哪一页,哪一页才真正进来。
mmap的使用场景
我在项目中遇到过几个典型场景,列出来供你参考:
- 大文件读写:比如日志文件、数据库文件、视频文件。用read/write来回拷贝,效率低得吓人。mmap一次映射,随机访问就像操作数组一样快。
- 进程间共享内存:两个进程可以mmap同一个文件,实现数据共享。不需要socket、不需要pipe,直接读写同一块内存。
- 设备驱动交互:有些硬件设备(比如帧缓冲、DMA缓冲区)可以通过mmap映射到用户空间,省去系统调用开销。
- 快速文件加载:比如加载配置文件、资源文件。mmap之后直接解析内存,比fread快不少。
我的建议:如果你要频繁读写同一个文件的多个位置,或者文件很大(超过几十MB),优先考虑mmap。如果只是小文件一次性读入,malloc + read就够了,别过度设计。
mmap vs malloc:到底怎么选?
这个问题我经常被问到。咱们直接上对比表:
| 对比维度 | mmap | malloc |
|---|---|---|
| 数据来源 | 文件或设备 | 堆内存(匿名映射) |
| 分配时机 | 建立映射,按需加载 | 立即分配虚拟地址 |
| 读写方式 | 直接内存访问 | 直接内存访问 |
| 数据持久化 | 自动同步到文件 | 进程退出即丢失 |
| 系统调用开销 | 一次mmap,后续无系统调用 | 每次读写都可能触发缺页 |
| 适用场景 | 大文件、共享内存、设备映射 | 小数据、临时变量、动态结构 |
| 内存浪费 | 按页对齐(4KB粒度) | 按块管理,碎片可控 |
你看,两者不是替代关系,而是互补关系。我个人的习惯是:文件操作优先想mmap,内存分配优先想malloc。如果既要文件又要内存,那就看数据量大小。
代码示例:mmap的基本用法
咱们直接看一段代码,感受一下:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main() {
int fd = open("data.bin", O_RDWR);
if (fd == -1) {
perror("open");
exit(1);
}
// 获取文件大小
off_t size = lseek(fd, 0, SEEK_END);
// 映射整个文件
char *map = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, 0);
if (map == MAP_FAILED) {
perror("mmap");
close(fd);
exit(1);
}
// 现在可以直接读写map,就像操作数组一样
// 比如修改第一个字节
map[0] = 'A';
// 记得解除映射
munmap(map, size);
close(fd);
return 0;
}
这段代码很简单,但有几个关键点要注意:
MAP_SHARED:修改会写回文件。如果只是读,用MAP_PRIVATE,修改不影响原文件。- 文件大小不能为0,否则mmap会失败。
- 映射长度最好是页大小的整数倍,否则最后一个页会有部分未映射。
避坑指南:我曾经踩过的坑
我曾经在一个嵌入式项目里用mmap映射NAND Flash上的配置文件。一开始跑得好好的,后来发现偶尔读出来的数据是旧的。查了两天才发现——没有调用msync。
mmap的写操作不会立即刷到磁盘,它只是修改了内存中的页缓存。如果系统崩溃或者意外断电,数据就丢了。所以:
重要提醒:如果你用MAP_SHARED并且修改了数据,记得在关键位置调用 msync(map, size, MS_SYNC) 强制同步。否则,你永远不知道数据什么时候真正落地。
另外还有一个坑:文件截断。如果mmap之后,另一个进程把文件截断了(比如truncate),你映射的区域可能变成“空洞”,访问时会触发SIGBUS信号,程序直接崩溃。我建议在映射期间,尽量避免其他进程修改文件大小。
mmap与malloc的底层关系
你可能不知道,malloc在分配大块内存时,底层用的就是mmap。没错,glibc的malloc实现里,当请求大小超过MMAP_THRESHOLD(默认128KB)时,malloc会调用mmap分配匿名映射,而不是从堆里切。
为什么会这样?因为大块内存用brk管理容易产生碎片,而且释放后不能归还给操作系统。而mmap分配的内存,munmap之后直接归还,干净利落。
所以你看,mmap和malloc并不是完全独立的两个世界。它们在内核层面是有交集的。
一张图看懂mmap的核心逻辑
下面这张SVG图,帮你梳理mmap从调用到数据访问的完整流程:
总结一下
mmap是一个很强大的工具,但它不是银弹。我个人的经验是:
- 大文件、频繁随机访问 → mmap
- 小文件、一次性读取 → malloc + read
- 进程间共享数据 → mmap(共享文件或匿名映射)
- 临时小对象 → malloc
记住一点:mmap让文件操作变成了内存操作,但代价是你要更小心地管理同步和生命周期。用好了,性能翻倍;用不好,数据丢失或者程序崩溃,都是有可能的。
嗯,今天就聊到这儿。希望下次你在项目里遇到大文件或者共享内存的需求时,能想起mmap这个老朋友。