4. 驱动中的 mmap 实现:struct file_operations 中的 mmap 回调与 remap_pfn_range() 详解

用户态调用 mmap() 后,内核最终会调用驱动注册在 struct file_operations 中的 mmap 回调函数。该回调的核心任务是将物理内存(或内核虚拟内存的物理页)直接映射到用户进程的虚拟地址空间。本章将深入剖析 mmap 回调的实现范式,并详细讲解最关键的辅助函数 remap_pfn_range() 的用法与参数。

4.1 struct file_operations 中的 mmap 回调

驱动通过填充 struct file_operations 的 mmap 成员来提供内存映射能力。该回调的原型如下:

int (*mmap) (struct file *filp, struct vm_area_struct *vma);

当 VMA(虚拟内存区域)创建后,内核会调用此回调。驱动在此函数中需要完成两件事:

  1. 设置 VMA 的标志和操作:通过 vma->vm_flags 和 vma->vm_ops 控制映射行为。
  2. 建立页表映射:调用 remap_pfn_range() 或 io_remap_pfn_range() 将物理地址范围映射到 VMA 覆盖的用户地址空间。
关键约束:mmap 回调运行在进程上下文,但持有 VMA 的写锁(mmap_lock)。回调中不能执行可能导致睡眠的长时间操作,如大量内存分配或复杂的 I/O 等待。

4.2 remap_pfn_range() 函数详解

remap_pfn_range() 是驱动实现 mmap 最常用的核心函数。它负责在指定 VMA 内,将连续的物理页框号(PFN)序列映射到用户虚拟地址。

4.2.1 函数原型与参数

int remap_pfn_range(struct vm_area_struct *vma,
                    unsigned long addr,
                    unsigned long pfn,
                    unsigned long size,
                    pgprot_t prot);
参数 类型 说明
vma struct vm_area_struct * 指向当前进程的 VMA 结构,由内核在调用 mmap 回调前创建。
addr unsigned long 用户虚拟地址的起始位置,必须与 vma->vm_start 对齐。
pfn unsigned long 要映射的物理内存的起始页框号(Page Frame Number)。
size unsigned long 映射区域的大小,以字节为单位,必须是 PAGE_SIZE 的整数倍。
prot pgprot_t 页表项的保护标志,通常使用 vma->vm_page_prot 或通过 pgprot_noncached() 修改。

4.2.2 返回值与错误码

  • 0:映射成功。
  • -EINVAL:参数无效,如 addr 或 size 未对齐、pfn 超出物理地址范围、VMA 标志冲突。
  • -ENOMEM:内核内存不足,无法分配页表。
  • -EAGAIN:映射被暂时阻塞,需重试(极少见)。

4.2.3 典型实现模板

static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
    unsigned long pfn_start;
    unsigned long size = vma->vm_end - vma->vm_start;

    // 1. 验证偏移和大小合法性
    if (offset + size > MY_DEVICE_SIZE)
        return -EINVAL;

    // 2. 计算物理页框号(假设设备内存从 0x10000000 开始)
    pfn_start = (MY_PHYS_BASE + offset) >> PAGE_SHIFT;

    // 3. 设置 VMA 标志:禁止交换、禁止扩展
    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;

    // 4. 建立页表映射(使用非缓存属性)
    if (remap_pfn_range(vma, vma->vm_start,
                        pfn_start, size,
                        pgprot_noncached(vma->vm_page_prot)))
        return -EAGAIN;

    return 0;
}
最佳实践:对于 I/O 内存,务必使用 pgprot_noncached() 或 pgprot_writecombine() 修改保护标志,避免 CPU 缓存导致的数据一致性问题。

4.3 mmap 回调与 remap_pfn_range 的协作流程

下图展示了从用户态 mmap() 系统调用到驱动回调完成页表映射的完整数据流:

mmap 系统调用到 remap_pfn_range 映射流程 用户态 mmap() 内核创建 VMA 驱动 mmap 回调 系统调用 回调 验证偏移/大小 设置 VMA 标志 remap_pfn_range() remap_pfn_range() 内部操作 遍历 VMA 所有页 计算页表项 (PTE) 设置物理地址 & 权限 刷新 TLB 用户进程可直接访问物理内存 说明:remap_pfn_range 不分配物理内存,仅修改页表,将用户虚拟地址指向已有的物理页框。

4.4 关键差异对比:remap_pfn_range vs. io_remap_pfn_range

内核提供了两个相似的映射函数,适用于不同的内存类型:

函数 适用场景 内存类型 架构差异
remap_pfn_range() 常规物理内存(如大块连续内存、DMA 缓冲区) 普通 RAM 或预留内存 所有架构通用
io_remap_pfn_range() I/O 内存(如 MMIO、PCIe BAR 空间) 设备寄存器或非 RAM 内存 在 x86 上等同于 remap_pfn_range;在 ARM/PowerPC 上会添加额外的 I/O 序屏障
常见错误:将 I/O 内存误用 remap_pfn_range() 映射,可能导致 CPU 缓存了寄存器读取结果,造成驱动逻辑错误。对于 MMIO 区域,始终优先使用 io_remap_pfn_range() 并配合 pgprot_noncached()。

4.5 故障排查要点

  • 映射失败返回 -EINVAL:检查 addr 是否与 vma->vm_start 一致,size 是否为 PAGE_SIZE 整数倍,pfn 是否有效。
  • 用户态访问触发 SIGBUS:通常因为 remap_pfn_range 未执行或返回错误,导致 VMA 内页表项无效。使用 strace 或 /proc/<pid>/smaps 检查映射状态。
  • 缓存一致性问题:映射 I/O 内存时未设置 pgprot_noncached(),导致读写被乱序或缓存。使用 dmesg 检查是否有架构相关的缓存警告。
  • VMA 标志冲突:如果用户态传入 MAP_SHARED 但驱动未设置 VM_SHARED,remap_pfn_range 会返回 -EINVAL。确保 vma->vm_flags 包含正确的共享/私有标志。
调试技巧:在 mmap 回调中添加 printk 打印 vma->vm_start、vma->vm_end、pfn 和返回值。使用 cat /proc/<pid>/maps 确认用户空间映射的虚拟地址范围与驱动预期一致。