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 回调的标准实现步骤:
- 权限与参数校验:检查
vma->vm_flags是否包含VM_SHARED(共享映射)等必要标志。 - 获取物理地址:从设备或分配的内存缓冲区获取待映射的物理起始地址。
- 设置 VMA 操作:为
vma->vm_ops指定自定义操作集(如open/close回调),用于管理映射区域的引用计数。 - 调用 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 管理以及驱动回调的协同工作。下图展示了这一过程的核心数据流。
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,
// ... 其他操作 ...
};