1. mmap 基础概念

mmap(memory map)是 Linux 系统提供的一种高效内存映射机制,它允许用户空间程序直接访问内核空间或设备内存,而无需通过传统的 read/write 系统调用进行数据拷贝。在设备驱动开发中,mmap 是实现高性能数据交互的核心手段之一。

1.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 建议的映射起始地址(通常传 NULL 由内核选择) NULL
length 映射区域的字节长度 页对齐大小(如 4096)
prot 映射区域的访问权限 PROT_READ | PROT_WRITE
flags 映射类型和行为标志 MAP_SHARED 或 MAP_PRIVATE
fd 文件描述符(设备文件或普通文件) open() 返回的 fd
offset 文件内偏移量(必须页对齐) 0
关键机制: mmap 的核心在于缺页中断。调用 mmap 时,内核仅在进程的虚拟地址空间中建立 VMA(虚拟内存区域),并不立即分配物理页面。只有当进程实际访问映射区域时,才会触发缺页异常,由内核的缺页处理程序完成物理页面的分配和映射。

1.2 虚拟内存与物理内存映射关系

现代操作系统采用虚拟内存管理,每个进程拥有独立的 4GB(32位)或更大(64位)虚拟地址空间。mmap 的本质就是在虚拟地址和物理地址之间建立页表映射。

虚拟内存与物理内存映射关系 进程虚拟地址空间 VMA 区域 A(mmap 映射) VMA 区域 B(堆/栈) VMA 区域 C(设备映射) 页表:虚拟页 → 物理页帧 物理内存(RAM / 设备内存) 物理页帧 0x1000 物理页帧 0x2000 设备物理内存 0xF0000000 物理地址空间(按页管理) 映射单位:Linux 以页(通常 4KB)为单位建立映射,虚拟页通过 MMU 页表转换为物理页帧

映射过程中涉及的核心数据结构:

  • task_struct:进程描述符,包含 mm_struct 指针
  • mm_struct:内存描述符,管理整个进程地址空间
  • vm_area_struct (VMA):描述一段连续的虚拟地址空间,包含起始地址、结束地址、权限、文件映射信息等
  • 页表 (Page Table):由 MMU 硬件遍历,完成虚拟地址到物理地址的转换

1.3 mmap 在驱动中的作用

在设备驱动开发中,mmap 提供了用户态与内核态之间零拷贝的数据交换通道。传统 read/write 方式需要在内核缓冲区和用户缓冲区之间进行数据拷贝,而 mmap 允许用户程序直接访问内核管理的物理内存。

mmap 在驱动中的角色:零拷贝数据通路 用户空间 用户进程直接访问映射地址 (无数据拷贝,无系统调用开销) 内核空间 设备驱动 mmap 回调函数 (建立页表映射,注册物理内存) 硬件设备物理内存 (帧缓冲 / DMA 缓冲区 / 寄存器空间) 页表映射 物理地址映射 典型应用场景:摄像头帧缓冲、GPU 显存、FPGA 共享内存、高速数据采集卡

驱动中使用 mmap 的典型场景:

应用场景 传统方式 mmap 方式 性能提升
视频帧采集 read() 每次拷贝一帧 直接映射 DMA 缓冲区 消除拷贝,帧率提升 2~5 倍
GPU 显存访问 ioctl + 数据搬运 映射显存到用户空间 延迟降低 10 倍以上
FPGA 共享内存 驱动中转数据 用户直接读写 FPGA 内存 带宽接近硬件极限
大容量数据采集 多次 read 系统调用 一次 mmap + 指针操作 系统调用开销降为 0
驱动开发者视角: 实现 mmap 的关键在于驱动中提供 file_operations 结构体的 .mmap 回调函数。该回调需要调用 remap_pfn_range() 或 io_remap_pfn_range() 内核 API,将物理页帧号(PFN)映射到用户空间的 VMA 中。

1.4 mmap 与常规 I/O 的对比

理解 mmap 的优势需要对比传统 I/O 路径:

  • 传统 read/write:用户缓冲区 → 系统调用 → 内核缓冲区 → 设备 → 内核缓冲区 → 系统调用返回 → 用户缓冲区(至少 2 次数据拷贝)
  • mmap 方式:用户空间直接映射设备内存 → 缺页时建立页表 → 用户直接读写(0 次数据拷贝)
注意事项: mmap 虽然高效,但并非万能。映射大块物理内存时需考虑页表开销;映射设备寄存器时需注意缓存一致性(使用 pgprot_noncached() 设置页属性);多进程共享映射时需处理同步问题。