3. mmap 系统调用 API 详解

mmap 是 Linux 系统中最核心的内存映射接口,它允许用户态程序将文件或设备内存直接映射到进程的虚拟地址空间。本章将深入解析其原型、每个参数的含义、返回值处理以及常见错误码,帮助你在工程实践中准确使用这一系统调用。

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 映射长度(字节) 必须大于 0;实际映射以页为单位向上取整
prot int 映射区域保护标志 PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE
flags int 映射类型与行为标志 MAP_SHARED、MAP_PRIVATE、MAP_ANONYMOUS 等
fd int 文件描述符 文件映射时有效;匿名映射传 -1
offset off_t 文件偏移量 必须是页大小(通常 4096)的整数倍
工程提示: 在驱动映射场景中,fd 通常指向通过 open() 打开的字符设备文件,offset 用于选择设备内部的物理内存区域或寄存器偏移。

3.2 参数逐项深度解析

3.2.1 addr —— 映射地址提示

addr 参数用于向内核建议映射的虚拟起始地址。内核并不保证一定使用该地址,除非同时指定了 MAP_FIXED 标志。工程实践中的最佳做法是:

  • 传 NULL:让内核自动选择合适的地址,兼容性最好,避免地址冲突。
  • 非 NULL + 无 MAP_FIXED:内核将 addr 视为提示,可能调整到附近可用区域。
  • 非 NULL + MAP_FIXED:强制内核在精确地址映射,若地址已被占用则失败(返回 MAP_FAILED)。
危险操作: 使用 MAP_FIXED 会无条件覆盖目标地址上的现有映射,可能导致进程崩溃或数据损坏。在驱动调试中,除非你完全掌控地址空间布局,否则应避免使用。

3.2.2 length —— 映射长度

length 指定映射的字节数。内核以页为单位管理内存,因此实际映射区域会向上取整到页大小的整数倍。例如,若页大小为 4096 字节,length = 5000 时实际映射 8192 字节。

关键约束:

  • length 必须大于 0。
  • 对于文件映射,length 可以超过文件大小,超出部分填充为 0(零页)。
  • 访问超出 length 但仍在页对齐范围内的地址,行为未定义。

3.2.3 prot —— 保护标志

prot 参数控制映射区域的内存保护属性,可以是以下值的按位或组合:

标志 值 含义
PROT_READ 0x1 允许读取
PROT_WRITE 0x2 允许写入
PROT_EXEC 0x4 允许执行
PROT_NONE 0x0 禁止任何访问

在驱动映射场景中,最常用的组合是 PROT_READ | PROT_WRITE,用于读写设备内存或 DMA 缓冲区。如果设备内存需要执行代码(如固件加载),则需额外添加 PROT_EXEC。

3.2.4 flags —— 映射类型与行为

flags 是 mmap 最复杂的参数,它决定了映射的共享属性、内存来源和特殊行为。核心标志包括:

标志 值 含义 典型场景
MAP_SHARED 0x01 共享映射,修改对其他进程可见,并写回文件 设备驱动映射、进程间共享内存
MAP_PRIVATE 0x02 私有映射,写时复制,修改不写回文件 程序加载、数据段映射
MAP_ANONYMOUS 0x20 匿名映射,不依赖文件描述符 分配大块内存、栈扩展
MAP_FIXED 0x10 强制在 addr 精确映射 特殊地址需求(谨慎使用)
MAP_POPULATE 0x08000 预分配页表,避免缺页中断 实时性要求高的场景
驱动映射关键规则: 设备文件映射必须使用 MAP_SHARED,否则驱动中的 mmap 回调可能不会被正确调用,且写时复制会导致用户态修改无法同步到设备。

3.2.5 fd —— 文件描述符

fd 是已打开的文件或设备文件的描述符。对于匿名映射(MAP_ANONYMOUS),fd 应设为 -1,某些系统也接受 /dev/zero 的 fd。

在驱动映射中,fd 通常来自:

  • open("/dev/mydevice", O_RDWR) 返回的字符设备 fd。
  • shm_open() 返回的 POSIX 共享内存 fd。
  • memfd_create() 创建的匿名文件 fd。

3.2.6 offset —— 文件偏移量

offset 指定文件映射的起始位置。对于设备驱动,offset 通常用于选择设备内部的物理内存区域或寄存器页。关键约束:

  • 必须页对齐:offset 必须是系统页大小(sysconf(_SC_PAGE_SIZE),通常 4096)的整数倍。
  • 驱动中,offset 被传递给 file_operations.mmap 回调的 vma->vm_pgoff 字段,用于区分不同的映射区域。

3.3 返回值处理

mmap 的返回值是映射区域的起始虚拟地址,失败时返回 MAP_FAILED(即 (void *) -1)。正确的返回值检查模式如下:

void *mapped_addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                         MAP_SHARED, fd, 0);
if (mapped_addr == MAP_FAILED) {
    perror("mmap failed");
    exit(EXIT_FAILURE);
}
// 使用 mapped_addr ...
// 使用完毕后调用 munmap
if (munmap(mapped_addr, 4096) == -1) {
    perror("munmap failed");
}
常见陷阱: 不要将 mmap 的返回值与 NULL 比较来判断失败。成功映射的地址可能为 0(虽然极少见),但 MAP_FAILED 是 (void *) -1,不是 NULL。

3.4 错误码详解

当 mmap 失败时,errno 被设置为以下错误码之一。理解这些错误码对于调试驱动映射问题至关重要:

错误码 含义 典型原因
EACCES 权限不足 文件未以可读方式打开,或 prot 指定了 PROT_WRITE 但文件以只读方式打开
EAGAIN 资源暂时不可用 文件被锁定,或映射所需的内存暂时无法满足
EBADF 无效文件描述符 fd 不是有效的打开文件描述符,或 MAP_ANONYMOUS 未设置但 fd 为 -1
EINVAL 参数无效 length 为 0、offset 未页对齐、flags 包含冲突组合(如 MAP_SHARED | MAP_ANONYMOUS 但无 fd)
ENFILE 系统文件表满 系统范围内打开的文件数达到上限
ENODEV 不支持映射 底层文件系统或设备驱动未实现 mmap 操作
ENOMEM 内存不足 进程虚拟地址空间不足,或内核无法分配页表
EPERM 操作不允许 MAP_FIXED 指定了不可映射的地址,或 prot 请求了 PROT_EXEC 但文件系统以 noexec 挂载
驱动调试重点: 当 mmap 返回 ENODEV 时,通常意味着设备驱动的 file_operations 结构中没有实现 .mmap 回调,或者回调返回了负值。这是驱动开发中最常见的错误之一。

3.5 mmap 调用流程与内核交互

下图展示了用户态调用 mmap 后,系统调用进入内核,经过 VMA 管理、文件映射回调,最终返回用户态地址的完整流程:

mmap 系统调用内核交互流程 mmap 系统调用内核交互流程 用户态调用 mmap() sys_mmap() 系统调用入口 do_mmap() VMA 分配 file->f_op->mmap() 驱动回调 remap_pfn_range() 页表建立 返回映射地址到用户态 关键内核函数 • do_mmap_pgoff() • mmap_region() • remap_pfn_range()

3.6 参数组合与典型场景

不同的参数组合适用于不同的工程场景。下表总结了最常见的组合及其用途:

场景 addr prot flags fd offset
设备驱动映射 NULL PROT_READ | PROT_WRITE MAP_SHARED 设备 fd 页对齐偏移
匿名大块内存 NULL PROT_READ | PROT_WRITE MAP_PRIVATE | MAP_ANONYMOUS -1 0
进程间共享内存 NULL PROT_READ | PROT_WRITE MAP_SHARED | MAP_ANONYMOUS -1 0
文件 I/O 替代 NULL PROT_READ MAP_PRIVATE 文件 fd 0
DMA 缓冲区映射 NULL PROT_READ | PROT_WRITE MAP_SHARED DMA buf fd 0

3.7 工程实践检查清单

在编写使用 mmap 的代码时,建议逐项检查以下要点:

  1. 参数校验:length 是否大于 0?offset 是否页对齐?
  2. 权限匹配:prot 中的读写标志是否与 open() 时的文件访问模式一致?
  3. 共享属性:设备映射是否使用了 MAP_SHARED?
  4. 返回值检查:是否使用 MAP_FAILED 而非 NULL 判断失败?
  5. 错误码分析:失败时是否打印 errno 对应的错误信息?
  6. 资源释放:是否在适当时机调用 munmap() 解除映射?
  7. 驱动支持:设备驱动是否实现了 .mmap 回调?
核心要点: mmap 的参数组合决定了映射的行为和安全性。在驱动映射场景中,MAP_SHARED + 正确的 prot + 页对齐的 offset 是成功映射的三大基石。每次调用后务必检查返回值并分析错误码,这是调试驱动映射问题的最直接手段。