4. mmap系统调用原理:文件映射与匿名映射、VMA结构体详解

mmap(memory map)是Linux内核中最核心的内存映射机制之一,它允许用户空间程序直接操作内核或设备的物理内存,而无需通过传统的read/write系统调用。理解mmap的原理,是掌握高性能驱动开发、DMA缓冲区分配以及零拷贝技术的基础。

核心概念: mmap的本质是在进程的虚拟地址空间(VMA)与物理内存(或文件/设备内存)之间建立映射关系。映射建立后,用户程序对映射区域的读写操作,会直接转化为对物理内存的访问。

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);
// 返回值:成功返回映射区域的起始地址,失败返回 MAP_FAILED (即 (void *) -1)

参数含义与常用取值:

参数 含义 常用取值
addr 建议的映射起始地址(通常传NULL由内核选择) NULL 或指定地址
length 映射区域的长度(字节) 必须为页大小(4096)的整数倍
prot 映射区域的访问权限 PROT_READ | PROT_WRITE | PROT_EXEC
flags 映射类型与行为标志 MAP_SHARED | MAP_PRIVATE | MAP_ANONYMOUS
fd 文件描述符(文件映射时有效) 有效fd(文件映射)或 -1(匿名映射)
offset 文件内的偏移量(文件映射时有效) 必须为页大小的整数倍

4.2 文件映射与匿名映射

根据 flags 参数中是否包含 MAP_ANONYMOUS,mmap分为两种基本类型:

4.2.1 文件映射(File-backed Mapping)

当 flags 不包含 MAP_ANONYMOUS 时,mmap将文件内容映射到进程的虚拟地址空间。对映射区域的读写,最终会同步回文件(如果使用 MAP_SHARED)。

  • 典型用途: 文件I/O加速、共享库加载、设备内存映射(如DMA缓冲区)。
  • 关键标志: MAP_SHARED(修改写回文件)、MAP_PRIVATE(写时复制,修改不写回文件)。

4.2.2 匿名映射(Anonymous Mapping)

当 flags 包含 MAP_ANONYMOUS 且 fd 为 -1 时,mmap分配一段由物理内存(或交换空间)支持的虚拟内存区域,不与任何文件关联。

  • 典型用途: 进程间共享内存(配合 MAP_SHARED)、动态内存分配(类似malloc的底层实现)。
  • 关键标志: MAP_ANONYMOUS | MAP_SHARED(共享匿名映射,可用于进程间通信)。
实战要点: 在驱动开发中,DMA缓冲区映射通常使用文件映射方式,通过驱动提供的mmap回调函数,将内核分配的DMA物理内存映射到用户空间。匿名映射则常用于用户态进程间的数据共享。

4.3 VMA结构体详解

VMA(Virtual Memory Area)是Linux内核中描述进程虚拟地址空间的基本数据结构。每个VMA对应一段连续的虚拟地址区间,具有相同的访问权限和映射属性。内核使用红黑树(rb_tree)和双向链表管理所有VMA。

4.3.1 VMA结构体核心字段

VMA结构体定义在 include/linux/mm_types.h 中,其核心字段如下:

struct vm_area_struct {
    unsigned long vm_start;          // VMA起始虚拟地址
    unsigned long vm_end;            // VMA结束虚拟地址(不包含)
    struct mm_struct *vm_mm;         // 所属进程的内存描述符

    unsigned long vm_flags;          // VMA标志位(读/写/执行/共享等)

    struct rb_node vm_rb;            // 红黑树节点,用于快速查找
    struct list_head vm_list;        // 双向链表节点,用于遍历

    const struct vm_operations_struct *vm_ops; // VMA操作函数集

    unsigned long vm_pgoff;          // 文件映射时的文件偏移(页为单位)
    struct file *vm_file;            // 映射的文件对象(匿名映射为NULL)

    void *vm_private_data;           // 私有数据指针(驱动常用)
};

4.3.2 VMA标志位(vm_flags)

标志 含义 对应mmap参数
VM_READ 允许读取 PROT_READ
VM_WRITE 允许写入 PROT_WRITE
VM_EXEC 允许执行 PROT_EXEC
VM_SHARED 共享映射(修改写回文件或共享内存) MAP_SHARED
VM_IO 映射I/O内存空间(设备内存) 驱动设置
VM_PFNMAP 映射物理页帧号(非page结构管理的内存) 驱动设置
VM_DONTEXPAND 禁止通过mremap扩展 驱动设置
VM_DONTDUMP 禁止在coredump中包含该区域 驱动设置

4.3.3 VMA操作函数集(vm_operations_struct)

驱动开发者最关注的是 vm_operations_struct,它定义了内核在操作VMA时调用的回调函数:

struct vm_operations_struct {
    void (*open)(struct vm_area_struct *vma);       // VMA被复制或引用时调用
    void (*close)(struct vm_area_struct *vma);      // VMA被解除映射时调用
    vm_fault_t (*fault)(struct vm_fault *vmf);      // 缺页异常处理(核心函数)
    vm_fault_t (*page_mkwrite)(struct vm_fault *vmf); // 页面写保护触发
    int (*access)(struct vm_area_struct *vma, unsigned long addr,
                  void *buf, int len, int write);   // 跨进程访问(ptrace等)
};
驱动开发关键: 在实现DMA缓冲区mmap时,驱动必须设置 VM_IO 和 VM_PFNMAP 标志,并实现 fault 回调(或使用 remap_pfn_range 直接建立页表)。open 和 close 回调用于管理引用计数,防止缓冲区被意外释放。

4.4 mmap在内核中的完整流程

下图展示了从用户态调用mmap到内核完成映射的完整流程:

mmap系统调用内核流程 mmap系统调用内核执行流程 用户态:mmap()调用 sys_mmap() 系统调用 do_mmap2() 参数校验 do_mmap() 创建VMA并插入红黑树 文件映射? 文件映射: 调用文件mmap回调 匿名映射: 分配物理页并映射 建立页表(缺页时触发fault回调) 返回映射起始地址给用户态 关键点 • VMA在do_mmap()中创建 • 驱动mmap回调在文件映射分支 • 缺页时才真正建立物理映射

4.5 驱动中的mmap回调实现

在字符设备驱动中,实现mmap的核心是填充 struct file_operations 中的 mmap 回调函数。典型的实现步骤如下:

  1. 验证参数: 检查offset和length是否合法。
  2. 设置VMA标志: 清除 VM_MAYWRITE(只读映射)或设置 VM_IO、VM_PFNMAP。
  3. 建立页表: 使用 remap_pfn_range() 或实现 fault 回调。
  4. 保存私有数据: 将设备结构体指针存入 vma->vm_private_data。
// 驱动mmap回调示例(使用remap_pfn_range)
static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct my_device *dev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long pfn = dev->dma_phys_addr >> PAGE_SHIFT;

    // 1. 设置VMA标志:标记为I/O内存,非page管理
    vma->vm_flags |= VM_IO | VM_PFNMAP;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

    // 2. 禁止coredump包含该区域
    vma->vm_flags |= VM_DONTDUMP | VM_DONTEXPAND;

    // 3. 建立物理地址到用户虚拟地址的页表映射
    if (remap_pfn_range(vma, vma->vm_start, pfn,
                        size, vma->vm_page_prot)) {
        return -EAGAIN;
    }

    // 4. 保存私有数据
    vma->vm_private_data = dev;

    return 0;
}

// 填充file_operations
static const struct file_operations my_device_fops = {
    .owner   = THIS_MODULE,
    .mmap    = my_device_mmap,
    // ... 其他操作
};
性能提示: 对于DMA缓冲区映射,建议使用 pgprot_noncached() 或 pgprot_writecombine() 设置页属性,避免CPU缓存导致的数据一致性问题。如果DMA缓冲区需要被CPU频繁读取,可以考虑 pgprot_writecombine() 以获得更好的读性能。

4.6 VMA与缺页异常

当用户程序首次访问mmap映射的虚拟地址时,会触发缺页异常(page fault)。内核通过VMA的红黑树快速定位到对应的VMA,然后调用VMA的 fault 回调(如果使用 remap_pfn_range 则直接建立页表,不会触发fault回调)。

下图展示了缺页异常处理流程中VMA的作用:

缺页异常与VMA查找流程 缺页异常处理:VMA查找与映射建立 用户访问映射地址 CPU触发缺页异常 do_page_fault() find_vma() 红黑树查找VMA 检查VMA权限(vm_flags) 调用vma->vm_ops->fault() 或 remap_pfn_range() VMA在缺页处理中的角色 1. 通过红黑树快速定位VMA 2. 从VMA获取访问权限 3. 从VMA获取vm_ops回调 4. 从VMA获取vm_private_data

4.7 本章小结

知识点 关键内容 驱动开发要点
文件映射 将文件或设备内存映射到用户空间 实现file_operations.mmap回调,使用remap_pfn_range
匿名映射 分配物理内存支持的虚拟区域,不与文件关联 用于进程间共享内存(MAP_SHARED | MAP_ANONYMOUS)
VMA结构体 描述进程虚拟地址空间的基本单元 设置VM_IO、VM_PFNMAP标志,保存私有数据
缺页异常 访问未建立物理映射的虚拟地址时触发 实现fault回调或使用remap_pfn_range预建立映射
页属性 控制缓存策略(non-cached / write-combine) 使用pgprot_noncached或pgprot_writecombine