2. Linux 内核 mmap 接口实现:struct vm_area_struct 与 file_operations 回调

在 Linux 内核中,mmap 驱动的核心在于两个关键要素:内存区域描述符 struct vm_area_struct 和 文件操作回调 file_operations 中的 .mmap 方法。用户态调用 mmap() 后,内核通过缺页中断和 VMA 操作,将物理内存映射到用户空间。本章深入剖析这两个核心结构,为后续 DMA 缓冲区共享打下基础。

核心要点: VMA 是进程虚拟地址空间的基本管理单元;.mmap 回调是驱动开发者唯一需要实现的接口,负责建立 VMA 与物理页的关联。

2.1 struct vm_area_struct:虚拟内存区域描述符

struct vm_area_struct(简称 VMA)描述进程地址空间中一段连续的虚拟内存区域。每个 VMA 对应一个独立的映射,具有相同的权限和后备对象。内核使用红黑树 + 链表管理进程的所有 VMA。

2.1.1 核心字段解析

字段 类型 说明
vm_start unsigned long VMA 起始虚拟地址(页对齐)
vm_end unsigned long VMA 结束虚拟地址(页对齐,不包含)
vm_flags unsigned long 访问权限与类型标志(VM_READ, VM_WRITE, VM_SHARED 等)
vm_page_prot pgprot_t 页表项权限位(由 vm_flags 转换而来)
vm_ops const struct vm_operations_struct * VMA 操作函数表(缺页处理、关闭通知等)
vm_private_data void * 驱动私有数据指针(通常指向设备结构体或 DMA 缓冲区描述符)
vm_file struct file * 映射的文件对象(mmap 关联的文件)
vm_mm struct mm_struct * 所属进程的内存描述符
驱动开发关注点: vm_private_data 是驱动传递私有上下文的桥梁;vm_ops 中的 .fault 回调是实现按需物理页映射的关键。

2.1.2 VMA 标志位(vm_flags)

标志位决定了 VMA 的访问权限和映射类型。驱动在 .mmap 回调中应检查用户传入的标志是否合法。

标志 值 含义
VM_READ 0x00000001 可读
VM_WRITE 0x00000002 可写
VM_EXEC 0x00000004 可执行
VM_SHARED 0x00000008 共享映射(多进程可见)
VM_IO 0x00000010 映射 I/O 地址空间(禁止 core dump)
VM_DONTEXPAND 0x00000020 禁止 mremap 扩展
VM_DONTDUMP 0x00000040 禁止包含在 core dump 中
VM_PFNMAP 0x00000080 基于页帧号的映射(无 struct page 后备)
DMA 缓冲区映射关键: 对于 DMA 缓冲区共享,通常需要设置 VM_SHARED | VM_IO | VM_DONTEXPAND | VM_DONTDUMP,确保映射不被意外扩展或 dump,且多进程可共享同一物理内存。

2.1.3 VMA 操作结构体 vm_operations_struct

驱动可以自定义 VMA 的操作行为,最常用的是 .fault 和 .open / .close。

struct vm_operations_struct {
    void (*open)(struct vm_area_struct *vma);
    void (*close)(struct vm_area_struct *vma);
    vm_fault_t (*fault)(struct vm_fault *vmf);
    vm_fault_t (*huge_fault)(struct vm_fault *vmf, unsigned int order);
    void (*map_pages)(struct vm_fault *vmf, pgoff_t start_pgoff, unsigned long nr_pages);
    unsigned long (*pagesize)(struct vm_area_struct *vma);
};
  • open/close:VMA 引用计数增减时调用,用于管理共享资源的生命周期。
  • fault:缺页处理核心函数,驱动在此分配物理页并插入页表。
  • map_pages:批量映射优化接口,减少缺页次数。

2.2 file_operations 中的 .mmap 回调

驱动通过实现 file_operations 结构体中的 .mmap 方法,拦截用户态的 mmap() 系统调用。该回调是驱动建立用户虚拟地址与设备物理内存映射的入口。

2.2.1 回调原型与参数

int (*mmap)(struct file *filp, struct vm_area_struct *vma);
参数 说明
filp 打开的设备文件指针,可通过 filp->private_data 获取驱动私有数据
vma 内核已创建好的 VMA 对象,驱动需要设置其 vm_ops 和 vm_private_data

2.2.2 标准实现流程

  1. 检查映射标志:验证 vma->vm_flags 是否包含 VM_SHARED(DMA 共享要求)。
  2. 设置 VMA 标志:添加 VM_IO | VM_DONTEXPAND | VM_DONTDUMP。
  3. 设置 vm_ops:将自定义的 vm_operations_struct 赋值给 vma->vm_ops。
  4. 设置私有数据:将设备结构体或 DMA 缓冲区描述符存入 vma->vm_private_data。
  5. 预映射或延迟映射:调用 remap_pfn_range() 立即建立页表,或依赖 .fault 按需映射。
static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct my_device *dev = filp->private_data;
    unsigned long pfn = virt_to_phys(dev->dma_buf) >> PAGE_SHIFT;
    unsigned long size = vma->vm_end - vma->vm_start;

    // 1. 检查标志
    if (!(vma->vm_flags & VM_SHARED))
        return -EINVAL;

    // 2. 设置 VMA 标志
    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;

    // 3. 设置 vm_ops
    vma->vm_ops = &my_vm_ops;

    // 4. 设置私有数据
    vma->vm_private_data = dev;

    // 5. 立即建立映射(适用于固定物理地址的 DMA 缓冲区)
    if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
        return -EAGAIN;

    return 0;
}
性能提示: 对于预先分配的 DMA 缓冲区,使用 remap_pfn_range() 一次性映射所有页,避免后续缺页中断的开销。对于大缓冲区,建议在 .mmap 中完成映射,而非依赖 .fault 逐页处理。

2.3 VMA 与 .mmap 回调的协作流程

下图展示了用户态 mmap() 调用到驱动回调完成映射的完整路径:

用户态 mmap() 到驱动映射完成流程 用户态 mmap() 调用 内核 sys_mmap() 创建 VMA 结构体 查找 filp->f_op->mmap 驱动 .mmap 回调执行 设置 vm_flags / vm_ops 设置 vm_private_data remap_pfn_range() 返回用户态,用户空间可直接访问设备内存 说明:驱动 .mmap 回调中完成 VMA 配置和页表建立,用户态后续访问触发缺页时由 vm_ops->fault 处理

2.4 关键差异对比:remap_pfn_range vs. fault 缺页

对比维度 remap_pfn_range(预映射) .fault 缺页(按需映射)
映射时机 .mmap 回调中立即建立所有页表 用户态首次访问时逐页建立
适用场景 固定物理地址的 DMA 缓冲区、MMIO 稀疏映射、大缓冲区延迟分配
性能 无缺页开销,访问延迟低 首次访问有缺页中断开销
内存占用 立即占用页表内存 按需占用,节省页表空间
实现复杂度 简单,单次调用 需要实现 .fault 回调,管理物理页生命周期
选择建议: DMA 缓冲区通常物理连续且预先分配,推荐使用 remap_pfn_range() 预映射。对于视频帧缓冲等大块但非连续内存,可结合 vm_ops->fault 实现按需映射,减少页表占用。

2.5 驱动开发避坑指南

  • 检查 VM_SHARED:DMA 缓冲区共享必须要求共享映射,私有映射会导致 COW 破坏物理地址一致性。
  • 设置 VM_IO:防止内核对该区域进行非 I/O 相关的优化(如 swap、core dump)。
  • 避免 vm_ops 为空:如果未设置 vm_ops,内核使用默认缺页处理,可能导致错误映射。
  • 注意页对齐:vm_start 和 vm_end 自动页对齐,驱动无需额外处理,但物理地址必须页对齐。
  • 引用计数管理:在 vm_ops->open 中增加设备引用计数,close 中减少,防止设备在映射期间被释放。
常见错误: 在 .mmap 回调中直接使用 virt_to_phys() 转换 kmalloc 分配的内存。kmalloc 返回的虚拟地址可能不连续,应使用 dma_alloc_coherent() 或 get_free_pages() 确保物理连续性。