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 层驱动开发中,按以下决策流程选择正确的映射类型:
- 是否需要持久化? 是 → 文件映射;否 → 匿名映射。
- 是否需要多进程共享? 是 → 共享映射;否 → 私有映射。
- 驱动数据通道: 优先选择 共享文件映射,兼顾持久化与零拷贝。
- 图形缓冲区: 使用 共享匿名映射,避免文件系统开销。
- 只读配置/代码: 使用 私有文件映射,利用写时复制节省内存。
HAL 层最佳实践: 对于需要高频读写的传感器数据通道,使用 共享文件映射 并配合
msync(MS_ASYNC) 异步回写,避免阻塞数据采集线程。对于一次性配置数据,使用 私有文件映射 确保驱动数据不被意外修改。
4.6 映射类型与内核页表交互
下图展示了四种映射类型在内核页表中的物理页管理差异:
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 的根本原因。