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 管理、文件映射回调,最终返回用户态地址的完整流程:
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 的代码时,建议逐项检查以下要点:
- 参数校验:
length是否大于 0?offset是否页对齐? - 权限匹配:
prot中的读写标志是否与open()时的文件访问模式一致? - 共享属性:设备映射是否使用了
MAP_SHARED? - 返回值检查:是否使用
MAP_FAILED而非NULL判断失败? - 错误码分析:失败时是否打印
errno对应的错误信息? - 资源释放:是否在适当时机调用
munmap()解除映射? - 驱动支持:设备驱动是否实现了
.mmap回调?
mmap 的参数组合决定了映射的行为和安全性。在驱动映射场景中,MAP_SHARED + 正确的 prot + 页对齐的 offset 是成功映射的三大基石。每次调用后务必检查返回值并分析错误码,这是调试驱动映射问题的最直接手段。