Linux_mmap内存映射技术详解

从系统调用原型到内核实现,从四种映射模式到实战 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_WRITEMAP_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"),所以是"脏页"

脏页的产生途径:

  1. mmap 写入 :进程通过 MAP_SHARED 映射文件后,直接往内存地址写数据,CPU 自动在页表项(PTE)上设置 dirty bit
  2. 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 参数)都会主动调用 msyncfsync 来确保持久化。

查看脏页状态
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 缓存最近的映射关系。munmapmprotect 时需要刷新 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 */

预防 :使用文件锁(flockfcntl)保护。

8.3 映射长度与页对齐

  • mmaplength 会被内核向上对齐到页大小(通常 4096)
  • offset 参数必须是页大小的整数倍 ,否则返回 EINVAL
  • munmapaddr 也必须页对齐
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 区域不会跨 execexecve 后所有映射丢失)

九、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 代写

详细分析见 OpenHarmony北向开发基础之系统参数Param机制

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