3. mmap 系统调用详解:函数原型、参数含义及返回值
在 Android HAL 层驱动开发中,mmap 是实现零拷贝数据传输的核心系统调用。它允许用户空间程序直接映射设备内存或文件区域,从而绕过内核缓冲区的数据拷贝。本节将深入剖析 mmap 的函数原型、每个参数的确切含义以及返回值处理,为后续在 HAL 层实战应用打下坚实基础。
3.1 函数原型与参数总览
mmap 系统调用的原型定义在 <sys/mman.h> 头文件中,其标准签名如下:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
该函数共有 6 个参数,每个参数都精确控制着映射的行为。下表对每个参数的作用和典型取值进行了归纳:
| 参数 | 类型 | 含义 | 典型取值 / 说明 |
|---|---|---|---|
| addr | void * |
映射区起始地址(建议值) | 通常传 NULL,由内核选择合适地址;非 NULL 时需配合 MAP_FIXED 使用 |
| length | size_t |
映射区长度(字节) | 必须为正整数,且通常为页大小(4KB)的整数倍 |
| prot | int |
映射区内存保护标志 | PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE 的组合 |
| flags | int |
映射类型与行为标志 | MAP_SHARED、MAP_PRIVATE、MAP_ANONYMOUS、MAP_FIXED 等 |
| fd | int |
文件描述符 | 映射普通文件或设备时传入;MAP_ANONYMOUS 时传 -1 |
| offset | off_t |
文件偏移量 | 必须为页大小的整数倍;指示从文件或设备内存的何处开始映射 |
/dev/ 下的设备节点时,fd 通过 open() 系统调用获得,offset 通常设置为 0,表示从设备内存的起始地址开始映射。如果设备驱动只导出了一段连续的 DMA 缓冲区,offset 可以配合驱动逻辑指定子区域。
3.2 参数深度解析
3.2.1 addr —— 映射起始地址
addr 参数为用户进程期望的映射起始虚拟地址。在实际开发中,推荐将其设置为 NULL,让内核根据进程的地址空间布局自动选择一个未使用的区域。只有当需要精确控制映射位置(例如在特定地址上建立共享内存)时,才与 MAP_FIXED 标志配合使用。
- addr = NULL: 内核自主选择地址,返回值为实际映射地址。这是最安全、最常用的方式。
- addr != NULL 且 flags 不含 MAP_FIXED: 内核将
addr视为一个提示,但仍可能选择其他地址。 - addr != NULL 且 flags 包含 MAP_FIXED: 内核必须将映射放置在
addr指定的地址,若该地址已被占用则映射失败。
3.2.2 length —— 映射长度
length 指定了映射到进程地址空间的字节数。内核以页(通常为 4096 字节)为单位管理内存,因此 length 会被自动向上取整到页大小的整数倍。例如,若 length 为 5000 字节,实际映射区域将为 8192 字节(2 页)。
length 为 0,mmap 调用将失败并返回 MAP_FAILED。在 HAL 层驱动中,务必确保从设备获取的缓冲区大小信息正确,避免传入零长度。
3.2.3 prot —— 内存保护标志
prot 参数描述了映射区域的内存访问权限,可以是以下常量的按位或组合:
| 标志 | 值 | 含义 |
|---|---|---|
PROT_READ |
0x1 | 页面可读 |
PROT_WRITE |
0x2 | 页面可写 |
PROT_EXEC |
0x4 | 页面可执行 |
PROT_NONE |
0x0 | 页面不可访问 |
在 HAL 层驱动数据传输场景中,最常用的组合是 PROT_READ | PROT_WRITE,允许用户空间同时读取和写入映射的 DMA 缓冲区。
3.2.4 flags —— 映射类型与行为
flags 是 mmap 中最复杂的参数,它决定了映射的共享属性、映射类型以及是否固定地址。核心标志如下:
- MAP_SHARED: 映射是共享的。对映射区域的写操作会直接写回底层文件或设备,并且对其他映射同一区域的进程可见。这是 HAL 层驱动映射设备内存时必须使用的标志。
- MAP_PRIVATE: 映射是私有的,采用写时复制(Copy-on-Write)机制。对映射区域的修改不会写回文件,而是创建私有副本。此标志不适用于设备内存映射。
- MAP_ANONYMOUS: 匿名映射,不与任何文件描述符关联。
fd参数必须设置为-1。用于分配大块零初始化内存或进程间共享内存。 - MAP_FIXED: 强制映射到
addr指定的地址。使用此标志需谨慎,可能覆盖已有映射。 - MAP_POPULATE: 为映射预先分配页表并填充物理页,避免后续访问时产生缺页中断。适用于实时性要求高的场景。
flags 必须设置为 MAP_SHARED。如果使用 MAP_PRIVATE,写操作将不会同步到设备,导致驱动与用户空间数据不一致。
3.2.5 fd —— 文件描述符
fd 是 open() 系统调用返回的文件描述符,指向要映射的文件或设备。在 Android 驱动场景中,fd 通常指向 /dev/ 下的设备节点(如 /dev/video0、/dev/i2c-1 或自定义的字符设备)。
- 对于普通文件映射,
fd必须有效且可读(如果prot包含PROT_READ)。 - 对于设备映射,设备驱动必须实现了
mmap文件操作(即struct file_operations中的.mmap回调)。 - 当使用
MAP_ANONYMOUS时,fd必须为-1。
3.2.6 offset —— 文件偏移量
offset 指定了从文件或设备内存的哪个位置开始映射。该参数必须为系统页大小的整数倍。在设备驱动中,offset 通常被驱动用于区分不同的 DMA 缓冲区或设备内存区域。
offset 来索引不同的缓冲区。例如,offset = 0 映射第一个缓冲区,offset = 0x1000 映射第二个缓冲区。HAL 层开发者需要与驱动开发者约定 offset 的语义。
3.3 返回值与错误处理
mmap 的返回值类型为 void *,即映射区域的起始虚拟地址。调用成功时,返回非 NULL 的指针;调用失败时,返回 MAP_FAILED(即 (void *) -1),并设置 errno 以指示错误原因。
| 返回值 | 含义 | 典型 errno |
|---|---|---|
非 NULL 指针 |
映射成功,返回映射区起始地址 | — |
MAP_FAILED |
映射失败 | 见下方常见错误码 |
常见的 errno 错误码及其含义:
- EACCES: 文件描述符
fd的打开模式与prot或flags冲突。例如,以只读方式打开文件,却要求PROT_WRITE。 - EAGAIN: 文件已被锁定,或映射需要太多内存。
- EINVAL: 参数无效。常见原因:
length为 0、offset不是页大小整数倍、flags包含MAP_FIXED但addr不可用。 - ENODEV: 底层文件系统不支持内存映射。对于设备文件,表示驱动未实现
mmap操作。 - ENOMEM: 内存不足,无法完成映射。
mmap 后必须立即检查返回值是否为 MAP_FAILED,并使用 perror() 或 strerror(errno) 记录错误日志。忽略返回值检查是导致难以调试的段错误(Segmentation Fault)的常见原因。
3.4 mmap 映射流程与内存布局
下图展示了从用户空间调用 mmap 到内核完成映射的完整流程,以及映射区域在进程虚拟地址空间中的位置关系。
3.5 典型调用示例(HAL 层上下文)
以下代码片段展示了在 HAL 层模块中如何安全地调用 mmap 映射设备 DMA 缓冲区:
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <android/log.h>
#define LOG_TAG "HAL_MMAP_DEMO"
#define DEVICE_PATH "/dev/my_dma_device"
#define DMA_BUF_SIZE (4 * 1024 * 1024) // 4MB 缓冲区
int hal_mmap_demo(void **mapped_addr) {
int fd;
void *addr;
// 1. 打开设备节点
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
"Failed to open %s: %s", DEVICE_PATH, strerror(errno));
return -1;
}
// 2. 调用 mmap:addr=NULL,length=4MB,prot=读写,flags=共享,offset=0
addr = mmap(NULL, DMA_BUF_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
"mmap failed: %s", strerror(errno));
close(fd);
return -1;
}
// 3. 映射成功后,可以关闭 fd(映射仍然有效)
close(fd);
// 4. 返回映射地址给调用者
*mapped_addr = addr;
__android_log_print(ANDROID_LOG_INFO, LOG_TAG,
"mmap succeeded, addr=%p, size=%d", addr, DMA_BUF_SIZE);
return 0;
}
// 清理映射
void hal_munmap_demo(void *addr, size_t length) {
if (addr != NULL) {
munmap(addr, length);
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "munmap addr=%p", addr);
}
}
mmap成功后可以立即关闭文件描述符fd,映射关系仍然保持。- 必须使用
MAP_SHARED标志,确保对映射区域的修改能同步到设备。 - 映射长度
length应与驱动导出的 DMA 缓冲区大小一致。 - 使用
munmap()解除映射,释放虚拟地址空间资源。
3.6 参数组合与适用场景速查
下表总结了 HAL 层开发中常见的 mmap 参数组合及其典型应用场景:
| 场景 | addr | prot | flags | fd | offset |
|---|---|---|---|---|---|
| 映射设备 DMA 缓冲区 | NULL |
PROT_READ | PROT_WRITE |
MAP_SHARED |
设备节点 fd | 0 或驱动指定 |
| 映射帧缓冲 (Framebuffer) | NULL |
PROT_READ | PROT_WRITE |
MAP_SHARED |
/dev/fb0 fd |
0 |
| 分配匿名共享内存 | NULL |
PROT_READ | PROT_WRITE |
MAP_SHARED | MAP_ANONYMOUS |
-1 |
0 |
| 映射 ION 缓冲区 | NULL |
PROT_READ | PROT_WRITE |
MAP_SHARED |
ION fd | 0 |
| 只读映射固件数据 | NULL |
PROT_READ |
MAP_SHARED |
固件文件 fd | 0 |