当前位置: 首页
业界动态
深入 mmap:被严重低估的 Linux 黑科技,MySQL/Redis/Nginx 都在用它

深入 mmap:被严重低估的 Linux 黑科技,MySQL/Redis/Nginx 都在用它

热心网友 时间:2026-04-17
转载

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,正是打通这三者壁垒的那把关键钥匙。

来源:https://www.51cto.com/article/837615.html

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
智界首款MPV V9座椅功能曝光 支持大床模式

智界首款MPV V9座椅功能曝光 支持大床模式

智界V9座椅设计解析:一台大型MPV的空间魔术 智界汽车最近揭晓了其首款MPV车型V9的内部座椅设计与功能细节。作为一款瞄准大型MPV市场的新选手,V9在空间利用和座椅灵活性上,确实拿出了不少值得细看的东西。 先看基础体格。V9的车身尺寸相当标准:长度5359毫米,宽度2009毫米,高度则有1859

时间:2026-04-17 10:24
红色沙漠暗藏刺客信条袖剑彩蛋,绯红追击者手套引玩家热议

红色沙漠暗藏刺客信条袖剑彩蛋,绯红追击者手套引玩家热议

红色沙漠中的隐秘彩蛋:一件向经典致敬的“袖剑” 最近,在体验热门游戏《红色沙漠》时,不少细心的玩家有了一个有趣的发现:在这片广阔的开放世界里,竟然藏着一件对经典动作系列的“致敬品”——外形酷似《刺客信条》标志性装备的袖剑。 如何获取“绯红追击者锁链手套” 这件名为“绯红追击者锁链手套”的道具,位置相

时间:2026-04-17 10:20
图解 epoll:从 select 到 epoll,一篇讲透 Linux 高性能 I/O

图解 epoll:从 select 到 epoll,一篇讲透 Linux 高性能 I/O

从 Select 到 Epoll:深入理解 Linux 高并发网络模型的核心演进 在服务器开发领域,有一个问题几乎成了面试官的“必考题”:“为什么 Nginx 能同时处理几万个并发连接?” 如果你的回答停留在“因为它用了 epoll”,那么下一个问题通常会接踵而至:“epoll 为什么比 selec

时间:2026-04-17 09:07
VTDR6135亮相ICDT 2026 云英谷斩获年度最佳显示组件产品银奖

VTDR6135亮相ICDT 2026 云英谷斩获年度最佳显示组件产品银奖

近日,2026年国际显示技术大会(ICDT)在重庆圆满落幕。云英谷VTDR6135 AMOLED显示驱动芯片凭借在显示组件领域的技术实力与创新表现,荣获SID中国区显示行业六大奖项(China Display Industry Award, 简称CDIA)中的“年度最佳显示组件产品奖”银奖。 SID

时间:2026-04-17 09:06
被忽视的"数字战场":为什么车联网靶场将成为智能汽车时代的护城河?

被忽视的"数字战场":为什么车联网靶场将成为智能汽车时代的护城河?

中国车联网靶场:从合规工具到战略基础设施的跨越之路 未来三到五年,国内车联网靶场市场将迎来一场深刻的蜕变。其目标不再是追赶,而是实现战略上的并跑,甚至在某些领域引领。驱动这场变革的,将是人工智能、强制性法规与开放生态的协同共振。最终,车联网靶场将彻底摆脱“孤立测试工具”的旧标签,演进为支撑整个产业安

时间:2026-04-17 09:02
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 日榜
  • 周榜
  • 月榜
热门教程
更多
  • 游戏攻略
  • 安卓教程
  • 苹果教程
  • 电脑教程