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接口需要遵循一套标准流程,从定义操作函数到注册设备节点。以下为典型步骤:

  1. 定义mmap回调函数:在驱动源码中实现xxx_mmap函数,填充struct file_operations的mmap成员。
  2. 分配并准备物理内存:在驱动初始化或open时,使用kmalloc、dma_alloc_coherent或alloc_pages分配连续的物理内存或DMA缓冲区。
  3. 注册字符设备:通过alloc_chrdev_region、cdev_init、cdev_add将file_operations注册到内核。
  4. 在mmap回调中调用remap_pfn_range:将物理页帧号(PFN)映射到用户空间的VMA。
最佳实践:在mmap回调中,务必检查vma->vm_flags是否包含VM_SHARED标志,以确保映射行为符合预期。对于DMA缓冲区,通常需要设置VM_IO和VM_DONTEXPAND标志。

4.3 mmap调用流程:从用户态到内核态

下图展示了用户空间调用mmap后,内核VFS层如何路由到驱动注册的mmap方法:

mmap系统调用内核侧路由流程 mmap系统调用内核侧路由流程 用户空间 mmap() VFS: do_mmap_pgoff() 查找 filp->f_op->mmap 驱动 xxx_mmap() (filp, vma) remap_pfn_range(vma, addr, pfn, size, prot) 关键内核API 将物理页帧号(PFN) 映射到用户VMA
流程要点:
  • 用户空间调用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和物理内存之间的关系:

mmap关键数据结构关系图 mmap关键数据结构关系图 struct file f_op → file_operations private_data → 设备结构体 struct vm_area_struct vm_start, vm_end vm_flags, vm_page_prot 物理内存页 连续物理页帧 PFN: 页帧号 映射建立过程 1. 驱动从 filp->private_data 获取设备内存物理地址 2. 计算物理页帧号 PFN = phys_addr >> PAGE_SHIFT 3. remap_pfn_range 将 PFN 映射到 vma->vm_start 起始的虚拟地址

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标志、验证物理地址连续性、在模块卸载时正确释放内存。