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(虚拟内存区域)创建后,内核会调用此回调。驱动在此函数中需要完成两件事:
- 设置 VMA 的标志和操作:通过
vma->vm_flags和vma->vm_ops控制映射行为。 - 建立页表映射:调用
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() 系统调用到驱动回调完成页表映射的完整数据流:
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 确认用户空间映射的虚拟地址范围与驱动预期一致。