4. 内核空间mmap接口实现:struct file_operations中的mmap成员
在Linux驱动模型中,mmap系统调用从用户空间发起后,最终会通过虚拟文件系统(VFS)层路由到具体设备驱动注册的struct file_operations中的mmap方法。本章将深入剖析该接口在内核侧的定义、注册流程以及完整的调用链路。
4.1 struct file_operations中的mmap成员
每个字符设备驱动都需要定义一个struct file_operations结构体实例,并将其注册到内核。该结构体中的mmap成员是驱动实现内存映射的入口点。
| 成员名称 | 函数原型 | 说明 |
|---|---|---|
mmap |
int (*mmap)(struct file *filp, struct vm_area_struct *vma) |
驱动核心映射方法,负责将设备内存或DMA缓冲区映射到用户空间 |
open |
int (*open)(struct inode *, struct file *) |
设备打开时调用,通常用于初始化资源 |
release |
int (*release)(struct inode *, struct file *) |
设备关闭时调用,用于清理 |
关键参数解析:
filp:指向已打开文件的struct file结构体,驱动可通过filp->private_data获取设备私有数据。vma:指向struct vm_area_struct,该结构体描述了用户空间虚拟地址区间(VMA)的属性,包括起始地址、长度、权限标志等。驱动需要根据此结构体完成物理内存到用户空间的映射。
4.2 驱动中mmap方法的注册流程
驱动实现mmap接口需要遵循一套标准流程,从定义操作函数到注册设备节点。以下为典型步骤:
- 定义mmap回调函数:在驱动源码中实现
xxx_mmap函数,填充struct file_operations的mmap成员。 - 分配并准备物理内存:在驱动初始化或
open时,使用kmalloc、dma_alloc_coherent或alloc_pages分配连续的物理内存或DMA缓冲区。 - 注册字符设备:通过
alloc_chrdev_region、cdev_init、cdev_add将file_operations注册到内核。 - 在mmap回调中调用remap_pfn_range:将物理页帧号(PFN)映射到用户空间的VMA。
最佳实践:在
mmap回调中,务必检查vma->vm_flags是否包含VM_SHARED标志,以确保映射行为符合预期。对于DMA缓冲区,通常需要设置VM_IO和VM_DONTEXPAND标志。
4.3 mmap调用流程:从用户态到内核态
下图展示了用户空间调用mmap后,内核VFS层如何路由到驱动注册的mmap方法:
流程要点:
- 用户空间调用
mmap(addr, length, prot, flags, fd, offset)后,陷入内核执行sys_mmap_pgoff。 - VFS层通过
filp->f_op->mmap找到驱动注册的回调函数。 - 驱动回调中必须调用
remap_pfn_range完成实际页表建立,否则映射无效。 - 映射成功后,用户空间可直接读写返回的虚拟地址,访问设备内存或DMA缓冲区。
4.4 驱动mmap方法实现示例
以下是一个典型的字符设备驱动mmap实现片段,展示了从定义到注册的完整代码结构:
// 1. 定义mmap回调函数
static int mydev_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct mydev_dev *dev = filp->private_data;
unsigned long pfn = virt_to_phys(dev->buffer) >> PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
// 设置VMA标志:IO内存、禁止交换、禁止扩展
vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;
// 映射物理内存到用户空间
if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
return -EAGAIN;
return 0;
}
// 2. 定义file_operations结构体
static const struct file_operations mydev_fops = {
.owner = THIS_MODULE,
.open = mydev_open,
.release = mydev_release,
.mmap = mydev_mmap,
};
// 3. 模块初始化时注册字符设备
static int __init mydev_init(void)
{
// 分配设备号、初始化cdev、添加设备
alloc_chrdev_region(&devno, 0, 1, "mydev");
cdev_init(&mydev_cdev, &mydev_fops);
cdev_add(&mydev_cdev, devno, 1);
return 0;
}
常见陷阱:
- 物理地址连续性:
remap_pfn_range要求映射的物理内存是连续的。使用kmalloc分配大块内存时可能失败,建议使用__get_free_pages或DMA API。 - 权限检查:如果用户空间以只读方式打开设备,但驱动映射了可写内存,可能导致安全漏洞。应在
mmap中检查vma->vm_flags的VM_WRITE标志。 - 内存释放:驱动必须在
release或模块卸载时正确释放映射的物理内存,否则会造成内存泄漏。
4.5 mmap与DMA缓冲区的结合
当驱动使用DMA缓冲区时,mmap的实现需要特别注意缓存一致性。以下表格对比了常规内存映射与DMA缓冲区映射的关键差异:
| 特性 | 常规内存映射 (kmalloc) | DMA缓冲区映射 (dma_alloc_coherent) |
|---|---|---|
| 物理连续性 | 连续(受限于分配大小) | 连续(硬件要求) |
| 缓存一致性 | 需手动处理(flush/invalidate) | 自动维护(一致性DMA) |
| 获取物理地址 | virt_to_phys() |
dma_alloc_coherent()返回的dma_handle |
| 映射API | remap_pfn_range |
dma_mmap_coherent() 或 remap_pfn_range |
| 典型应用 | 普通数据缓冲区 | 视频帧缓冲、网络数据包、音频流 |
推荐做法:对于DMA缓冲区,优先使用
dma_mmap_coherent()辅助函数,它内部处理了缓存属性和页表设置,比直接调用remap_pfn_range更安全。调用方式为:dma_mmap_coherent(dev, vma, cpu_addr, dma_handle, size)。
4.6 调用流程中的关键数据结构关系
下图展示了struct file、struct vm_area_struct和物理内存之间的关系:
4.7 本章小结
- 接口定义:驱动通过
struct file_operations的mmap成员向内核注册映射回调,原型为int (*mmap)(struct file *, struct vm_area_struct *)。 - 注册流程:实现回调 → 分配物理内存 → 注册字符设备 → 在回调中调用
remap_pfn_range。 - 调用链路:用户mmap → VFS层 → 驱动mmap回调 → remap_pfn_range → 建立页表。
- DMA特殊处理:使用
dma_alloc_coherent分配缓冲区,并优先使用dma_mmap_coherent进行映射,确保缓存一致性。 - 安全与健壮性:检查VMA标志、验证物理地址连续性、在模块卸载时正确释放内存。