从系统调用原型到内核实现,从四种映射模式到实战 demo,全面剖析 Linux mmap 内存映射技术。
一、什么是 mmap
mmap(Memory Mapped I/O)是 Linux 提供的一种将文件或设备 映射到进程虚拟地址空间的机制。映射建立后,进程可以像访问普通内存一样直接读写文件内容,不需要调用 read()/write() 系统调用。
一句话概括 :mmap 让你用指针操作文件。
传统 I/O:
用户空间 ←→ 内核缓冲区 ←→ 磁盘 (两次拷贝 + 系统调用)
mmap:
用户空间 ←→ 页缓存 ←→ 磁盘 (零拷贝,直接访问页缓存)
二、系统调用原型
c
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
int msync(void *addr, size_t length, int flags);
int mprotect(void *addr, size_t length, int prot);
int madvise(void *addr, size_t length, int advice);
2.1 mmap 参数详解
c
void *mmap(
void *addr, // 建议映射起始地址,通常传 NULL(由内核选择)
size_t length, // 映射长度(字节),会向上对齐到页大小(通常 4096)
int prot, // 内存保护标志
int flags, // 映射类型和行为标志
int fd, // 文件描述符(匿名映射时传 -1)
off_t offset // 文件偏移量(必须是页大小的整数倍)
);
// 返回值:成功返回映射区域起始地址,失败返回 MAP_FAILED((void *)-1)
2.2 prot 保护标志
| 标志 | 值 | 含义 |
|---|---|---|
PROT_NONE |
0x0 | 不可访问 |
PROT_READ |
0x1 | 可读 |
PROT_WRITE |
0x2 | 可写 |
PROT_EXEC |
0x4 | 可执行 |
可以用 | 组合,例如 PROT_READ | PROT_WRITE 表示可读写。
约束 :prot 不能超出 open() 时文件描述符的权限。例如以 O_RDONLY 打开的文件,不能映射为 PROT_WRITE(MAP_SHARED 时会返回 EACCES)。
2.3 flags 映射标志
| 标志 | 含义 |
|---|---|
MAP_SHARED |
共享映射:修改对其他映射同一文件的进程可见,修改会写回文件 |
MAP_PRIVATE |
私有映射:修改不影响原文件,不对其他进程可见(Copy-On-Write) |
MAP_ANONYMOUS |
匿名映射:不关联文件,fd 必须为 -1(用于分配纯内存) |
MAP_FIXED |
强制使用 addr 参数指定的地址(危险,可能覆盖已有映射) |
MAP_FIXED_NOREPLACE |
类似 MAP_FIXED,但如果地址已被占用则失败(Linux 4.17+) |
MAP_POPULATE |
预填充页表,避免后续缺页异常(对性能敏感场景有用) |
MAP_HUGETLB |
使用大页(Huge Pages)映射 |
MAP_LOCKED |
锁定映射页面到物理内存,不会被换出到 swap |
MAP_NORESERVE |
不预留 swap 空间 |
2.4 munmap
c
int munmap(void *addr, size_t length);
释放映射。addr 必须是页对齐的地址(通常就是 mmap 返回的地址),length 可以小于原映射长度(部分解除映射)。
2.5 msync
c
int msync(void *addr, size_t length, int flags);
// flags:
// MS_SYNC --- 同步刷盘,等待写入完成后返回
// MS_ASYNC --- 异步刷盘,通知内核尽快写入,立即返回
// MS_INVALIDATE --- 使其他映射失效(强制从文件重新读取)
MAP_SHARED 映射的修改最终会被内核自动写回文件(通过脏页回写机制),但时机不确定。msync 可以显式控制刷盘时机。
三、四种映射模式
mmap 的两个关键维度是文件 vs 匿名 和共享 vs 私有,组合出四种模式:
文件映射 匿名映射
(关联一个文件) (不关联文件,fd=-1)
┌────────────────────┬────────────────────┐
共享 │ MAP_SHARED │ MAP_SHARED | │
(SHARED) │ + 文件 fd │ MAP_ANONYMOUS │
│ │ │
│ 用途:多进程共享 │ 用途:父子进程 │
│ 文件内容;IPC 通信 │ 共享内存 │
├────────────────────┼────────────────────┤
私有 │ MAP_PRIVATE │ MAP_PRIVATE | │
(PRIVATE)│ + 文件 fd │ MAP_ANONYMOUS │
│ │ │
│ 用途:加载 .so、 │ 用途:malloc 大块 │
│ 读取配置文件 │ 内存分配 │
└────────────────────┴────────────────────┘
3.1 MAP_SHARED + 文件(共享文件映射)
特点:
- 多个进程映射同一文件时,看到的是同一块物理内存
- 任何进程的修改,其他进程立刻可见
- 修改会被内核写回磁盘(通过脏页回写或 msync)
典型用途:进程间通信(IPC)、数据库文件(如 SQLite WAL)、OpenHarmony param 系统
3.2 MAP_PRIVATE + 文件(私有文件映射)
特点:
- 读取时共享同一物理页面(节省内存)
- 写入时触发 Copy-On-Write(COW):内核复制一份私有副本,修改只对当前进程可见
- 原始文件不会被修改
典型用途 :动态链接库 .so 加载(代码段共享,数据段 COW)、只读配置文件读取
3.3 MAP_SHARED + MAP_ANONYMOUS(共享匿名映射)
特点:
- 不关联任何文件,纯内存区域
fork()后父子进程共享这块内存
典型用途:父子进程间通信
3.4 MAP_PRIVATE + MAP_ANONYMOUS(私有匿名映射)
特点:
- 不关联文件,纯内存
fork()后父子进程各自独立(COW)
典型用途 :malloc() 分配大块内存时底层调用。glibc 中当 malloc 请求超过 MMAP_THRESHOLD(默认 128KB)时,会使用 mmap 代替 brk。
四、内核实现原理
4.1 虚拟内存与页表
进程虚拟地址空间(64位系统)
┌──────────────────┐ 0xFFFFFFFFFFFFFFFF
│ 内核空间 │
├──────────────────┤ 0xFFFF800000000000(典型分界)
│ │
│ 栈 (stack) │ ← 向下增长
│ ↓ │
│ ...空闲... │
│ ↑ │
│ mmap 映射区域 │ ← mmap 分配的内存在这里
│ ↑ │
│ ...空闲... │
│ ↑ │
│ 堆 (heap) │ ← brk/sbrk 向上增长
│ │
│ BSS 段 │
│ 数据段 │
│ 代码段 (.text) │
├──────────────────┤
│ NULL 保护页 │
└──────────────────┘ 0x0000000000000000
mmap() 的本质是在进程的虚拟地址空间中创建一个 VMA(Virtual Memory Area)结构体,记录映射的起始地址、长度、权限、关联的文件等信息。此时并不分配物理内存,也不读取文件内容。
4.2 缺页异常(Page Fault)
真正的物理内存分配发生在进程首次访问映射地址时:
进程访问 mmap 地址
↓
CPU 查页表 → 页表项为空(PTE 无效)
↓
触发缺页异常(Page Fault)
↓
内核缺页处理程序(do_page_fault → handle_mm_fault)
↓
┌─ 文件映射:
│ 1. 分配物理页面
│ 2. 从磁盘读取文件内容填充页面(page cache)
│ 3. 建立页表映射:虚拟地址 → 物理页面
│
└─ 匿名映射:
1. 分配物理页面(或映射到零页 zero page)
2. 清零页面内容
3. 建立页表映射
进程恢复执行,访问成功
关键点 :这就是为什么 mmap 返回很快(只创建 VMA,不做 I/O),而首次访问可能稍慢(触发缺页异常 + 磁盘 I/O)。
4.3 Copy-On-Write(COW)
MAP_PRIVATE 映射的写操作流程:
进程 A 和 B 都映射了同一文件(MAP_PRIVATE)
初始状态:
进程 A 的页表 ──→ 物理页 P(只读标记)
进程 B 的页表 ──→ 物理页 P(只读标记)
进程 A 写入:
1. CPU 检测到写入只读页 → 触发保护异常
2. 内核分配新物理页 P'
3. 复制 P 的内容到 P'
4. 更新进程 A 的页表:指向 P'(标记为可读写)
5. 进程 B 的页表仍指向 P(不受影响)
结果:A 看到修改后的值,B 看到原始值
4.4 脏页回写(Dirty Page Writeback)
什么是"脏页"
Linux 把内存划分成 4KB 大小的页(Page)。当一个页的内容被修改了,但还没写回磁盘,这个页就被标记为**"脏"(dirty)**。
内存 磁盘
┌──────────────────────┐ ┌──────────────────────┐
│ 页 A: "Hello" [干净] │ ══════════│ 文件偏移0: "Hello" │ 内容一致
│ 页 B: "MMAP!" [脏] │ ≠≠≠≠≠≠≠≠≠│ 文件偏移4K: "World" │ 内容不一致!
│ 页 C: "Foo" [干净] │ ══════════│ 文件偏移8K: "Foo" │ 内容一致
└──────────────────────┘ └──────────────────────┘
页 B 被修改了(内存是 "MMAP!" 但磁盘还是 "World"),所以是"脏页"
脏页的产生途径:
- mmap 写入 :进程通过
MAP_SHARED映射文件后,直接往内存地址写数据,CPU 自动在页表项(PTE)上设置 dirty bit - write() 系统调用:内核先把数据写到 page cache 中的对应页,然后标记为脏
回写时机
脏页不会立刻写回磁盘(那样太慢了),内核会攒一批再写,时机由以下条件控制:
脏页产生
│
├── 条件 1:脏页存在超过 30 秒
│ (vm.dirty_expire_centisecs = 3000,即 30 秒)
│ → 内核后台线程定期扫描,过期的脏页被回写
│
├── 条件 2:脏页占总内存的比例超过 10%
│ (vm.dirty_background_ratio = 10)
│ → 内核后台线程开始异步回写,不阻塞应用
│
├── 条件 3:脏页占总内存的比例超过 20%
│ (vm.dirty_ratio = 20)
│ → 产生脏页的进程被阻塞,强制同步回写(很慢!)
│
├── 条件 4:应用主动调用
│ → msync(MS_SYNC) --- 把指定 mmap 区域的脏页刷盘
│ → fsync(fd) --- 把指定文件的脏页刷盘
│ → sync() --- 把所有脏页刷盘
│
└── 条件 5:munmap / 文件关闭
→ 触发回写(但不保证立即完成)
回写过程
内核 writeback 线程(如 kworker/flush)
│
▼
遍历脏页链表
│
▼
对每个脏页调用文件系统的 writepage()
│
▼
文件系统提交 I/O 请求到块设备层
│
▼
磁盘控制器执行实际写入
│
▼
清除页的 dirty 标记 → 变成"干净页"
跟 mmap 的关系
当你用 MAP_SHARED 映射文件并写入时:
c
char *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
memcpy(p, "Hello", 5); // ← 这一刻,内存中的页变成了脏页
// 但磁盘上的文件还没变
// ... 最多 30 秒后,内核自动把这个脏页写回磁盘
// 或者你主动调用:
msync(p, 4096, MS_SYNC); // ← 立刻强制刷盘,返回时保证磁盘已更新
如果不调 msync,数据可能在脏页还没回写的时候突然断电,修改就丢了。 这就是为什么对数据安全性要求高的场景(数据库、OpenHarmony 的 persist 参数)都会主动调用 msync 或 fsync 来确保持久化。
查看脏页状态
bash
# 查看当前系统的脏页数量
$ cat /proc/meminfo | grep Dirty
Dirty: 1024 kB ← 当前有 1MB 脏页还没写回磁盘
# 查看回写相关内核参数
$ sysctl vm.dirty_ratio
vm.dirty_ratio = 20 ← 脏页超过 20% 内存时阻塞写入进程
$ sysctl vm.dirty_background_ratio
vm.dirty_background_ratio = 10 ← 脏页超过 10% 内存时后台开始异步回写
$ sysctl vm.dirty_expire_centisecs
vm.dirty_expire_centisecs = 3000 ← 脏页超过 30 秒自动回写
4.5 TLB(Translation Lookaside Buffer)
每次虚拟地址到物理地址的转换都需要查页表(多级页表可能需要 4-5 次内存访问),为了加速,CPU 使用 TLB 缓存最近的映射关系。munmap 或 mprotect 时需要刷新 TLB(flush_tlb_range),这是一个相对昂贵的操作。
五、实战 Demo
5.1 Demo 1:用 mmap 替代 read/write 读写文件
c
/**
* demo_file_rw.c --- 用 mmap 读取和修改文件内容
*
* 编译: gcc demo_file_rw.c -o demo_file_rw
* 准备: echo "Hello, this is a test file for mmap demo." > /tmp/mmap_test.txt
* 运行: ./demo_file_rw /tmp/mmap_test.txt
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "用法: %s <文件路径>\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDWR);
if (fd < 0) { perror("open"); return 1; }
struct stat sb;
fstat(fd, &sb);
size_t file_size = sb.st_size;
printf("文件大小: %zu 字节\n", file_size);
/* mmap 映射整个文件 */
char *mapped = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd); /* 映射建立后 fd 可以关闭 */
if (mapped == MAP_FAILED) { perror("mmap"); return 1; }
/* 直接像操作内存一样读取文件 */
printf("文件内容: %.*s\n", (int)file_size, mapped);
/* 直接修改内存 = 修改文件(MAP_SHARED) */
if (file_size >= 5) {
memcpy(mapped, "MMAP!", 5);
printf("已将前5字节改为 'MMAP!'\n");
}
/* msync 确保修改写入磁盘 */
msync(mapped, file_size, MS_SYNC);
/* 释放映射 */
munmap(mapped, file_size);
printf("验证: cat %s 查看修改结果\n", argv[1]);
return 0;
}
运行效果:
bash
$ echo "Hello, this is a test file for mmap demo." > /tmp/mmap_test.txt
$ gcc demo_file_rw.c -o demo_file_rw && ./demo_file_rw /tmp/mmap_test.txt
文件大小: 43 字节
文件内容: Hello, this is a test file for mmap demo.
已将前5字节改为 'MMAP!'
验证: cat /tmp/mmap_test.txt 查看修改结果
$ cat /tmp/mmap_test.txt
MMAP!, this is a test file for mmap demo.
5.2 Demo 2:两个进程通过 MAP_SHARED 通信
写入端(ipc_writer.c):
c
/**
* ipc_writer.c --- 通过共享文件映射实现进程间通信(写入端)
*
* 编译: gcc ipc_writer.c -o ipc_writer
* 运行: ./ipc_writer
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdatomic.h>
#define SHM_FILE "/dev/shm/ipc_demo"
#define SHM_SIZE 4096
typedef struct {
atomic_int version; /* 版本号,每次写入递增 */
int data_len; /* 数据长度 */
char data[0]; /* 柔性数组 */
} SharedBlock;
int main()
{
int fd = open(SHM_FILE, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { perror("open"); return 1; }
ftruncate(fd, SHM_SIZE);
SharedBlock *blk = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
if (blk == MAP_FAILED) { perror("mmap"); return 1; }
/* 初始化 */
atomic_store(&blk->version, 0);
blk->data_len = 0;
const char *messages[] = {
"第一条消息:mmap 是 Linux 最强大的系统调用之一",
"第二条消息:OpenHarmony param 系统基于 mmap 实现",
"第三条消息:mmap 实现了零拷贝的进程间通信",
"EXIT",
NULL
};
for (int i = 0; messages[i]; i++) {
sleep(1); /* 每秒写入一条 */
int len = strlen(messages[i]);
memcpy(blk->data, messages[i], len + 1);
blk->data_len = len;
/* 原子递增版本号(通知读取端有新数据) */
atomic_fetch_add(&blk->version, 1);
printf("[writer] v%d: %s\n", atomic_load(&blk->version), messages[i]);
}
munmap(blk, SHM_SIZE);
return 0;
}
读取端(ipc_reader.c):
c
/**
* ipc_reader.c --- 通过共享文件映射实现进程间通信(读取端)
*
* 编译: gcc ipc_reader.c -o ipc_reader
* 运行: ./ipc_reader (先启动 writer,再启动 reader)
*
* 关键点:reader 不需要任何 IPC 机制,直接读共享内存
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdatomic.h>
#define SHM_FILE "/dev/shm/ipc_demo"
#define SHM_SIZE 4096
typedef struct {
atomic_int version;
int data_len;
char data[0];
} SharedBlock;
int main()
{
int fd = open(SHM_FILE, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "请先运行 ipc_writer\n");
return 1;
}
/* 只读映射 */
const SharedBlock *blk = mmap(NULL, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0);
close(fd);
if (blk == MAP_FAILED) { perror("mmap"); return 1; }
int last_version = 0;
printf("[reader] 等待消息...\n");
while (1) {
int cur = atomic_load(&blk->version);
if (cur > last_version) {
printf("[reader] v%d: %.*s\n", cur, blk->data_len, blk->data);
if (strcmp(blk->data, "EXIT") == 0) break;
last_version = cur;
}
usleep(50000); /* 50ms 轮询 */
}
munmap((void *)blk, SHM_SIZE);
printf("[reader] 通信结束\n");
return 0;
}
运行效果:
bash
# 终端1
$ gcc ipc_writer.c -o ipc_writer && ./ipc_writer
[writer] v1: 第一条消息:mmap 是 Linux 最强大的系统调用之一
[writer] v2: 第二条消息:OpenHarmony param 系统基于 mmap 实现
[writer] v3: 第三条消息:mmap 实现了零拷贝的进程间通信
[writer] v4: EXIT
# 终端2
$ gcc ipc_reader.c -o ipc_reader && ./ipc_reader
[reader] 等待消息...
[reader] v1: 第一条消息:mmap 是 Linux 最强大的系统调用之一
[reader] v2: 第二条消息:OpenHarmony param 系统基于 mmap 实现
[reader] v3: 第三条消息:mmap 实现了零拷贝的进程间通信
[reader] v4: EXIT
[reader] 通信结束
5.3 Demo 3:MAP_PRIVATE 的 COW 行为验证
c
/**
* demo_cow.c --- 验证 MAP_PRIVATE 的 Copy-On-Write 行为
*
* 编译: gcc demo_cow.c -o demo_cow
* 运行: ./demo_cow
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
int main()
{
/* 创建测试文件 */
const char *path = "/tmp/cow_test.txt";
int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
const char *init_data = "AAAAAAAAAA"; /* 10 个 A */
write(fd, init_data, strlen(init_data));
/* MAP_PRIVATE 映射 */
char *mapped = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
close(fd);
if (mapped == MAP_FAILED) { perror("mmap"); return 1; }
printf("[parent] 初始值: %.10s\n", mapped);
fflush(stdout); /* fork 前必须刷新缓冲区,否则子进程会重复输出 */
pid_t pid = fork();
if (pid == 0) {
/* 子进程:修改映射内容 */
memcpy(mapped, "CHILD_MOD!", 10);
printf("[child] 修改后: %.10s\n", mapped);
munmap(mapped, 4096);
_exit(0); /* 子进程用 _exit 避免再次刷新 stdio 缓冲 */
}
waitpid(pid, NULL, 0);
/* 父进程:检查是否被子进程影响 */
printf("[parent] 子进程退出后: %.10s\n", mapped);
/* 检查原始文件 */
fd = open(path, O_RDONLY);
char buf[11] = {0};
read(fd, buf, 10);
close(fd);
printf("[file] 磁盘上的内容: %s\n", buf);
munmap(mapped, 4096);
unlink(path);
return 0;
}
运行效果:
bash
$ gcc demo_cow.c -o demo_cow && ./demo_cow
[parent] 初始值: AAAAAAAAAA
[child] 修改后: CHILD_MOD!
[parent] 子进程退出后: AAAAAAAAAA ← 父进程不受影响(COW)
[file] 磁盘上的内容: AAAAAAAAAA ← 文件也不受影响(MAP_PRIVATE)
5.4 Demo 4:匿名映射实现父子进程共享内存
c
/**
* demo_anonymous.c --- 匿名共享映射实现父子进程通信
*
* 编译: gcc demo_anonymous.c -o demo_anonymous
* 运行: ./demo_anonymous
*
* 不需要任何文件,纯内存通信
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <stdatomic.h>
typedef struct {
atomic_int ready;
char message[256];
} SharedData;
int main()
{
/* MAP_SHARED | MAP_ANONYMOUS:父子进程共享的匿名内存 */
SharedData *shm = mmap(NULL, sizeof(SharedData),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
-1, 0); /* fd=-1, offset=0 */
if (shm == MAP_FAILED) { perror("mmap"); return 1; }
atomic_store(&shm->ready, 0);
memset(shm->message, 0, sizeof(shm->message));
pid_t pid = fork();
if (pid == 0) {
/* 子进程:写入消息 */
strcpy(shm->message, "来自子进程的消息:匿名共享映射不需要文件!");
atomic_store(&shm->ready, 1);
printf("[child] 消息已写入\n");
_exit(0);
}
/* 父进程:等待并读取 */
while (atomic_load(&shm->ready) == 0) {
usleep(10000);
}
printf("[parent] 收到: %s\n", shm->message);
waitpid(pid, NULL, 0);
munmap(shm, sizeof(SharedData));
return 0;
}
运行效果:
bash
$ gcc demo_anonymous.c -o demo_anonymous && ./demo_anonymous
[child] 消息已写入
[parent] 收到: 来自子进程的消息:匿名共享映射不需要文件!
5.5 Demo 5:大文件高效处理(mmap vs read 性能对比)
c
/**
* demo_benchmark.c --- mmap vs read() 性能对比
*
* 编译: gcc demo_benchmark.c -O2 -o demo_benchmark
* 运行: ./demo_benchmark
*
* 创建 256MB 文件,分别用 mmap 和 read 扫描全部内容,统计耗时
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/time.h>
#define FILE_SIZE (256 * 1024 * 1024) /* 256 MB */
#define TEST_FILE "/tmp/mmap_bench.dat"
static double now_ms()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0;
}
static void create_test_file()
{
printf("创建 %d MB 测试文件...\n", FILE_SIZE / (1024 * 1024));
int fd = open(TEST_FILE, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (ftruncate(fd, FILE_SIZE) != 0) { perror("ftruncate"); return; }
/* 用 mmap 快速填充 */
char *p = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
memset(p, 'X', FILE_SIZE);
munmap(p, FILE_SIZE);
close(fd);
}
/* 方法1:mmap 扫描 */
static long long scan_mmap()
{
int fd = open(TEST_FILE, O_RDONLY);
char *p = mmap(NULL, FILE_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
/* 提示内核:顺序访问 */
madvise(p, FILE_SIZE, MADV_SEQUENTIAL);
long long sum = 0;
for (size_t i = 0; i < FILE_SIZE; i += 4096) {
sum += p[i]; /* 每页采样一个字节 */
}
munmap(p, FILE_SIZE);
return sum;
}
/* 方法2:read() 扫描 */
static long long scan_read()
{
int fd = open(TEST_FILE, O_RDONLY);
char buf[65536]; /* 64KB 缓冲区 */
long long sum = 0;
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
for (ssize_t i = 0; i < n; i += 4096) {
sum += buf[i];
}
}
close(fd);
return sum;
}
int main()
{
create_test_file();
/* 清空 page cache(需要 root) */
sync();
/* system("echo 3 > /proc/sys/vm/drop_caches"); */
double t1, t2;
/* mmap 扫描 */
t1 = now_ms();
long long s1 = scan_mmap();
t2 = now_ms();
printf("mmap 扫描: %.1f ms (校验: %lld)\n", t2 - t1, s1);
/* read 扫描 */
t1 = now_ms();
long long s2 = scan_read();
t2 = now_ms();
printf("read 扫描: %.1f ms (校验: %lld)\n", t2 - t1, s2);
unlink(TEST_FILE);
return 0;
}
典型输出(数据在 page cache 中时):
bash
$ gcc demo_benchmark.c -O2 -o demo_benchmark && ./demo_benchmark
创建 256 MB 测试文件...
mmap 扫描: 24.0 ms (校验: 5767168)
read 扫描: 49.8 ms (校验: 5767168)
mmap 通常比 read 快 2-3 倍(热数据场景),因为省去了内核态到用户态的数据拷贝。
5.6 Demo 6:mmap 实现简易环形缓冲区(Ring Buffer)
c
/**
* demo_ringbuf.c --- 利用 mmap 的虚拟地址映射特性实现无需取模的环形缓冲区
*
* 原理:将同一块物理内存映射到虚拟地址空间中连续的两倍区域,
* 这样写入超过缓冲区末尾时,自动回绕到开头,无需 % 运算。
*
* 编译: gcc demo_ringbuf.c -o demo_ringbuf
* 运行: ./demo_ringbuf
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
typedef struct {
char *buf; /* 映射基地址 */
size_t capacity; /* 缓冲区容量 */
size_t read_pos;
size_t write_pos;
} RingBuffer;
RingBuffer *ring_create(size_t capacity)
{
/* 容量必须是页大小的整数倍 */
long page_size = sysconf(_SC_PAGESIZE);
capacity = (capacity + page_size - 1) & ~(page_size - 1);
RingBuffer *rb = calloc(1, sizeof(RingBuffer));
rb->capacity = capacity;
/*
* 技巧:先 mmap 两倍大小的匿名区域占位,
* 然后用 MAP_FIXED 将同一个 fd 映射到前半和后半
*/
char path[] = "/dev/shm/ringbuf-XXXXXX";
int fd = mkstemp(path);
unlink(path); /* 立即删除文件名,fd 仍有效 */
ftruncate(fd, capacity);
/* 先占两倍虚拟地址空间 */
rb->buf = mmap(NULL, capacity * 2, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
/* 前半部分映射到 fd */
mmap(rb->buf, capacity, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);
/* 后半部分也映射到同一个 fd(关键!) */
mmap(rb->buf + capacity, capacity, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);
close(fd);
return rb;
}
void ring_destroy(RingBuffer *rb)
{
munmap(rb->buf, rb->capacity * 2);
free(rb);
}
size_t ring_write(RingBuffer *rb, const void *data, size_t len)
{
size_t avail = rb->capacity - (rb->write_pos - rb->read_pos);
if (len > avail) len = avail;
/* 直接 memcpy,即使跨越缓冲区边界也不需要分两次! */
memcpy(rb->buf + (rb->write_pos % rb->capacity), data, len);
rb->write_pos += len;
return len;
}
size_t ring_read(RingBuffer *rb, void *data, size_t len)
{
size_t used = rb->write_pos - rb->read_pos;
if (len > used) len = used;
memcpy(data, rb->buf + (rb->read_pos % rb->capacity), len);
rb->read_pos += len;
return len;
}
int main()
{
RingBuffer *rb = ring_create(4096);
char buf[256];
/* 写入一些数据 */
for (int i = 0; i < 20; i++) {
int len = snprintf(buf, sizeof(buf), "消息 #%02d: mmap 环形缓冲区真香!\n", i);
ring_write(rb, buf, len);
}
/* 读取所有数据 */
size_t n;
printf("=== 读取缓冲区内容 ===\n");
while ((n = ring_read(rb, buf, sizeof(buf) - 1)) > 0) {
buf[n] = '\0';
printf("%s", buf);
}
ring_destroy(rb);
return 0;
}
运行效果:
bash
$ gcc demo_ringbuf.c -o demo_ringbuf && ./demo_ringbuf
=== 读取缓冲区内容 ===
消息 #00: mmap 环形缓冲区真香!
消息 #01: mmap 环形缓冲区真香!
...(自动回绕,无需取模操作)
消息 #19: mmap 环形缓冲区真香!
六、mmap vs read/write 对比
| 维度 | mmap | read/write |
|---|---|---|
| 数据拷贝 | 零拷贝(直接访问 page cache) | 需要内核态→用户态拷贝 |
| 系统调用 | 仅 mmap/munmap(一次性开销) | 每次读写都需要系统调用 |
| 随机访问 | 高效(直接用指针偏移) | 低效(需要 lseek + read) |
| 顺序读取大文件 | 稍快(省拷贝开销) | 也很快(内核预读优化) |
| 小文件(<几十KB) | 不划算(mmap/munmap 开销 > read 开销) | 更简单高效 |
| 内存占用 | 占用虚拟地址空间 | 只占用用户提供的缓冲区 |
| 缺页异常 | 有(首次访问每页都触发) | 无 |
| 多进程共享 | 天然支持(MAP_SHARED) | 需要额外 IPC 机制 |
| 文件大于物理内存 | 可以映射(按需换入换出) | 需要分块读取 |
| 线程安全 | 需要自行处理同步 | read/write 本身原子性较好 |
经验法则:
- 频繁随机访问同一文件 → mmap
- 多进程共享数据 → mmap(MAP_SHARED)
- 顺序读取后丢弃 → read + 大缓冲区
- 小文件(<64KB) → read/write 更简单
- 数据库/缓存系统 → mmap(如 SQLite、LMDB、LevelDB)
七、mmap vs POSIX 共享内存 vs System V 共享内存
Linux 提供三种进程间共享内存的机制:
| 维度 | mmap(文件共享映射) | POSIX shm(shm_open) | System V shm(shmget) |
|---|---|---|---|
| 创建方式 | open() + mmap() |
shm_open() + mmap() |
shmget() + shmat() |
| 底层实现 | tmpfs 或真实文件 | tmpfs(/dev/shm/) |
内核 IPC 对象 |
| 持久化 | 真实文件映射可持久化 | 仅内存(/dev/shm/) |
仅内存 |
| 命名方式 | 文件路径 | /name 字符串 |
整数 key |
| 大小限制 | 文件大小 / 虚拟地址空间 | /dev/shm 大小 |
SHMMAX 内核参数 |
| 无关进程间共享 | 可以(通过文件路径) | 可以(通过名字) | 可以(通过 key) |
| API 复杂度 | 简单 | 简单 | 复杂 |
| 现代 Linux 推荐 | 是 | 是 | 不推荐(遗留接口) |
POSIX 共享内存示例:
c
/* 编译需要链接 -lrt: gcc shm_demo.c -o shm_demo -lrt */
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
/* 等价于 open("/dev/shm/my_shm", O_RDWR | O_CREAT, 0644) */
int fd = shm_open("/my_shm", O_RDWR | O_CREAT, 0644);
ftruncate(fd, 4096);
void *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
/* ... 使用 p ... */
munmap(p, 4096);
shm_unlink("/my_shm"); /* 删除共享内存对象 */
shm_open本质就是在/dev/shm/下创建文件 +open,然后mmap。OpenHarmony 的 param 系统没有使用shm_open,而是直接open+mmap,效果完全一样。
八、常见陷阱与最佳实践
8.1 SIGBUS 信号
触发条件 :访问了 mmap 映射范围内但超出文件实际大小的地址。
c
/* 文件只有 100 字节,但映射了 4096 字节 */
int fd = open("small.txt", O_RDONLY); /* 文件 100 字节 */
char *p = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 0);
p[99]; /* OK --- 在文件范围内 */
p[100]; /* SIGBUS!--- 超出文件大小 */
p[4095]; /* SIGBUS! */
预防 :映射前用 fstat() 获取文件大小,确保访问不越界。或者先 ftruncate() 扩展文件。
8.2 文件截断竞争
另一个进程在你 mmap 期间 truncate 了文件,也会导致 SIGBUS:
c
/* 进程 A: mmap 了 1MB 文件 */
/* 进程 B: truncate(fd, 0) 把文件清空 */
/* 进程 A: 访问 p[500000] → SIGBUS */
预防 :使用文件锁(flock 或 fcntl)保护。
8.3 映射长度与页对齐
mmap的length会被内核向上对齐到页大小(通常 4096)offset参数必须是页大小的整数倍 ,否则返回EINVALmunmap的addr也必须页对齐
c
long page_size = sysconf(_SC_PAGESIZE); /* 获取系统页大小 */
printf("页大小: %ld\n", page_size); /* 通常 4096 */
/* 错误:offset 不是页对齐的 */
mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 100); /* EINVAL */
/* 正确:offset 必须是 4096 的倍数 */
mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 4096); /* OK */
8.4 MAP_SHARED 写入的可见性
MAP_SHARED 的修改对其他映射了同一文件的进程立刻可见 (因为共享同一物理页),但不保证立刻写入磁盘。要确保持久化:
c
/* 方法 1:显式 msync */
msync(mapped, length, MS_SYNC);
/* 方法 2:munmap 会隐式触发回写(但不保证立即完成) */
munmap(mapped, length);
/* 方法 3:对 fd 调用 fsync(需要 fd 仍然打开) */
fsync(fd);
8.5 不要在 mmap 区域使用 realloc
mmap 返回的内存不属于 malloc 管理,不能用 realloc 扩展。要扩展映射,使用 mremap:
c
/* Linux 特有:原地扩展映射 */
void *new_addr = mremap(old_addr, old_size, new_size, MREMAP_MAYMOVE);
8.6 fork 后的映射继承
MAP_SHARED映射在fork()后父子进程共享(修改互相可见)MAP_PRIVATE映射在fork()后各自独立(COW)mmap区域不会跨 exec (execve后所有映射丢失)
九、mmap 在 OpenHarmony 中的应用
9.1 系统参数(Param)
OpenHarmony 的 param 系统是 mmap 最典型的应用。init 进程在 /dev/__parameters__/ 下创建多个 tmpfs 文件,通过 MAP_SHARED 映射为共享内存,使用 Trie 前缀树组织键值对。
init 进程 其他所有进程
│ │
├── open("/dev/__parameters__/xx") │
├── ftruncate(fd, size) │
├── mmap(PROT_READ|WRITE, SHARED) ├── mmap(PROT_READ, SHARED)
├── 写入 Trie 树 ├── 直接读 Trie 树(零系统调用)
└── 监听 Unix Socket 写入请求 └── 写请求 → Socket → init 代写
9.2 Binder 驱动
Android 和 OpenHarmony 的 Binder IPC 机制中,mmap 用于建立用户空间与内核空间的共享缓冲区,实现一次拷贝(而非传统 IPC 的两次拷贝):
传统 IPC(如管道、Socket):
发送方用户空间 → copy_from_user → 内核缓冲区 → copy_to_user → 接收方用户空间
(两次拷贝)
Binder(mmap 优化):
发送方用户空间 → copy_from_user → 内核缓冲区(同时 mmap 到接收方)→ 接收方直接读
(一次拷贝)
9.3 Ashmem / DMA-BUF
- Ashmem (Anonymous Shared Memory):Android/OpenHarmony 中的匿名共享内存驱动,本质是对
mmap+ tmpfs 的封装,支持 pin/unpin 机制(允许内核在内存不足时回收未锁定的页面) - DMA-BUF:用于 GPU、相机等设备的零拷贝缓冲区共享,底层也依赖 mmap
9.4 动态链接库加载
所有 .so 共享库的加载都依赖 mmap:
bash
# 查看进程的 mmap 映射
$ cat /proc/<pid>/maps
7f8a3c000000-7f8a3c021000 r--p /system/lib64/libc.so ← 只读代码段(MAP_PRIVATE)
7f8a3c021000-7f8a3c098000 r-xp /system/lib64/libc.so ← 可执行代码段
7f8a3c098000-7f8a3c0a1000 r--p /system/lib64/libc.so ← 只读数据
7f8a3c0a1000-7f8a3c0a4000 rw-p /system/lib64/libc.so ← 可读写数据(COW)
多个进程加载同一个 .so 时,代码段(r-xp)在物理内存中只有一份,极大节省内存。
十、mmap 相关的 /proc 接口
| 文件 | 说明 |
|---|---|
/proc/<pid>/maps |
查看进程的所有 VMA(虚拟内存区域)映射 |
/proc/<pid>/smaps |
每个 VMA 的详细内存统计(RSS、PSS、Shared、Private 等) |
/proc/<pid>/smaps_rollup |
smaps 的汇总版本 |
/proc/<pid>/map_files/ |
映射文件的符号链接(需要 root) |
/proc/sys/vm/max_map_count |
单进程最大 VMA 数量(默认 65536,可调) |
/proc/sys/vm/dirty_ratio |
脏页占内存比例阈值(默认 20%,超过则同步写回) |
/proc/sys/vm/dirty_expire_centisecs |
脏页过期时间(默认 3000 = 30 秒) |
实用命令:
bash
# 查看某进程的所有 mmap 映射
cat /proc/$(pidof init)/maps
# 查看系统当前 mmap 区域数量
wc -l /proc/$(pidof init)/maps
# 查看最大允许的 mmap 数量
cat /proc/sys/vm/max_map_count
# 查看某个文件被哪些进程 mmap 了
lsof +D /dev/__parameters__/
十一、关键源码索引(Linux 内核)
| 文件 | 功能 |
|---|---|
mm/mmap.c |
do_mmap() --- mmap 系统调用核心实现 |
mm/memory.c |
handle_mm_fault() --- 缺页异常处理 |
mm/filemap.c |
文件映射的页缓存管理(readpage、writepage) |
mm/rmap.c |
反向映射(rmap),追踪物理页被哪些 VMA 映射 |
mm/page-writeback.c |
脏页回写策略 |
mm/shmem.c |
tmpfs / 匿名共享映射的实现 |
mm/mremap.c |
mremap() --- 重新映射(扩展/移动映射区域) |
include/linux/mm_types.h |
vm_area_struct(VMA)数据结构定义 |
arch/arm64/mm/fault.c |
ARM64 架构的缺页异常入口 |
十二、总结
| 概念 | 一句话说明 |
|---|---|
| mmap | 将文件/设备映射到虚拟地址空间,用指针操作文件 |
| MAP_SHARED | 多进程共享同一物理页,修改写回文件 |
| MAP_PRIVATE | 写时复制(COW),修改不影响文件和其他进程 |
| MAP_ANONYMOUS | 不关联文件的纯内存映射 |
| 缺页异常 | 首次访问时才分配物理页 + 读取文件(惰性加载) |
| COW | MAP_PRIVATE 写入时,内核复制一份私有副本 |
| 脏页回写 | MAP_SHARED 的修改最终由内核异步写回磁盘 |
| 零拷贝 | mmap 直接访问 page cache,省去内核→用户空间的拷贝 |
| SIGBUS | 访问超出文件实际大小的 mmap 区域时触发 |
| page 对齐 | offset 必须页对齐,length 会被自动对齐 |
| 在 OHOS 中 | param 系统、Binder IPC、动态库加载、Ashmem 都基于 mmap |