3. mmap 系统调用原理

mmap 是 Linux 中实现用户空间与内核空间高效数据交换的核心系统调用。它允许用户程序将设备内存或文件直接映射到进程的虚拟地址空间,从而避免传统的 read/write 拷贝开销。在 DMA 缓冲区共享场景中,mmap 是驱动向用户态暴露物理连续内存的标准接口。

3.1 mmap 函数原型与参数

mmap 系统调用的用户态原型定义在 <sys/mman.h> 中:

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);

各参数含义如下表:

参数 类型 说明
addr void * 映射起始地址建议值,通常传 NULL 由内核选择
length size_t 映射长度(字节),必须为页大小(4KB)的整数倍
prot int 内存保护标志:PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE
flags int 映射类型与行为:MAP_SHARED、MAP_PRIVATE、MAP_ANONYMOUS 等
fd int 文件描述符(文件映射时有效,匿名映射传 -1)
offset off_t 文件内偏移量,必须为页大小对齐
关键约束:驱动开发中,offset 通常被驱动用于区分不同的 DMA 缓冲区或物理内存区域。用户态传入的 offset 会直接传递给驱动的 mmap 回调函数,驱动据此决定映射哪一段物理内存。

3.2 映射类型:文件映射 vs 匿名映射

mmap 根据 flags 参数分为两大类:

  • 文件映射(File-backed Mapping):将文件或设备内存映射到进程地址空间。设置 fd 为打开的文件描述符,offset 指定文件内起始位置。驱动设备映射属于此类。
  • 匿名映射(Anonymous Mapping):不关联任何文件,由内核分配零填充的物理页。设置 flags 包含 MAP_ANONYMOUS,fd 传 -1。常用于进程间共享内存(MAP_SHARED | MAP_ANONYMOUS)或 malloc 内部实现。
特性 文件映射 匿名映射
关联对象 文件或设备 无(内核匿名页)
fd 参数 有效文件描述符 -1
MAP_ANONYMOUS 不设置 必须设置
数据来源 文件内容或设备内存 全零页(写时拷贝)
驱动场景 DMA 缓冲区、帧缓冲、寄存器映射 用户态进程间共享内存
回写行为 MAP_SHARED 时写回文件 无回写(内存匿名)
驱动开发重点:设备驱动使用文件映射方式实现 mmap。用户态通过 open 设备节点获得 fd,然后调用 mmap(fd, ...) 触发驱动的 file_operations.mmap 回调。驱动在该回调中建立用户虚拟地址到物理 DMA 内存的映射关系。

3.3 VMA(虚拟内存区域)结构

每次 mmap 调用成功后,内核会在进程的虚拟地址空间中创建一个 VMA(Virtual Memory Area)。VMA 是描述一段连续虚拟内存区域的内核数据结构,定义在 linux/mm_types.h 中。

3.3.1 VMA 核心字段

字段 类型 说明
vm_start unsigned long VMA 起始虚拟地址
vm_end unsigned long VMA 结束虚拟地址(不含)
vm_flags unsigned long VMA 标志位:读/写/执行/共享/私有等
vm_file struct file * 关联的文件对象(文件映射时有效)
vm_ops const struct vm_operations_struct * VMA 操作函数表(驱动需实现)
vm_private_data void * 驱动私有数据指针

3.3.2 vm_operations_struct 回调

驱动在 mmap 回调中需要设置 vma->vm_ops,指向驱动自定义的操作函数表:

struct vm_operations_struct {
    void (*open)(struct vm_area_struct *vma);       // VMA 被复制时调用
    void (*close)(struct vm_area_struct *vma);      // VMA 被销毁时调用
    vm_fault_t (*fault)(struct vm_fault *vmf);      // 缺页处理(核心回调)
    vm_fault_t (*page_mkwrite)(struct vm_fault *vmf); // 页变为可写时调用
    int (*access)(struct vm_area_struct *vma, unsigned long addr,
                  void *buf, int len, int write);   // 内核访问用户页
};
驱动常见错误:许多初学者在 mmap 回调中直接调用 remap_pfn_range 映射物理内存,却忘记设置 vma->vm_ops。这会导致 VMA 缺少正确的生命周期管理,在进程 fork 或内存回收时出现内核崩溃。对于 DMA 缓冲区映射,至少应实现 open 和 close 回调来管理引用计数。

3.4 mmap 映射流程与 VMA 关系

下图展示了用户态调用 mmap 到驱动建立 VMA 的完整流程:

mmap 系统调用流程与 VMA 创建 用户进程 mmap(fd, ...) sys_mmap() 内核入口 do_mmap() 核心函数 分配 VMA 结构 (vm_area_struct) 驱动 mmap 回调 file_operations.mmap remap_pfn_range() 建立页表映射 VMA 插入进程红黑树 mmap 返回虚拟地址 用户态虚拟地址 直接访问 DMA 内存 VMA 生命周期关键点: 1. do_mmap() 分配 VMA 并设置 vm_file = 设备文件对象 2. 驱动 mmap 回调中设置 vma->vm_ops 和 vma->vm_private_data 3. remap_pfn_range() 建立虚拟地址到物理页的页表项

3.5 驱动 mmap 回调实现要点

驱动中 file_operations.mmap 回调的标准实现模板:

static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct my_dev *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. 校验 offset 和 size 合法性
    if (offset + size > dev->dma_buf_size)
        return -EINVAL;

    // 2. 设置 VMA 标志:禁止交换、禁止合并
    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;

    // 3. 设置 VMA 操作函数表
    vma->vm_ops = &my_vm_ops;
    vma->vm_private_data = dev;

    // 4. 调用 open 回调增加引用计数
    my_vm_ops.open(vma);

    // 5. 建立页表映射
    pfn = virt_to_phys(dev->dma_buf_virt) >> PAGE_SHIFT;
    pfn += (offset >> PAGE_SHIFT);
    if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
        return -EAGAIN;

    return 0;
}
VMA 标志位说明:
VM_IO — 标记该 VMA 映射的是 I/O 内存,内核不会对其做页缓存或回写。
VM_DONTEXPAND — 禁止通过 mremap 扩展该 VMA。
VM_DONTDUMP — 禁止在 core dump 中包含该 VMA 内容(DMA 缓冲区通常不需要 dump)。
这三个标志是 DMA 缓冲区映射的标准配置,缺一不可。

3.6 VMA 与缺页异常

当用户进程访问映射的虚拟地址但对应页表项无效时,CPU 触发缺页异常,内核调用 VMA 的 fault 回调。对于使用 remap_pfn_range 预先建立页表映射的 DMA 缓冲区,缺页异常不会发生,因为页表项在 mmap 时已全部建立。

但对于稀疏映射或按需映射的场景(如帧缓冲设备),驱动需要实现 fault 回调:

static vm_fault_t my_vma_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct my_dev *dev = vma->vm_private_data;
    unsigned long offset = (vmf->pgoff - vma->vm_pgoff) << PAGE_SHIFT;
    struct page *page;

    // 根据 offset 获取对应的物理页
    page = my_get_dma_page(dev, offset);
    if (!page)
        return VM_FAULT_SIGBUS;

    // 将物理页插入到 vmf 的页表
    vmf->page = page;
    return 0;
}
性能对比:
remap_pfn_range 方式:mmap 时一次性建立所有页表,访问时无缺页开销,适合连续物理内存。
fault 回调方式:按需建立页表,节省页表内存,适合稀疏映射或内存池管理。
DMA 缓冲区共享通常使用 remap_pfn_range 方式以获得最佳访问性能。

3.7 本章小结

  • mmap 系统调用通过 prot 和 flags 控制映射权限与类型,驱动开发中主要使用文件映射 + MAP_SHARED。
  • VMA 是内核管理虚拟内存区域的核心结构,驱动通过 vm_ops 和 vm_private_data 与 VMA 交互。
  • 驱动 mmap 回调的标准流程:校验参数 → 设置 VMA 标志 → 设置 vm_ops → 调用 open → 建立页表映射。
  • VM_IO | VM_DONTEXPAND | VM_DONTDUMP 是 DMA 缓冲区 VMA 的标准标志组合。
  • 缺页异常处理(fault 回调)提供了按需映射的灵活性,但 DMA 共享场景更推荐 remap_pfn_range 的预映射方式。