深入 mmap:被严重低估的 Linux 黑科技,MySQL/Redis/Nginx 都在用它
Mmap 不是什么神秘的黑科技,它的本质是:用虚拟内存的页表映射,替代数据拷贝。
先来思考一个简单的问题。
免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈
当你打开浏览器浏览一个网页时,数据从服务器传输到你的屏幕,中间经历了多少次“复制”?
或者,当你用 MySQL 查询一条数据时,从磁盘读取到返回结果,数据又被搬运了几次?
答案可能会让很多人感到意外:至少 4 次。
更关键的是,这其中至少有 2 次拷贝,是完全可以省掉的。而省掉它的核心技术,就是 mmap。
从 MySQL、Redis、Nginx,到你每天运行的每一个程序,背后都离不开它的支持。然而,即便写了多年代码,很多开发者对其核心原理依然一知半解。

一、先说说,没有 mmap 之前有多慢
平时读取文件,代码可能只有一行:
read(fd, buf, 4096);
看起来简洁明了,但背后发生了什么?
磁盘
│
│ 第①次拷贝(DMA 搬运)
▼
内核缓冲区(Page Cache)
│
│ 第②次拷贝(内核 → 你的程序)
▼
你的 buf[]
数据被完整地复制了两次。如果涉及写回操作,同样的过程还要再来一遍。一次完整的读写,数据至少被搬运了 4 次。
对于小文件,这种开销或许感知不强。但想象一下 MySQL 每秒处理数万次查询,Redis 承载数十万 QPS,或者 Nginx 发送一个 500MB 的视频文件……这 4 次拷贝,就成了系统性能难以逾越的天花板。
二、mmap 做了一件听起来很简单、但很优雅的事
它的核心思想可以用一句话概括:将文件直接映射到程序的虚拟地址空间,让读写内存等同于读写文件,从而消除中间那一次关键的数据拷贝。
对比一下两种方式:
传统 read():
┌──────────┐ 拷贝① ┌──────────┐ 拷贝② ┌──────────┐
│ 磁盘 │ ──────► │ 页缓存 │ ──────► │ 用户buf │
└──────────┘ └──────────┘ └──────────┘
(内核空间) (用户空间)
↑
多了这次拷贝!
mmap:
┌──────────┐ 拷贝① ┌──────────┐
│ 磁盘 │ ──────► │ 页缓存 │
└──────────┘ └────┬─────┘
│ 直接映射(零拷贝!)
▼
┌──────────┐
│ 进程虚拟 │
│ 地址空间 │ ← 用户直接读写这里
└──────────┘
程序直接“看到”并操作的,就是内核页缓存里的那份数据。省掉从内核到用户空间的那次拷贝,这正是 mmap 实现“零拷贝”的关键所在。
三、mmap 用起来是什么感觉?
先看函数签名:
void *mmap(void *addr, // 映射到哪(传 NULL 让内核决定)
size_t length, // 映射多少字节
int prot, // 权限(读/写/执行)
int flags, // 关键参数,下面讲
int fd, // 文件描述符
off_t offset); // 从文件哪里开始映射
其中,flags 参数是灵魂,主要分为两组:
MAP_SHARED → 修改会同步回文件,其他进程也能看到
MAP_PRIVATE → 写时复制,你改了不影响原文件
MAP_ANONYMOUS → 不关联文件,纯粹申请内存(fd 传 -1)
通过这几种标志的组合,可以覆盖 mmap 几乎所有的核心应用场景。
四、四种用法,一次全搞懂
1. 用法一:读一个大文件
传统方式需要循环调用 read(),管理缓冲区,处理边界条件。而 mmap 的方式则优雅得多:
int fd = open("data.bin", O_RDONLY);
struct stat st;
fstat(fd, &st);
// 把整个文件映射进来
char *p = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
close(fd); // 映射建立后,fd 可以关了
// 直接用指针读,像操作数组一样
printf("文件头:%02x %02x %02x %02x\n", p[0], p[1], p[2], p[3]);
printf("第1000个字节:%c\n", p[1000]);
munmap(p, st.st_size);
需要随机访问任意位置?直接使用 p[offset] 即可,无需调用 lseek。内核自动管理缓存,开发者无需操心缓冲区细节。
2. 用法二:修改文件内容(MAP_SHARED 写回)
int fd = open("data.bin", O_RDWR);
ftruncate(fd, 1024); // 先确保文件有这么大
char *p = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 直接写,内核会自动同步回磁盘
p[0] = 'H';
p[1] = 'i';
// 想立刻刷盘,不等内核调度?
msync(p, 1024, MS_SYNC); // 同步等待刷完
munmap(p, 1024);
close(fd);
这正是 SQLite 等数据库的底层原理之一。它们通过 mmap 将数据库文件映射到内存,直接修改内存页,由操作系统负责将脏页刷回磁盘,从而省去了大量 write() 系统调用的开销。
3. 用法三:父子进程共享内存
过去使用 System V 的 shmget 接口,不仅繁琐,还需要手动清理内核资源。现代写法则简洁得多:
// 在 fork() 前创建,子进程自动继承
int *shared = mmap(NULL, sizeof(int),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
-1, 0);
*shared = 0;
if (fork() == 0) {
*shared = 42; // 子进程写入
printf("子进程写:%d\n", *shared);
} else {
wait(NULL);
printf("父进程读到:%d\n", *shared); // 输出 42
}
munmap(shared, sizeof(int)); // 进程退出自动释放,无需手动清理
这种方式简洁、优雅,且不会留下任何内核垃圾。
4. 用法四:你每天都在用,但完全不知道
当你敲下 ./my_program 执行一个程序时,内核是如何将代码加载进内存的?
答案就是:mmap。
ELF 可执行文件(磁盘)
┌──────────────────┐
│ .text 代码段 │ ──→ MAP_PRIVATE mmap ──→ 进程虚拟空间[代码段]
│ .rodata 只读数据 │ ──→ MAP_PRIVATE mmap ──→ 进程虚拟空间[只读段]
│ .data 数据段 │ ──→ MAP_PRIVATE mmap ──→ 进程虚拟空间[数据段]
└──────────────────┘
动态库 libc.so
│ │ ──→ MAP_SHARED mmap ──→ 所有进程共享同一份代码页
为什么使用 MAP_PRIVATE?这涉及到写时复制(Copy-on-Write)机制。100 个进程都使用 libc,但代码页在物理内存中只有一份。只有当某个进程试图写入(例如进行重定位)时,内核才会为其复制一份独立的物理页。
这就是为什么启动 100 个 Nginx worker 进程,其内存占用远小于单进程内存占用的 100 倍。
五、mmap 最聪明的地方:它是懒的
mmap 调用本身非常快。它并不会立刻将文件内容读入内存,而只是在进程的地址空间里“登记”一段映射关系,物理内存上什么也没发生。
调用 mmap()
↓
在进程虚拟地址空间登记一段映射(物理内存:什么都没发生)
↓
你第一次访问 p[0]
↓
CPU 触发【缺页中断】(Page Fault)
↓
内核:把文件对应那 4KB 从磁盘读进 Page Cache,建立映射
↓
你的程序继续执行,感知不到任何中断
即使映射一个 1GB 的文件,也只有程序真正访问过的内存页才会被加载。其余部分,则安静地留在磁盘上。
如果明确知道访问模式,还可以给内核一些“提示”来优化性能:
madvise(p, size, MADV_SEQUENTIAL); // 告诉内核:我要顺序读,提前预读
madvise(p, size, MADV_DONTNEED); // 告诉内核:这段我不需要了,释放吧
六、写时复制:fork 为什么这么快?
MAP_PRIVATE 标志配合着一个精妙的机制:写时复制(Copy-on-Write, COW)。
char *p = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE, fd, 0);
p[0] = 'X'; // 这个修改,不会写回文件
这个过程发生了什么:
初始:进程 A 的虚拟地址 ──指向──→ 原始物理页(文件内容)
写入 p[0] = 'X' 时:
① 内核偷偷复制一份新的物理页
② 把进程 A 的页表指向新页
③ 在新页上写入 'X'
④ 原始文件的物理页,纹丝不动
进程 A 看到的:已修改的副本
文件里的:原始内容,未受影响
fork() 系统调用正是利用了这个机制。子进程创建时,与父进程共享所有物理内存页,不做任何复制。只有当某一方真正尝试写入某个内存页时,内核才会复制该页。
Redis 的 RDB 持久化就依赖于此。fork 一个子进程负责将数据写入磁盘,父进程继续处理请求。只有父进程修改的内存页才会被复制,其余部分保持共享。这就是 Redis 在执行快照时,服务几乎无停顿的秘密。
七、谁在用 mmap?(你每天都在打交道的)
1. MySQL / InnoDB
InnoDB 的 Buffer Pool 使用 mmap 分配大块内存。读写数据文件时直接操作内存页,由操作系统负责脏页刷盘,从而大幅减少了 write() 系统调用的开销。
2. Redis
如前所述,fork 子进程进行 RDB 快照时,父子进程通过 COW 共享内存页。主进程几乎无感知,子进程则从容地将数据写入磁盘。这一切都依赖于 mmap 和 COW 的协同工作。
3. Nginx
Nginx 的多个 worker 进程之间需要共享状态数据,例如限流计数、SSL 会话缓存、连接数统计。这些共享数据区域,底层都是通过 mmap(MAP_SHARED | MAP_ANONYMOUS) 分配的,使得所有 worker 进程能看到同一块内存,无需复杂的进程间通信。
顺带一提,Nginx 发送静态文件使用的是另一个技术——sendfile。它能在内核态直接将数据送到网卡,是比 mmap 更彻底的零拷贝。两者是独立的机制,各司其职。
4. 你的每一行代码
你编写的每一个 C/C++ 程序运行时,其代码段、数据段以及所有动态库,都是通过 mmap 映射进内存的,而非通过 read 加载。
当你使用 malloc 申请超过 128KB 的内存时,glibc 底层调用的也是 mmap(MAP_PRIVATE | MAP_ANONYMOUS),而非 brk()。
可以说,mmap 无处不在,只是它通常隐藏在系统底层,不被轻易察觉。
八、使用 mmap 的几个大坑,别踩
坑1:映射空文件会触发 SIGBUS
// 错误:文件是空的,一访问就崩
int fd = open("new.dat", O_RDWR | O_CREAT, 0666);
char *p = mmap(NULL, 4096, PROT_WRITE, MAP_SHARED, fd, 0);
p[0] = 'a'; // SIGBUS
// 正确:先设置文件大小
ftruncate(fd, 4096);
char *p = mmap(NULL, 4096, PROT_WRITE, MAP_SHARED, fd, 0);
p[0] = 'a'; // OK
坑2:offset 必须是页大小的整数倍
long page_size = sysconf(_SC_PAGESIZE); // 通常是 4096
mmap(NULL, len, PROT_READ, MAP_SHARED, fd, page_size * 2); // 正确
mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 100); // 有问题: EINVAL
坑3:munmap 之后指针就是野指针
char *p = mmap(...);
munmap(p, size);
char c = p[0]; // Segfault,未定义行为
坑4:小文件用 mmap 反而更慢
mmap 建立映射、处理缺页中断都有固定开销。对于小于几十KB的文件,老老实实使用 read() 通常效率更高。mmap 的优势在于处理大文件、随机访问以及多进程共享的场景。
九、mmap vs 传统 read/write,一张表看懂
(此处保留原文中关于对比的表格或描述,因原文未提供具体表格内容,故保持结构提示)
十、最后,一句话把 mmap 讲透
传统 I/O:磁盘 ──DMA──→ 内核 Page Cache ──CPU拷贝──→ 你的 buf[]
↑
这次可以省掉
mmap:磁盘 ──DMA──→ 内核 Page Cache
↕ 页表映射(零拷贝)
你的程序直接“看到”这里
说到底,mmap 并非什么神秘的黑科技。它的本质在于:用虚拟内存的页表映射机制,替代了一次不必要的数据拷贝。
理解了 mmap,也就理解了:
- 为什么数据库倾向于直接操作内存页,而非反复调用 read/write。
- 为什么 100 个进程共用同一个动态库,内存占用不会简单乘以 100。
- 为什么 Redis 在 fork 子进程持久化时,主进程服务几乎不会卡顿。
- 为什么在 Linux 上运行一个程序,其速度远比想象中要快。
虚拟内存、物理内存、文件系统——在 Linux 中,这三者被统一在同一套精密的机制下管理。而 mmap,正是打通这三者壁垒的那把关键钥匙。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
智界首款MPV V9座椅功能曝光 支持大床模式
智界V9座椅设计解析:一台大型MPV的空间魔术 智界汽车最近揭晓了其首款MPV车型V9的内部座椅设计与功能细节。作为一款瞄准大型MPV市场的新选手,V9在空间利用和座椅灵活性上,确实拿出了不少值得细看的东西。 先看基础体格。V9的车身尺寸相当标准:长度5359毫米,宽度2009毫米,高度则有1859
红色沙漠暗藏刺客信条袖剑彩蛋,绯红追击者手套引玩家热议
红色沙漠中的隐秘彩蛋:一件向经典致敬的“袖剑” 最近,在体验热门游戏《红色沙漠》时,不少细心的玩家有了一个有趣的发现:在这片广阔的开放世界里,竟然藏着一件对经典动作系列的“致敬品”——外形酷似《刺客信条》标志性装备的袖剑。 如何获取“绯红追击者锁链手套” 这件名为“绯红追击者锁链手套”的道具,位置相
图解 epoll:从 select 到 epoll,一篇讲透 Linux 高性能 I/O
从 Select 到 Epoll:深入理解 Linux 高并发网络模型的核心演进 在服务器开发领域,有一个问题几乎成了面试官的“必考题”:“为什么 Nginx 能同时处理几万个并发连接?” 如果你的回答停留在“因为它用了 epoll”,那么下一个问题通常会接踵而至:“epoll 为什么比 selec
VTDR6135亮相ICDT 2026 云英谷斩获年度最佳显示组件产品银奖
近日,2026年国际显示技术大会(ICDT)在重庆圆满落幕。云英谷VTDR6135 AMOLED显示驱动芯片凭借在显示组件领域的技术实力与创新表现,荣获SID中国区显示行业六大奖项(China Display Industry Award, 简称CDIA)中的“年度最佳显示组件产品奖”银奖。 SID
被忽视的"数字战场":为什么车联网靶场将成为智能汽车时代的护城河?
中国车联网靶场:从合规工具到战略基础设施的跨越之路 未来三到五年,国内车联网靶场市场将迎来一场深刻的蜕变。其目标不再是追赶,而是实现战略上的并跑,甚至在某些领域引领。驱动这场变革的,将是人工智能、强制性法规与开放生态的协同共振。最终,车联网靶场将彻底摆脱“孤立测试工具”的旧标签,演进为支撑整个产业安
- 日榜
- 周榜
- 月榜
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
相关攻略
2015-03-10 11:25
2015-03-10 11:05
2021-08-04 13:30
2015-03-10 11:22
2015-03-10 12:39
2022-05-16 18:57
2025-05-23 13:43
2025-05-23 14:01
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题

