4. mmap 映射类型:文件映射与匿名映射、私有映射与共享映射的区别与使用场景

在 Android HAL 层驱动开发中,mmap 系统调用提供了四种核心映射组合,理解它们的差异是设计高效数据传输通道的前提。映射类型由两个维度决定:映射源(文件 vs 匿名)与 映射可见性(私有 vs 共享)。

4.1 映射类型二维矩阵

下表从数据来源、内存行为、进程间可见性三个维度,清晰划分四种组合:

映射维度 文件映射 (File-backed) 匿名映射 (Anonymous)
私有映射 (Private) MAP_PRIVATE | MAP_FILE
写时复制,修改不写回文件
MAP_PRIVATE | MAP_ANONYMOUS
进程私有堆/栈扩展,零初始化
共享映射 (Shared) MAP_SHARED | MAP_FILE
修改直接同步回文件,多进程可见
MAP_SHARED | MAP_ANONYMOUS
进程间共享内存,无文件后援
HAL 层实战要点: 驱动与用户空间的数据传输通常使用 共享文件映射(MAP_SHARED | MAP_FILE),因为它既能保证数据持久化,又能实现零拷贝。而匿名共享映射常用于 SurfaceFlinger 与 GPU 驱动之间的图形缓冲区传递。

4.2 文件映射 vs 匿名映射

4.2.1 文件映射 (File-backed Mapping)

将磁盘文件的一部分直接映射到进程虚拟地址空间。内核通过页缓存(Page Cache)管理文件内容,读写映射区域等同于读写文件,无需额外的 read()/write() 系统调用。

  • 典型场景: 驱动暴露 /dev/xxx 设备文件,HAL 层通过 mmap 映射该文件描述符实现数据交换。
  • 优势: 数据可持久化,支持进程间共享(配合 MAP_SHARED),内核自动处理脏页回写。
  • 注意事项: 文件大小必须固定,映射长度不能超过文件实际大小;驱动中需实现 mmap 文件操作函数。

4.2.2 匿名映射 (Anonymous Mapping)

不关联任何文件,由内核分配一块全零初始化的物理内存。进程退出后内存自动释放,无持久化能力。

  • 典型场景: 进程私有堆内存(malloc 底层实现)、线程栈扩展、以及不需要文件后援的进程间共享内存。
  • 优势: 创建速度快,无文件系统开销,适合临时数据缓冲区。
  • HAL 层应用: 在 Gralloc 分配图形缓冲区时,常使用匿名共享映射实现 SurfaceFlinger 与应用进程之间的零拷贝传递。
代码示例:匿名共享映射创建共享内存
#include <sys/mman.h>
#include <unistd.h>

// 创建 4MB 匿名共享内存
size_t size = 4 * 1024 * 1024;
void *addr = mmap(NULL, size,
                  PROT_READ | PROT_WRITE,
                  MAP_SHARED | MAP_ANONYMOUS,
                  -1, 0);
if (addr == MAP_FAILED) {
    // 错误处理
}
// fork 后子进程可直接访问 addr,实现零拷贝通信

4.3 私有映射 vs 共享映射

4.3.1 私有映射 (Private Mapping)

采用 写时复制 (Copy-on-Write, CoW) 机制。多个进程映射同一文件时,最初共享同一物理页;当某个进程尝试写入时,内核会复制该页到新物理页,修改仅对当前进程可见。

  • 内存开销: 只有写入的页面才会产生副本,未修改页面仍共享。
  • 数据隔离: 修改不会传播到原文件或其他进程的映射。
  • 典型场景: 动态链接库加载(如 libc.so)、进程初始化数据段、调试时避免破坏原始文件。

4.3.2 共享映射 (Shared Mapping)

所有映射同一区域的进程直接共享物理内存页。一个进程的修改立即对其他进程可见,并且(对于文件映射)内核会在适当时机将脏页写回文件。

  • 同步保证: 内核保证共享映射的写操作对其他进程立即可见(无需手动刷新 TLB)。
  • 文件同步: 共享文件映射的修改最终会同步到磁盘,但时间点不确定;可使用 msync() 强制同步。
  • 典型场景: 驱动与 HAL 层的数据通道、进程间大数据传输、数据库共享缓存。
驱动开发避坑: 在 HAL 层使用共享文件映射时,务必在驱动中正确实现 vm_operations_struct 的 fault 和 page_mkwrite 回调。否则,当内核尝试回写脏页时,可能因缺少页错误处理而导致内核崩溃(OOPS)。

4.4 四种组合的完整对比

特性 私有文件映射 共享文件映射 私有匿名映射 共享匿名映射
数据来源 磁盘文件 磁盘文件 全零内存 全零内存
写时复制 是 否 是 否
修改写回文件 否 是(异步/msync) N/A N/A
进程间共享 否(修改隔离) 是 否 是
持久化 否 是 否 否
典型用途 ELF 加载、只读配置 驱动数据通道、DB 缓存 malloc、栈扩展 进程间共享内存、图形缓冲区

4.5 映射类型选择决策树

在 HAL 层驱动开发中,按以下决策流程选择正确的映射类型:

  1. 是否需要持久化? 是 → 文件映射;否 → 匿名映射。
  2. 是否需要多进程共享? 是 → 共享映射;否 → 私有映射。
  3. 驱动数据通道: 优先选择 共享文件映射,兼顾持久化与零拷贝。
  4. 图形缓冲区: 使用 共享匿名映射,避免文件系统开销。
  5. 只读配置/代码: 使用 私有文件映射,利用写时复制节省内存。
HAL 层最佳实践: 对于需要高频读写的传感器数据通道,使用 共享文件映射 并配合 msync(MS_ASYNC) 异步回写,避免阻塞数据采集线程。对于一次性配置数据,使用 私有文件映射 确保驱动数据不被意外修改。

4.6 映射类型与内核页表交互

下图展示了四种映射类型在内核页表中的物理页管理差异:

四种 mmap 映射类型的内核页表交互 进程 A 虚拟地址空间 进程 B 虚拟地址空间 私有文件映射 (CoW) 私有文件映射 (已写) 共享文件映射 私有匿名映射 共享匿名映射 私有文件映射 (CoW) 共享文件映射 共享匿名映射 物理页框 (Page Cache) 物理页框 (CoW 副本) 物理页框 (共享) 物理页框 (匿名) 物理页框 (共享匿名) 共享读 (未写时) 写后独立副本 直接共享同一物理页 进程私有 进程间共享匿名页 关键理解 私有映射通过写时复制实现进程隔离,共享映射通过直接共享物理页实现零拷贝通信。 文件映射依赖 Page Cache,匿名映射直接使用匿名页框。

4.7 HAL 层实战场景映射

Android 子系统 推荐映射类型 原因
Camera HAL 数据流 共享文件映射 驱动写入帧数据,HAL 层零拷贝读取,支持多进程预览
Sensor HAL 事件队列 共享匿名映射 高频小数据,无需持久化,减少文件系统开销
Audio HAL 共享缓冲区 共享文件映射 需要持久化调试数据,支持跨进程音频路由
Gralloc 图形缓冲区 共享匿名映射 SurfaceFlinger 与 App 进程间零拷贝传递,无文件依赖
Vendor 库加载 私有文件映射 只读代码段,写时复制节省物理内存
性能指标参考: 在 Qualcomm SM8450 平台上,使用共享文件映射传输 4K 视频帧(~8MB)时,mmap 方式比传统 read/write 方式减少约 62% 的 CPU 占用,延迟降低 78%。这是 Android 图形栈普遍采用 mmap 的根本原因。