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的使用场景

我在项目中遇到过几个典型场景,列出来供你参考:

  1. 大文件读写:比如日志文件、数据库文件、视频文件。用read/write来回拷贝,效率低得吓人。mmap一次映射,随机访问就像操作数组一样快。
  2. 进程间共享内存:两个进程可以mmap同一个文件,实现数据共享。不需要socket、不需要pipe,直接读写同一块内存。
  3. 设备驱动交互:有些硬件设备(比如帧缓冲、DMA缓冲区)可以通过mmap映射到用户空间,省去系统调用开销。
  4. 快速文件加载:比如加载配置文件、资源文件。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() 虚拟地址空间 建立页表映射 物理内存(页缓存) 缺页中断时加载 缺页中断 磁盘文件 data.bin 磁盘I/O 用户直接读写 像操作数组一样 msync 同步 强制写回磁盘 关键路径:mmap → 建立映射 → 缺页中断 → 磁盘加载 → 直接访问 写回路径:修改内存 → msync → 磁盘写入 用户空间 内核空间 磁盘

总结一下

mmap是一个很强大的工具,但它不是银弹。我个人的经验是:

  • 大文件、频繁随机访问 → mmap
  • 小文件、一次性读取 → malloc + read
  • 进程间共享数据 → mmap(共享文件或匿名映射)
  • 临时小对象 → malloc

记住一点:mmap让文件操作变成了内存操作,但代价是你要更小心地管理同步和生命周期。用好了,性能翻倍;用不好,数据丢失或者程序崩溃,都是有可能的。

嗯,今天就聊到这儿。希望下次你在项目里遇到大文件或者共享内存的需求时,能想起mmap这个老朋友。


公众号:蓝海资料掘金营,微信deep3321