3. mmap 系统调用详解:参数 prot、flags 与 offset 对齐要求

mmap 是 Linux 内核提供的核心内存映射系统调用,它允许用户态程序直接操作文件或匿名内存页。理解其三个核心参数——prot(保护标志)、flags(映射类型)和 offset(偏移对齐)——是掌握 DMA 缓冲区共享技术的基础。本节将逐一拆解这些参数的语义、组合规则和底层约束。

3.1 参数 prot:内存保护标志

prot 参数定义映射区域的内存访问权限,由以下标志按位或组合而成:

标志 值 含义 典型场景
PROT_READ 0x1 允许读取映射区域 只读文件映射、代码段
PROT_WRITE 0x2 允许写入映射区域 DMA 缓冲区、共享内存
PROT_EXEC 0x4 允许执行映射区域中的指令 动态库加载、JIT 编译
PROT_NONE 0x0 禁止任何访问 守卫页、地址空间预留
关键规则: 对于 DMA 缓冲区共享场景,必须同时设置 PROT_READ | PROT_WRITE。如果只设置 PROT_READ,写入操作会触发 SIGSEGV 段错误。内核在建立页表时会根据 prot 设置页表项的 R/W 位,硬件 MMU 直接执行该检查。

3.2 参数 flags:映射类型与行为控制

flags 参数决定映射的共享属性、内存来源和内核行为。以下是最关键的三个标志:

3.2.1 MAP_SHARED vs MAP_PRIVATE

标志 写时复制 对文件的可见性 适用场景
MAP_SHARED 否 写入立即同步回文件(或 DMA 设备) DMA 缓冲区、进程间共享内存、文件 I/O 加速
MAP_PRIVATE 是 写入仅对当前进程可见,不写回文件 程序加载、调试断点、只读数据私有副本
DMA 共享陷阱: 如果使用 MAP_PRIVATE 映射 DMA 缓冲区,写入操作会触发缺页异常并分配新的物理页(写时复制),导致用户态写入的地址与 DMA 设备实际操作的物理地址不一致,造成数据损坏。DMA 场景必须使用 MAP_SHARED。

3.2.2 MAP_ANONYMOUS

MAP_ANONYMOUS(或 MAP_ANON)创建一个不关联任何文件的匿名映射。此时 fd 参数应设为 -1,offset 被忽略。内核分配初始化为零的物理页。典型用途:

  • 分配大块连续虚拟地址空间(如 malloc 底层使用)
  • 进程间共享匿名内存(配合 MAP_SHARED 和 fork())
  • DMA 缓冲区分配(通过 /dev/dma_heap 等驱动)

3.2.3 其他重要 flags

标志 作用
MAP_FIXED 强制在指定地址映射,若地址已被占用则覆盖
MAP_POPULATE 预分配物理页,避免首次访问时缺页
MAP_LOCKED 锁定映射页,防止被换出(类似 mlock)
MAP_NORESERVE 不预留交换空间,用于大块稀疏映射

3.3 offset 对齐要求:页边界约束

offset 参数指定映射在文件中的起始位置。内核要求 offset 必须是系统页大小的整数倍(通常为 4096 字节,即 PAGE_SIZE)。

底层原因: 内核的页缓存(page cache)和 MMU 页表均以页为单位管理。如果 offset 不对齐,内核无法将文件中的某个页直接映射到进程地址空间,因为文件页的起始位置与虚拟页的起始位置不匹配。内核会返回 EINVAL 错误。

对于 DMA 缓冲区场景,offset 通常设为 0,表示从文件(设备)的起始地址开始映射。如果设备驱动支持多缓冲区,offset 可设置为 n * PAGE_SIZE 来映射第 n 个缓冲区。

#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("/dev/dma_buf", O_RDWR);
    if (fd < 0) return -1;

    size_t length = 4 * 1024 * 1024;  // 4MB DMA 缓冲区
    // 正确:offset 为 0(页对齐)
    void *addr = mmap(NULL, length,
                      PROT_READ | PROT_WRITE,
                      MAP_SHARED,
                      fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return -1;
    }

    // 错误示例:offset 不是页对齐
    // void *bad = mmap(NULL, length,
    //                   PROT_READ | PROT_WRITE,
    //                   MAP_SHARED,
    //                   fd, 1234);  // 1234 % 4096 != 0 → EINVAL

    // 使用映射区域...
    munmap(addr, length);
    close(fd);
    return 0;
}

3.4 参数组合与内核校验流程

下图展示了内核在 mmap 系统调用入口处对 prot、flags 和 offset 的校验流程:

mmap 参数校验流程 mmap 系统调用入口 校验 prot:是否包含非法组合? (PROT_NONE 不能与 READ/WRITE/EXEC 共存) 校验 flags:MAP_SHARED vs MAP_PRIVATE 互斥? (MAP_ANONYMOUS 时 fd 必须为 -1) 校验 offset:是否为 PAGE_SIZE 整数倍? (非文件映射时 offset 被忽略) 返回 -EINVAL 进入 do_mmap() 核心 建立 VMA + 页表映射

3.5 参数组合速查表

以下表格总结了不同场景下 prot 和 flags 的推荐组合:

场景 prot flags fd offset
DMA 缓冲区共享 PROT_READ | PROT_WRITE MAP_SHARED 设备 fd 0 或页对齐
进程间匿名共享 PROT_READ | PROT_WRITE MAP_SHARED | MAP_ANONYMOUS -1 0
只读文件映射 PROT_READ MAP_PRIVATE 文件 fd 0
可执行代码加载 PROT_READ | PROT_EXEC MAP_PRIVATE ELF fd 页对齐
大块匿名内存 PROT_READ | PROT_WRITE MAP_PRIVATE | MAP_ANONYMOUS -1 0
调试技巧: 使用 strace -e mmap 跟踪进程的 mmap 调用,可以观察实际传入的 prot、flags 和 offset 值,帮助排查参数错误。例如:mmap(NULL, 4194304, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0x7f1234560000

3.6 常见错误与排查

  • EINVAL:offset 不是页对齐,或 prot/flags 包含非法组合(如 PROT_NONE | PROT_READ)
  • EACCES:文件未以正确的权限打开(如 PROT_WRITE 但 fd 是只读打开的)
  • ENOMEM:无法分配足够的虚拟地址空间或物理内存
  • ENODEV:文件系统不支持 mmap(如某些伪文件系统)