在学习 Linux 内存管理、多线程或高性能 I/O 时,mmap() 是一个绕不开的系统调用。很多人第一次接触它,都会把它理解成"另一种 malloc",但实际上 mmap 是 Linux 虚拟内存机制中最核心、最基础的接口之一。
本文将从是什么、能做什么、怎么用、为什么重要四个层次,系统地梳理 mmap。
一、一句话理解 mmap
mmap()的本质不是"分配内存",而是建立一段虚拟地址空间与某种资源之间的映射关系。
这里的"资源"可以是:
-
磁盘文件
-
匿名内存(不对应任何文件)
-
设备(如共享内存、显存等)
一旦映射建立,程序就可以像访问普通内存一样访问这些资源。
二、mmap 在 Linux 中处于什么位置?
从抽象层次看:
应用程序
↓
libc (malloc / fopen / pthread)
↓
mmap / brk / read / write ← 关键接口
↓
虚拟内存系统(页表 / 缺页异常)
↓
物理内存 / 磁盘 / 设备
mmap 是连接"用户程序"与"虚拟内存系统"的桥梁。
三、mmap 的两种核心用法
文件映射(File-backed mapping)
int fd = open("data.bin", O_RDONLY);
void *addr = mmap(NULL, size,
PROT_READ,
MAP_PRIVATE,
fd, 0);
含义:
-
把文件的一部分映射进进程地址空间
-
对内存的访问 ≈ 对文件的访问
特点:
-
不需要
read()/write() -
利用页缓存,按需加载
-
支持零拷贝
典型应用:
-
加载共享库(
.so) -
大文件随机访问
-
数据库、搜索引擎
匿名映射(Anonymous mapping)
void *addr = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
含义:
-
映射一段"无来源"的内存
-
不对应任何磁盘文件
这正是:
-
线程栈
-
大块动态内存
-
共享内存
的底层来源。
四、mmap 和 malloc 的关系
这是一个经典问题。
| 对比项 | malloc | mmap |
|---|---|---|
| 层级 | 库函数 | 系统调用 |
| 小内存 | 使用 brk 扩展堆 | 不适合 |
| 大内存 | 内部直接用 mmap | 非常适合 |
| 回收 | 可能延迟 | munmap 立即释放 |
| 碎片 | 容易产生 | 相对较少 |
glibc 的 malloc 在分配大块内存时,本身就会调用 mmap。
五、为什么 mmap 不"立刻占用"物理内存?
这是理解虚拟内存的关键。
mmap → 建立映射关系
访问 → 缺页异常
缺页 → 分配物理页
特点:
-
mmap 本身几乎是 O(1)
-
物理内存按需分配
-
未访问的映射不消耗 RAM
这也是 mmap 高效、可扩展 的根本原因。
六、mmap 与多线程:线程栈从哪里来?
在 Linux 中:
-
主线程栈:由内核在
execve时建立 -
新线程栈:
-
由 pthread 库
-
使用
mmap(MAP_ANONYMOUS)分配
-
在 /proc/<pid>/maps 中常见:
7ffde9c1d000-7ffde9c3e000 rw-p ... [stack]
7f8c2a400000-7f8c2ac00000 rw-p ... [stack:tid]
所谓"线程栈在共享映射区",指的正是这些 mmap 出来的匿名映射区域。
七、为什么 mmap 比 read/write 快?
read/write 至少需要一次"内核缓冲区 → 用户缓冲区"的数据拷贝;
mmap 让用户进程直接访问内核页缓存中的数据,从而避免了这一次拷贝。
先看 read/write 的真实路径
假设你调用:
read(fd, user_buf, size);
实际发生的事情是:
磁盘
↓ DMA
页缓存(Page Cache) ← 内核态
↓ memcpy
用户缓冲区 user_buf ← 用户态
关键点来了:
-
磁盘 → 页缓存
-
这是 DMA
-
必须有(磁盘不能直接 DMA 到用户空间)
-
-
页缓存 → user_buf
-
这是一次 CPU 拷贝
-
跨内核态 / 用户态边界
-
成本高、不可避免(对 read/write)
-
这一步就是大家说的"用户态 ↔ 内核态拷贝"
mmap 的路径:拷贝去哪了?
现在换成 mmap:
char *p = mmap(...);
char x = p[0];
实际路径是:
磁盘
↓ DMA
页缓存(Page Cache)
↑
用户进程直接访问(VA → 同一物理页)
关键差异
-
没有 memcpy
-
用户虚拟地址 直接映射到页缓存中的物理页
-
CPU 只是做一次普通的内存 load/store
页缓存既是"内核缓冲区",也是"用户可见内存"
所以 mmap 到底"避免"了哪一次拷贝?
我们精确地说:
| 阶段 | read/write | mmap |
|---|---|---|
| 磁盘 → 内核页缓存 | 必须 | 必须 |
| 内核 → 用户 | memcpy | 无拷贝 |
| 用户访问 | 普通内存 | 普通内存 |
mmap 避免的是:
页缓存 → 用户缓冲区 的那次数据复制
为什么这次拷贝"特别贵"?
CPU 成本高
-
memcpy 是:
-
逐字节 / cache line 拷贝
-
占用 CPU
-
-
大文件 → 明显拖慢程序
Cache 污染
-
read/write:
-
数据被复制到 user_buf
-
cache 中出现两份相同数据
-
-
mmap:
- 只有一份物理页
NUMA / 大内存下更明显
-
大页拷贝跨 NUMA 节点
-
mmap 直接访问本地页缓存
八、从内核视角看 mmap(一句话)
mmap 的作用,是在进程页表中记录一条规则:
"当访问这段虚拟地址时,应当如何处理该访问。"
-
从文件读?
-
分配匿名页?
-
是否共享?
-
是否写时复制?
九、常见误区澄清
❌ mmap = 分配物理内存
✅ mmap = 建立虚拟地址映射
❌ mmap 只能映射文件
✅ 匿名内存是最常见用途之一
❌ 线程栈是特殊区域
✅ 在线程实现中,它只是 mmap 出来的一块内存
十、总结
mmap 是 Linux 虚拟内存机制的核心接口。
它不仅支撑了文件映射、高性能 I/O,也支撑了线程栈、动态内存分配和共享内存。