3. mmap 系统调用详解:函数原型、参数含义及返回值

在 Android HAL 层驱动开发中,mmap 是实现零拷贝数据传输的核心系统调用。它允许用户空间程序直接映射设备内存或文件区域,从而绕过内核缓冲区的数据拷贝。本节将深入剖析 mmap 的函数原型、每个参数的确切含义以及返回值处理,为后续在 HAL 层实战应用打下坚实基础。

核心要点: mmap 的本质是在进程的虚拟地址空间中创建一段映射区域,该区域与文件描述符 fd 指向的内核缓冲区或设备内存建立直接关联。映射建立后,用户程序对这段内存的读写操作,会直接转化为对底层物理内存或设备寄存器的访问。

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 文件偏移量 必须为页大小的整数倍;指示从文件或设备内存的何处开始映射
HAL 开发提示: 在映射 /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: 为映射预先分配页表并填充物理页,避免后续访问时产生缺页中断。适用于实时性要求高的场景。
HAL 层黄金法则: 映射设备 DMA 缓冲区时,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 缓冲区或设备内存区域。

驱动设计模式: 许多 Android 多媒体驱动(如 V4L2、ION)利用 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: 内存不足,无法完成映射。
安全检查: 在 HAL 层代码中,每次调用 mmap 后必须立即检查返回值是否为 MAP_FAILED,并使用 perror() 或 strerror(errno) 记录错误日志。忽略返回值检查是导致难以调试的段错误(Segmentation Fault)的常见原因。

3.4 mmap 映射流程与内存布局

下图展示了从用户空间调用 mmap 到内核完成映射的完整流程,以及映射区域在进程虚拟地址空间中的位置关系。

mmap 系统调用流程与虚拟内存布局 用户空间 HAL 层 调用 mmap() 系统调用 内核 VMA 管理 创建 vm_area_struct 调用驱动 驱动 .mmap 回调 建立页表映射 进程虚拟地址空间(32位示例) 栈 (Stack) — 高地址 mmap 映射区域 (DMA 缓冲区 / 设备内存) 返回地址:0xb6f00000 堆 (Heap) — 低地址 代码段 / 数据段 (Text & Data) 映射区域位于栈和堆之间,由内核动态管理。HAL 层通过返回的指针直接访问该区域。

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);
    }
}
关键点回顾:
  1. mmap 成功后可以立即关闭文件描述符 fd,映射关系仍然保持。
  2. 必须使用 MAP_SHARED 标志,确保对映射区域的修改能同步到设备。
  3. 映射长度 length 应与驱动导出的 DMA 缓冲区大小一致。
  4. 使用 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