4. mmap 系统调用原理:原型、映射类型与映射标志

mmap 是 Linux 内核提供给用户空间最强大的内存管理接口之一。它允许用户程序将设备内存、文件内容或匿名内存直接映射到进程的虚拟地址空间,从而绕过传统的 read/write 系统调用,实现零拷贝的高效数据访问。理解 mmap 的原型、映射类型和映射标志,是编写 mmap 驱动的基础。

mmap 系统调用整体架构 mmap 系统调用架构 用户空间 进程虚拟地址空间 mmap() 返回映射起始虚拟地址 通过指针直接读写映射区域 内核空间 VMA (vm_area_struct) 管理 文件映射 / 匿名映射 / 设备映射 缺页中断 → 物理页分配 → 页表建立 核心思想:将内核/设备/文件的物理内存页直接映射到用户进程的页表中

4.1 mmap 函数原型

mmap 系统调用的用户空间接口定义在 <sys/mman.h> 中,其原型如下:

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数说明:

参数 类型 含义 典型值
addr void * 建议的映射起始地址(内核可忽略) NULL(由内核选择)
length size_t 映射区域的字节长度 设备寄存器区域大小
prot int 映射区域的内存保护标志 PROT_READ | PROT_WRITE
flags int 映射类型和共享属性 MAP_SHARED 或 MAP_PRIVATE
fd int 文件描述符(文件映射时使用) 设备文件 fd 或 -1
offset off_t 文件内的偏移量(页对齐) 物理地址偏移
驱动开发要点: 在设备驱动中,fd 对应通过 open() 打开的设备文件,offset 通常设置为物理地址的页内偏移。内核驱动通过 mmap 文件操作来响应这个调用。

4.2 映射类型:文件映射 vs 匿名映射

mmap 根据映射的数据来源分为两种基本类型:

文件映射与匿名映射对比 映射类型对比 文件映射 (File-backed) fd ≥ 0,关联具体文件 数据来源:磁盘文件 / 设备内存 写回策略:由 MAP_SHARED 决定 典型场景:设备寄存器映射 文件 I/O、共享库加载 匿名映射 (Anonymous) fd = -1,flags 含 MAP_ANONYMOUS 数据来源:物理内存(无后端文件) 初始内容:全零(清零页) 典型场景:进程间共享内存 malloc 大块内存、栈扩展

4.2.1 文件映射

  • 定义: 将文件或设备内存的某一段映射到进程地址空间。
  • 关键参数: fd 必须是一个有效的文件描述符,offset 指定文件内的起始位置。
  • 驱动视角: 设备驱动通过实现 file_operations 中的 .mmap 方法,将设备物理地址映射给用户空间。
  • 典型应用: 帧缓冲设备、DMA 缓冲区、硬件寄存器访问。

4.2.2 匿名映射

  • 定义: 映射一块不与任何文件关联的物理内存。
  • 关键参数: fd 设置为 -1,flags 中必须包含 MAP_ANONYMOUS。
  • 驱动视角: 匿名映射通常用于用户空间进程间通信(IPC),驱动中较少直接使用。
  • 典型应用: malloc() 内部实现、共享内存 shmget、栈自动扩展。
驱动开发重点: 设备驱动 mmap 属于文件映射类型。用户空间调用 mmap 时传入设备文件 fd,内核最终调用驱动注册的 .mmap 回调函数,由驱动负责将物理地址映射到用户虚拟地址空间。

4.3 映射标志:MAP_SHARED vs MAP_PRIVATE

映射标志决定了多个进程映射同一区域时的可见性和写传播行为,这是 mmap 语义的核心。

MAP_SHARED 与 MAP_PRIVATE 行为对比 MAP_SHARED vs MAP_PRIVATE MAP_SHARED(共享映射) 写操作直接修改底层文件/设备 其他进程可见修改 写回磁盘(文件映射时) 驱动 mmap 必须使用此标志 物理地址映射的唯一选择 ✅ 多进程共享同一物理页 ✅ 写穿透到硬件寄存器 MAP_PRIVATE(私有映射) 写操作触发“写时复制” 修改不写回底层文件 其他进程不可见修改 驱动 mmap 禁止使用此标志 会导致物理地址映射失效 ❌ 写时复制 → 物理页分离 ❌ 无法控制硬件寄存器

4.3.1 MAP_SHARED(共享映射)

  • 行为: 对映射区域的写操作会直接修改底层文件或设备内存,并且对其他映射了同一区域的进程可见。
  • 内核机制: 页表项设置为可写且共享,缺页时建立的物理页直接关联到文件页缓存或设备内存。
  • 驱动要求: 设备驱动 mmap 必须使用 MAP_SHARED,否则用户空间的写操作无法到达硬件寄存器。
  • 同步保证: 写操作对其他进程立即可见(取决于缓存一致性)。

4.3.2 MAP_PRIVATE(私有映射)

  • 行为: 对映射区域的写操作会触发“写时复制”(Copy-on-Write),创建一份私有副本,修改不写回底层文件。
  • 内核机制: 初始时页表项设置为只读,第一次写操作触发缺页中断,内核分配新物理页并复制内容。
  • 驱动限制: 设备驱动 mmap 禁止使用 MAP_PRIVATE,因为写时复制会导致用户空间操作的不是原始物理地址。
  • 典型用途: 程序加载、动态库映射(避免修改影响其他进程)。
驱动开发陷阱: 用户空间调用 mmap 时如果错误地使用了 MAP_PRIVATE,驱动虽然可能成功返回,但后续用户对映射区域的写操作将不会反映到硬件寄存器上。驱动开发者应在 .mmap 回调中检查 vma->vm_flags,确保包含 VM_SHARED 标志,否则返回错误。

4.4 映射标志组合速查表

标志组合 映射类型 写可见性 写回后端 驱动适用性
MAP_SHARED 文件映射 所有进程可见 立即写回 ✅ 必须使用
MAP_PRIVATE 文件映射 仅本进程可见 不写回 ❌ 禁止使用
MAP_SHARED | MAP_ANONYMOUS 匿名共享 所有进程可见 N/A IPC 场景
MAP_PRIVATE | MAP_ANONYMOUS 匿名私有 仅本进程可见 N/A malloc 实现

4.5 驱动开发中的 mmap 标志检查

在设备驱动的 .mmap 回调函数中,必须验证用户空间传递的映射标志是否符合要求。以下是一个典型的内核驱动检查代码片段:

static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
    // 检查映射标志:必须为共享映射
    if (!(vma->vm_flags & VM_SHARED)) {
        pr_err("mmap: MAP_PRIVATE is not supported\n");
        return -EINVAL;
    }

    // 检查保护标志:至少需要读权限
    if (!(vma->vm_flags & VM_READ)) {
        pr_err("mmap: PROT_READ is required\n");
        return -EACCES;
    }

    // 设置映射区域为 IO 内存(禁止缓存)
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

    // 调用 remap_pfn_range 建立页表映射
    if (remap_pfn_range(vma, vma->vm_start,
                        phys_addr >> PAGE_SHIFT,
                        vma->vm_end - vma->vm_start,
                        vma->vm_page_prot)) {
        pr_err("mmap: remap_pfn_range failed\n");
        return -EAGAIN;
    }

    return 0;
}
关键点总结:
  1. 用户空间 mmap 的 flags 参数中的 MAP_SHARED 对应内核 VMA 的 VM_SHARED 标志。
  2. 驱动必须检查 VM_SHARED,拒绝 MAP_PRIVATE 请求。
  3. 物理地址映射使用 remap_pfn_range() 或 io_remap_pfn_range() 完成。
  4. IO 内存需要设置 pgprot_noncached() 禁止 CPU 缓存,确保对硬件寄存器的读写直接生效。