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 标准实现流程
- 检查映射标志:验证
vma->vm_flags是否包含VM_SHARED(DMA 共享要求)。 - 设置 VMA 标志:添加
VM_IO | VM_DONTEXPAND | VM_DONTDUMP。 - 设置 vm_ops:将自定义的
vm_operations_struct赋值给vma->vm_ops。 - 设置私有数据:将设备结构体或 DMA 缓冲区描述符存入
vma->vm_private_data。 - 预映射或延迟映射:调用
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() 调用到驱动回调完成映射的完整路径:
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() 确保物理连续性。