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 回调的完整流程:

mmap 系统调用内核处理流程 用户空间调用 mmap() sys_mmap() 系统调用入口 参数校验 addr/length/prot/flags/offset 查找 vma / 文件映射 根据 fd 找到 struct file 调用 file_operations.mmap 驱动分配物理页 / 建立页表 建立用户页表映射 更新进程页目录 / 页表项 返回映射地址到用户空间 失败返回 MAP_FAILED + errno

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 工具跟踪系统调用参数,快速定位参数错误。