1. mmap 系统调用概述

mmap(memory map)是 Linux 系统中最核心的内存映射系统调用之一。它允许用户态程序将文件或设备内存直接映射到进程的虚拟地址空间,从而绕过传统的 read/write 中间缓冲层,实现零拷贝数据访问。本章从 mmap 的基本作用出发,对比用户态与内核态内存映射的本质差异,并通过性能基准数据说明 mmap 为何在高速数据采集、帧缓冲驱动和 DMA 缓冲区共享场景中成为首选方案。

1.1 mmap 的作用与核心语义

mmap 的核心语义是“建立虚拟地址到物理地址或文件页缓存的一级映射”。映射一旦建立,用户态程序可以直接通过指针读写映射区域,内核在缺页异常时按需填充物理页。mmap 支持两种映射类型:

  • 文件映射:将文件指定区间映射到进程地址空间,修改会同步回文件(MAP_SHARED)或仅对进程私有(MAP_PRIVATE)。
  • 匿名映射:不关联文件,由内核分配物理页,常用于进程间共享内存(MAP_ANONYMOUS | MAP_SHARED)。

在设备驱动场景中,mmap 通常用于将设备物理内存(如帧缓冲、DMA 缓冲区、MMIO 寄存器)直接暴露给用户态,避免数据在内核缓冲区和用户缓冲区之间的冗余拷贝。

关键指标: mmap 一次映射后,后续访问仅产生缺页开销(约 0.5–2 µs/页),而 read/write 每次调用都包含系统调用切换(约 0.3–1 µs)加数据拷贝(约 0.5–2 µs/4KB)。批量大块数据时,mmap 可减少 40%–70% 的 CPU 开销。
mmap 映射关系:用户虚拟地址 → 物理内存/文件页缓存 mmap 映射关系图 用户态进程 虚拟地址空间 内核态 VMA + 页表 物理内存 / 文件页缓存 mmap 系统调用 缺页 → 建立页表 用户态指针 直接读写 零拷贝访问 一次映射,多次直接访问 无系统调用 + 无数据拷贝

1.2 用户态与内核态内存映射的区别

用户态和内核态对内存映射的视角、权限和生命周期存在本质差异。下表从多个维度进行对比:

对比维度 用户态映射 内核态映射
地址空间 进程虚拟地址空间(0–3GB 典型) 内核虚拟地址空间(3GB+ 或独立地址)
页表 进程页表(每个进程独立) 内核页表(全局共享)
映射建立 通过 mmap 系统调用,由内核完成 通过 ioremap / kmap / vmap 直接建立
访问权限 受进程权限、文件描述符、MAP_SHARED/PRIVATE 控制 内核代码任意访问(无用户态保护)
缺页处理 缺页异常 → 内核填充物理页 通常预映射,无缺页
生命周期 随进程:munmap 或进程退出时释放 随驱动:模块卸载或显式 iounmap 释放
缓存策略 默认启用 CPU Cache(可配置 MAP_SYNC / MAP_NONCACHE) ioremap 默认 uncached;kmap 默认 cached
驱动开发要点: 用户态 mmap 映射设备内存时,驱动必须实现 file_operations 中的 .mmap 回调,并在其中调用 remap_pfn_range 或 io_remap_pfn_range 来建立物理地址到用户虚拟地址的页表映射。同时需要根据设备内存特性(是否支持 CPU Cache)设置 VMA 标志位(如 vm_flags |= VM_IO | VM_PFNMAP)。
用户态与内核态内存映射架构对比 用户态 vs 内核态内存映射架构 用户态映射路径 用户进程 mmap 系统调用 内核 VMA + 页表 物理内存 内核态映射路径 内核驱动模块 ioremap / kmap 内核虚拟地址 物理内存 / MMIO 系统调用边界

1.3 mmap 与 read/write 的性能对比

read/write 是传统的文件 I/O 方式,每次调用都涉及用户态 ↔ 内核态上下文切换以及数据在用户缓冲区和内核页缓存之间的拷贝。mmap 通过直接映射消除了冗余拷贝,在大块数据传输场景下优势显著。

性能差异核心原因

  • 系统调用次数: read/write 每次读写都需要一次系统调用;mmap 仅在建立映射时有一次系统调用,后续访问完全由缺页机制驱动,无额外系统调用。
  • 数据拷贝次数: read/write 至少一次 CPU 拷贝(内核页缓存 → 用户缓冲区);mmap 零拷贝,用户直接访问页缓存或设备物理内存。
  • 上下文切换: read/write 每次调用都切换上下文;mmap 映射建立后无切换。

基准测试数据(典型场景:4KB 块大小,1GB 数据量)

操作方式 系统调用次数 CPU 拷贝次数 总耗时(秒) CPU 利用率
read/write 262,144 次 2 次(读+写) 3.2 85%
mmap + memcpy 1 次(映射) 1 次(仅 memcpy) 1.8 45%
mmap 直接访问 1 次(映射) 0 次 0.9 25%
注意: mmap 并非在所有场景都优于 read/write。对于小数据块(< 512 字节)或随机访问模式,mmap 的缺页开销和 TLB 抖动可能抵消其零拷贝优势。此外,mmap 映射的文件如果被截断或修改,可能导致 SIGBUS 信号。驱动开发中,映射设备内存时还需考虑缓存一致性(Cache Coherency)问题。
mmap 与 read/write 数据路径对比 mmap vs read/write 数据路径对比 read/write 路径(2次拷贝) 用户缓冲区 内核页缓存 磁盘 / 设备 拷贝① 拷贝② mmap 路径(0次拷贝) 用户虚拟地址 页表映射 物理内存 / 设备 缺页映射 直接访问 零拷贝!

1.4 驱动开发中的 mmap 典型应用场景

在 Linux 驱动开发中,mmap 主要用于以下三类场景:

  1. 帧缓冲(Frame Buffer)驱动: 将显存物理地址映射到用户态,GUI 程序直接绘制像素,无需经过内核。
  2. DMA 缓冲区共享: 将 DMA 引擎使用的物理连续内存映射给用户态,实现高速数据采集(如摄像头、软件无线电)。
  3. MMIO 寄存器访问: 将设备控制寄存器的物理地址映射到用户态,用户态程序直接控制硬件(需谨慎处理缓存和原子性)。
驱动 mmap 回调模板(简化):
static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long phys_addr = MY_DEVICE_PHYS_BASE;
    unsigned long size = vma->vm_end - vma->vm_start;

    // 设置 VMA 标志:IO 内存 + 非标准页表
    vma->vm_flags |= VM_IO | VM_PFNMAP;
    // 禁止 VMA 合并
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

    // 建立物理地址到用户虚拟地址的页表映射
    if (remap_pfn_range(vma, vma->vm_start,
                        phys_addr >> PAGE_SHIFT,
                        size, vma->vm_page_prot))
        return -EAGAIN;

    return 0;
}

上述代码中,remap_pfn_range 是驱动 mmap 的核心函数,它将连续的物理页帧号(PFN)映射到用户 VMA 区间。pgprot_noncached 用于禁用 CPU Cache,保证对设备寄存器的读写顺序性和可见性——这是驱动 mmap 与普通文件 mmap 的关键区别之一。