2. mmap 系统调用详解
mmap 是 Linux 内核提供给用户空间用于创建内存映射的核心系统调用。理解其函数原型、每个参数的含义、返回值以及常见错误码,是调试驱动与用户空间共享内存问题的基础。本章将逐项拆解 mmap 的完整语义。
2.1 函数原型
mmap 在 POSIX 和 Linux 中的标准原型定义在 <sys/mman.h> 头文件中:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
返回类型为 void *,指向映射区域的起始地址。失败时返回 MAP_FAILED(即 (void *) -1),并设置 errno。
2.2 参数解析
下表汇总了六个参数的类型、含义及常用取值:
| 参数 | 类型 | 含义 | 常用值 / 说明 |
|---|---|---|---|
addr |
void * |
映射起始地址(提示地址) | NULL 由内核选择;非 NULL 仅作提示,需配合 MAP_FIXED |
length |
size_t |
映射长度(字节) | 必须大于 0;实际映射按页对齐(4KB 或大页) |
prot |
int |
映射区域保护标志 | PROT_READ / PROT_WRITE / PROT_EXEC / PROT_NONE |
flags |
int |
映射类型与行为标志 | MAP_SHARED / MAP_PRIVATE / MAP_ANONYMOUS / MAP_FIXED 等 |
fd |
int |
文件描述符 | 文件映射时有效;匿名映射传 -1 |
offset |
off_t |
文件偏移量 | 必须按页大小对齐(通常为 4KB 的整数倍) |
2.2.1 addr —— 映射提示地址
- 典型用法:传入
NULL,让内核在进程地址空间中自由选择一块未使用的区域。 - 非 NULL 提示:内核会尽量从
addr开始映射,但不保证;除非同时指定MAP_FIXED。 - MAP_FIXED 陷阱:强制将映射放在
addr,若该地址已被占用则覆盖原有映射,极易导致程序崩溃,驱动调试中应谨慎使用。
2.2.2 length —— 映射长度
- 指定需要映射的字节数。内核实际分配的内存会向上取整到页大小(通常 4096 字节)的整数倍。
- 对于设备驱动共享内存,
length通常等于驱动分配的 DMA 缓冲区大小或硬件寄存器区域大小。
2.2.3 prot —— 保护标志
保护标志控制对映射区域的访问权限,可组合使用:
| 标志 | 值 | 含义 |
|---|---|---|
PROT_READ |
0x1 | 页面可读 |
PROT_WRITE |
0x2 | 页面可写 |
PROT_EXEC |
0x4 | 页面可执行 |
PROT_NONE |
0x0 | 页面不可访问 |
⚠️ 驱动调试注意:如果用户空间只指定
PROT_READ 而驱动期望写入,会导致 SEGFAULT。共享内存场景通常使用 PROT_READ | PROT_WRITE。
2.2.4 flags —— 映射类型与行为
flags 是 mmap 最复杂的参数,决定了映射的共享属性、匿名性、固定性等。核心标志如下:
| 标志 | 含义 | 驱动共享内存场景 |
|---|---|---|
MAP_SHARED |
映射对文件/设备可见,修改会写回底层 | 必须使用,否则驱动无法感知用户修改 |
MAP_PRIVATE |
写时复制,修改不写回底层 | 不适用于驱动共享内存,会导致数据不同步 |
MAP_ANONYMOUS |
匿名映射,不依赖文件描述符 | 用于进程间匿名共享,驱动场景较少 |
MAP_FIXED |
强制使用 addr 指定地址 |
危险,可能覆盖已有映射,不推荐 |
MAP_POPULATE |
预分配页表,避免缺页中断 | 对实时性要求高的驱动场景有用 |
2.2.5 fd —— 文件描述符
- 对于文件映射:
fd是通过open()打开的文件或设备文件的描述符。 - 对于设备驱动映射:
fd通常来自open("/dev/xxx"),内核驱动通过file_operations.mmap回调处理。 - 对于匿名映射:
fd传-1,且flags必须包含MAP_ANONYMOUS。
2.2.6 offset —— 文件偏移量
- 指定从文件或设备偏移多少字节开始映射。
- 必须按页大小对齐(通常为 4096 的整数倍)。如果传入非对齐偏移,
mmap返回EINVAL。 - 在驱动中,
offset常被用来选择不同的物理内存区域或 DMA 缓冲区索引。
2.3 返回值与错误码
2.3.1 成功返回值
- 返回映射区域的起始虚拟地址(
void *)。 - 该地址位于进程的用户空间,可通过指针直接读写。
2.3.2 失败返回值
- 返回
MAP_FAILED(即(void *) -1)。 - 必须检查返回值,否则后续解引用会导致
SEGFAULT。
2.3.3 常见错误码(errno)
| 错误码 | 含义 | 典型触发场景 |
|---|---|---|
EACCES |
权限不足 | fd 以只读方式打开,但 prot 要求 PROT_WRITE |
EAGAIN |
资源暂时不可用 | 文件被锁或映射资源暂时不足 |
EBADF |
无效文件描述符 | fd 不是有效的打开文件描述符 |
EINVAL |
参数无效 | offset 未页对齐;length 为 0;flags 组合矛盾 |
ENFILE |
系统文件表满 | 系统级打开文件数达到上限 |
ENODEV |
设备不支持 | 底层文件系统或设备驱动未实现 mmap 操作 |
ENOMEM |
内存不足 | 内核无法分配足够的内存或页表 |
EPERM |
操作不允许 | MAP_FIXED 指定地址不可用,或 prot 与 flags 冲突 |
💡 调试技巧:在驱动共享内存调试中,
EINVAL 和 ENODEV 是最常见的两个错误。遇到 EINVAL 时优先检查 offset 是否页对齐;遇到 ENODEV 时检查驱动是否注册了 mmap 回调。
2.4 mmap 调用流程与内核交互
下图展示了用户空间调用 mmap 后,进入内核并最终调用驱动 mmap 回调的完整流程:
2.5 典型调用示例
以下代码展示了用户空间如何调用 mmap 映射一个设备驱动共享内存:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(void)
{
int fd;
void *map_addr;
size_t map_size = 4096; // 映射 4KB
// 1. 打开设备文件
fd = open("/dev/my_shared_mem", O_RDWR);
if (fd < 0) {
perror("open");
exit(EXIT_FAILURE);
}
// 2. 调用 mmap
map_addr = mmap(NULL, map_size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, 0);
if (map_addr == MAP_FAILED) {
fprintf(stderr, "mmap failed: %s (errno=%d)\n",
strerror(errno), errno);
close(fd);
exit(EXIT_FAILURE);
}
// 3. 使用共享内存
printf("Shared memory mapped at: %p\n", map_addr);
// 写入数据
memcpy(map_addr, "Hello from userspace!", 21);
// 4. 解除映射
munmap(map_addr, map_size);
close(fd);
return 0;
}
🔍 关键检查点:
open()的权限必须与prot匹配(O_RDWR 对应 PROT_READ|PROT_WRITE)。MAP_SHARED是驱动共享内存的唯一正确选择。offset必须为 0 或页对齐值,否则返回EINVAL。- 使用完必须调用
munmap释放映射,否则造成资源泄漏。
2.6 参数组合与驱动行为对照
不同的 prot 和 flags 组合会直接影响驱动 mmap 回调的行为。下表总结了常见组合及其在驱动调试中的含义:
| prot | flags | 驱动行为 | 典型场景 |
|---|---|---|---|
| PROT_READ | MAP_SHARED | 驱动只允许读取,写入触发 SIGSEGV | 只读状态寄存器映射 |
| PROT_READ|PROT_WRITE | MAP_SHARED | 驱动允许读写,修改直接反映到物理内存 | DMA 缓冲区、控制寄存器 |
| PROT_READ|PROT_WRITE | MAP_PRIVATE | 写时复制,驱动无法感知用户修改 | ❌ 不适用于驱动共享内存 |
| PROT_NONE | MAP_SHARED | 驱动映射但不可访问,任何访问触发 SIGSEGV | 占位映射、保护页 |
| PROT_READ|PROT_EXEC | MAP_SHARED | 驱动映射可读可执行,不可写 | 固件代码映射(罕见) |
2.7 常见调试陷阱总结
- 陷阱1:offset 未页对齐 —— 返回
EINVAL,驱动甚至不会被调用。 - 陷阱2:fd 权限不足 —— 返回
EACCES,检查open()模式。 - 陷阱3:使用 MAP_PRIVATE 代替 MAP_SHARED —— 驱动无法感知写入,数据不同步。
- 陷阱4:length 为 0 —— 返回
EINVAL。 - 陷阱5:忘记检查返回值 —— 直接解引用
MAP_FAILED导致SEGFAULT。 - 陷阱6:多次 mmap 同一区域未 munmap —— 虚拟地址空间泄漏,最终
ENOMEM。
✅ 最佳实践:在驱动调试初期,建议在
mmap 调用后立即打印返回地址和 errno,并使用 strace 工具跟踪系统调用参数,快速定位参数错误。