1. mmap 系统调用原理:虚拟内存与物理内存映射关系、页表机制、缺页中断处理流程

mmap 是 Linux 内核中最核心的内存映射系统调用之一,它允许用户空间程序直接操作设备内存或文件映射区域,而无需经过传统的 read/write 数据拷贝路径。理解 mmap 的底层原理,是掌握 DMA 缓冲区共享与高性能驱动开发的基础。

1.1 虚拟内存与物理内存的映射关系

现代操作系统普遍采用虚拟内存管理机制。每个用户态进程拥有独立的虚拟地址空间(32 位系统为 4GB,64 位系统通常为 128TB),而物理内存则由所有进程共享。mmap 的核心作用就是在虚拟地址与物理地址(或设备 I/O 内存)之间建立映射。

关键概念: 虚拟地址(VA)是进程看到的地址,物理地址(PA)是硬件总线上的真实地址。mmap 通过操作进程的页表,将一段连续的虚拟地址映射到一段物理内存或设备内存区域。
驱动开发者视角: 在字符设备驱动中实现 mmap 回调函数时,本质上是告诉内核:“当用户访问这个虚拟地址范围时,请将其重定向到这块物理内存或 DMA 缓冲区。”
虚拟地址到物理地址映射关系 虚拟地址 → 物理地址映射关系 进程虚拟地址空间 虚拟页 VMA (mmap 区域) 虚拟页 (堆/栈/数据) 虚拟页 (代码段) 虚拟页 (其他映射) 物理地址空间 / 设备内存 DMA 缓冲区 (物理连续) 物理页框 (内核分配) 物理页框 (文件缓存) 设备寄存器 (I/O 内存) mmap 建立映射后,用户态直接访问虚拟地址即等同于访问物理内存或设备 I/O 内存

1.2 页表机制:多级页表与地址转换

页表是 CPU 内存管理单元(MMU)用于将虚拟地址转换为物理地址的核心数据结构。Linux 在 x86-64 架构下使用 4 级页表(PGD、P4D、PUD、PMD、PTE),在 ARM64 下使用 3 级或 4 级页表。mmap 的本质操作就是修改进程的页表项(PTE),建立虚拟页到物理页框的映射。

多级页表层级与功能
页表层级 缩写 作用 覆盖范围(x86-64)
Page Global Directory PGD 顶级页表,指向下一级页表 512 GB
Page Upper Directory P4D 第二级(x86-64 特有) 1 GB
Page Middle Directory PUD 第三级,指向 PMD 2 MB
Page Table Entry PTE 最末级,直接指向物理页框 4 KB
性能陷阱: 每次缺页中断都需要遍历多级页表并填充 PTE,这是 mmap 首次访问时的性能开销来源。对于实时性要求高的 DMA 缓冲区共享场景,建议使用 MAP_POPULATE 标志提前预分配页表,避免运行时缺页抖动。
多级页表地址转换流程 多级页表地址转换流程(x86-64 4级页表) 虚拟地址: [63:48] 符号扩展 | [47:39] PGD索引 | [38:30] P4D索引 | [29:21] PUD索引 | [20:12] PMD索引 | [11:0] 页内偏移 CR3 → PGD 顶级页表基址 PGD → P4D 第1级索引 P4D → PUD 第2级索引 PUD → PMD 第3级索引 PMD → PTE 物理页框 最终物理地址 = 页框基址 + 页内偏移 mmap 驱动中通过 remap_pfn_range() 直接操作 PTE,将设备物理页框映射到用户虚拟地址 每次缺页中断时,内核自动遍历页表并填充 PTE,之后 CPU 可直接通过 TLB 缓存访问

1.3 缺页中断处理流程

当用户进程首次访问 mmap 映射的虚拟地址时,由于页表项(PTE)尚未填充有效物理地址,CPU 会触发缺页异常(Page Fault)。内核缺页中断处理程序负责解析异常地址、查找对应的 VMA、分配物理页框并填充页表。对于 mmap 驱动的 DMA 缓冲区共享场景,缺页中断处理流程尤为关键。

缺页中断处理步骤

  1. 异常捕获: CPU 在访问虚拟地址时,MMU 检测到 PTE 的 Present 位为 0,触发缺页异常,保存异常地址到 CR2 寄存器。
  2. 查找 VMA: 内核调用 find_vma() 在进程的 VMA 红黑树中查找异常地址所属的虚拟内存区域。
  3. 权限检查: 验证访问类型(读/写/执行)是否与 VMA 的权限标志匹配,不匹配则发送 SIGSEGV。
  4. 调用 mmap 回调: 如果 VMA 关联了文件或设备驱动,内核调用驱动注册的 vm_ops->fault() 或 vm_ops->page_mkwrite() 回调。
  5. 物理页分配与映射: 驱动回调中分配物理页框(或返回设备 DMA 缓冲区的物理页),调用 vm_insert_page() 或 remap_pfn_range() 填充 PTE。
  6. 恢复执行: 页表更新完成后,CPU 重新执行触发异常的指令,此时 TLB 命中,正常访问物理内存。
缺页中断类型与处理差异
缺页类型 触发条件 处理方式 mmap 驱动关注点
Major Fault 页不在内存中,需要从磁盘/设备读取 阻塞等待 I/O 完成 DMA 缓冲区通常已在内存中,应避免 Major Fault
Minor Fault 页在内存中但 PTE 未建立映射 仅填充页表,不涉及 I/O mmap 驱动典型场景,快速建立映射
Write Fault 写操作触发写时复制(COW)或只读页 复制页框或升级权限 DMA 共享缓冲区需注意写权限管理
缺页中断处理流程 缺页中断处理流程(mmap 驱动视角) 用户访问 mmap 地址 MMU 检测 PTE Present=0 CPU 触发缺页异常 (#PF) 内核: find_vma() 查找 VMA 调用 vm_ops->fault() 回调 驱动: 分配物理页 / 返回 DMA 缓冲区页 填充 PTE,恢复用户态执行 驱动 fault() 回调示例 struct page *page = virt_to_page(dma_buf); vm_insert_page(vma, addr, page);

1.4 mmap 驱动实现的关键内核 API

在 Linux 字符设备驱动中实现 mmap 功能,主要涉及以下内核 API 和数据结构:

  • struct vm_operations_struct:定义 VMA 的操作回调,包括 open()、close()、fault()、page_mkwrite() 等。
  • remap_pfn_range():将连续的物理页框范围直接映射到 VMA,适用于 DMA 缓冲区等物理连续内存。
  • vm_insert_page():在 fault() 回调中逐页插入物理页框,适用于按需分配的场景。
  • dma_alloc_coherent():分配 DMA 一致性缓冲区,返回 CPU 虚拟地址和物理地址(或 DMA 地址)。
// mmap 驱动核心实现片段
static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct my_dev *dev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long pfn = virt_to_phys(dev->dma_buf) >> PAGE_SHIFT;

    // 设置 VMA 标志:禁止交换、禁止合并
    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;
}

// 注册 vm_ops
static const struct vm_operations_struct my_vm_ops = {
    .open  = my_vma_open,
    .close = my_vma_close,
    .fault = my_vma_fault,
};

static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
    vma->vm_ops = &my_vm_ops;
    vma->vm_private_data = filp->private_data;
    my_vma_open(vma);  // 增加引用计数
    return 0;
}
性能调优要点: 对于 DMA 缓冲区共享场景,推荐在 mmap 回调中直接使用 remap_pfn_range() 一次性建立所有页表映射,而不是依赖缺页中断逐页填充。这样可以避免运行时缺页中断的开销,尤其适合高吞吐量、低延迟的数据采集和传输场景。

1.5 本章小结

mmap 系统调用的底层原理围绕虚拟内存与物理内存的映射关系展开,核心机制包括:

  • 通过多级页表实现虚拟地址到物理地址的转换,MMU 硬件保证访问效率。
  • 缺页中断处理流程是 mmap 按需映射的基础,驱动开发者通过实现 vm_ops->fault() 回调控制物理页的分配时机。
  • 对于 DMA 缓冲区共享,推荐使用 remap_pfn_range() 预建立映射,避免运行时缺页中断带来的性能抖动。
  • 理解页表机制和缺页中断流程,是后续章节中实现零拷贝数据传递、多进程共享 DMA 缓冲区的前提。