4. 驱动中实现 mmap:file_operations 中的 mmap 回调、remap_pfn_range 函数详解、物理地址到用户空间的映射

在 Linux 驱动中,mmap 是实现零拷贝数据传输的核心机制之一。它允许用户空间程序直接访问内核或设备物理内存,绕过传统的 read/write 系统调用,从而大幅降低数据搬移的开销。本章将深入剖析驱动中 mmap 的实现细节,重点讲解 file_operations 中的 mmap 回调、核心函数 remap_pfn_range 的工作原理,以及如何将物理地址安全地映射到用户空间。

4.1 file_operations 中的 mmap 回调

每个字符设备驱动都可以通过 struct file_operations 注册一个 mmap 方法。当用户空间程序调用 mmap() 系统调用时,内核最终会调用驱动注册的这个回调函数。

成员 函数原型 说明
mmap int (*mmap)(struct file *filp, struct vm_area_struct *vma) 驱动核心入口,负责建立用户虚拟地址与物理内存的映射关系

关键参数解析:

  • filp:指向打开的设备文件结构体,驱动可通过 filp->private_data 获取设备私有数据。
  • vma:虚拟内存区域结构体,描述了用户空间请求的映射范围、权限和标志。驱动需要根据 vma 的信息来建立映射。

驱动 mmap 回调的标准实现步骤:

  1. 权限与参数校验:检查 vma->vm_flags 是否包含 VM_SHARED(共享映射)等必要标志。
  2. 获取物理地址:从设备或分配的内存缓冲区获取待映射的物理起始地址。
  3. 设置 VMA 操作:为 vma->vm_ops 指定自定义操作集(如 open/close 回调),用于管理映射区域的引用计数。
  4. 调用 remap_pfn_range:将物理页帧号(PFN)范围映射到 vma 描述的虚拟地址范围。
核心原则: 驱动 mmap 回调不负责分配物理内存,只负责将 已存在的物理内存 映射到用户空间。物理内存通常来自 kmalloc、dma_alloc_coherent 或设备 I/O 内存。
// 典型的驱动 mmap 回调实现框架
static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct my_device *dev = filp->private_data;
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long pfn;

    // 1. 校验映射大小和偏移
    if (size > dev->buf_size || offset >= dev->buf_size)
        return -EINVAL;

    // 2. 获取物理页帧号
    pfn = virt_to_phys(dev->buffer) >> PAGE_SHIFT;
    pfn += (offset >> PAGE_SHIFT);

    // 3. 设置 VMA 标志(禁止交换、禁止缓存穿透等)
    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;

    // 4. 建立映射
    if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
        return -EAGAIN;

    return 0;
}

4.2 remap_pfn_range 函数详解

remap_pfn_range 是驱动实现 mmap 时最核心的内核函数。它负责在进程的页表中创建或更新页表项,将连续的物理页帧映射到连续的虚拟地址空间。

参数 类型 描述
vma struct vm_area_struct * 描述目标虚拟内存区域的 VMA 结构体
addr unsigned long 用户空间虚拟起始地址,通常为 vma->vm_start
pfn unsigned long 物理页帧号(Page Frame Number),即物理地址右移 PAGE_SHIFT 位
size unsigned long 映射的字节大小,必须是页大小的整数倍
prot pgprot_t 页表项的保护标志,通常继承自 vma->vm_page_prot

函数行为与注意事项:

  • 页对齐要求:addr、pfn 和 size 都必须是页对齐的。驱动应确保传入的物理地址和大小满足此条件。
  • 缓存属性:prot 参数决定了映射的缓存策略。对于 DMA 缓冲区,通常需要设置为 非缓存(non-cacheable) 或 写合并(write-combine),以保证 CPU 与设备之间的数据一致性。
  • 错误返回:如果映射区域与现有映射冲突或内存不足,函数返回负的错误码(如 -EAGAIN)。
  • 不可用于 I/O 内存:对于设备 I/O 内存(如 MMIO),应使用 io_remap_pfn_range 替代,后者能正确处理不同架构的 I/O 内存语义。
常见陷阱: 不要对 remap_pfn_range 映射的内存执行 get_user_pages 操作。因为这类映射通常标记为 VM_IO,内核会拒绝建立反向映射,导致 get_user_pages 失败。

4.3 物理地址到用户空间的映射流程

从物理地址到用户空间虚拟地址的完整映射流程,涉及硬件 MMU、内核 VMA 管理以及驱动回调的协同工作。下图展示了这一过程的核心数据流。

物理地址到用户空间映射流程 用户空间进程 调用 mmap() 系统调用 内核 VMA 层 创建 vm_area_struct 驱动 mmap 回调 调用 remap_pfn_range 物理内存 DMA 缓冲区 / 设备内存 物理地址: 0x1000_0000 MMU 页表 页表项 (PTE) PFN + 权限 + 缓存属性 用户虚拟地址空间 映射起始: 0x7f00_0000 大小: 1 MB PFN 计算 映射建立 映射建立后的关键特性: 1. 用户空间读写映射区域时,CPU 直接通过 MMU 访问物理内存,无数据拷贝。 2. 驱动可通过 vma->vm_ops->fault 实现按需页面调度(高级用法)。 3. 映射的生命周期由 VMA 引用计数管理,用户调用 munmap 时自动解除。

4.4 缓存一致性管理

当使用 remap_pfn_range 映射 DMA 缓冲区时,缓存一致性 是必须处理的关键问题。CPU 缓存可能导致设备写入的数据在 CPU 读取时不可见,反之亦然。

解决方案对比:

方法 实现方式 适用场景 性能影响
非缓存映射 设置 pgprot_noncached(prot) 控制寄存器、状态寄存器等对时序敏感的场景 最低(CPU 直接访问设备)
写合并映射 设置 pgprot_writecombine(prot) 帧缓冲区、DMA 数据缓冲区 中等(允许 CPU 写合并)
手动缓存刷新 使用 dma_sync_single_for_cpu/device 共享映射且需要临时一致性的场景 较高(需要显式同步)
// 在驱动 mmap 回调中设置缓存属性
#include <asm/pgtable.h>

static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
    // ... 获取 pfn 等操作 ...

    // 设置为非缓存映射(适用于设备 I/O 内存)
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

    // 或者设置为写合并映射(适用于 DMA 数据缓冲区)
    // vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);

    if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
        return -EAGAIN;

    return 0;
}
最佳实践: 对于 DMA 数据缓冲区,优先使用 dma_alloc_coherent 分配内存。该函数自动返回非缓存或一致性的虚拟地址,驱动可以直接将其物理地址用于 remap_pfn_range,无需额外处理缓存属性。

4.5 完整实现示例:DMA 缓冲区的 mmap 映射

以下是一个完整的驱动代码片段,展示了如何将 DMA 一致性缓冲区映射到用户空间。

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/dma-mapping.h>
#include <linux/slab.h>

struct my_device {
    void *cpu_addr;
    dma_addr_t dma_addr;
    size_t buf_size;
    struct device *dev;
};

static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct my_device *mydev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long pfn;

    // 校验大小
    if (size > mydev->buf_size)
        return -EINVAL;

    // 获取 DMA 缓冲区的物理页帧号
    pfn = dma_to_phys(mydev->dev, mydev->dma_addr) >> PAGE_SHIFT;

    // 设置 VMA 标志:禁止交换、禁止 fork 时拷贝
    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;

    // 对于一致性 DMA 内存,无需额外设置缓存属性
    if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
        return -EAGAIN;

    return 0;
}

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .mmap = my_mmap,
    // ... 其他操作 ...
};