1. mmap 系统调用原理:虚拟内存与物理内存映射关系、页表机制、缺页中断处理流程
mmap 是 Linux 内核中最核心的内存映射系统调用之一,它允许用户空间程序直接操作设备内存或文件映射区域,而无需经过传统的 read/write 数据拷贝路径。理解 mmap 的底层原理,是掌握 DMA 缓冲区共享与高性能驱动开发的基础。
1.1 虚拟内存与物理内存的映射关系
现代操作系统普遍采用虚拟内存管理机制。每个用户态进程拥有独立的虚拟地址空间(32 位系统为 4GB,64 位系统通常为 128TB),而物理内存则由所有进程共享。mmap 的核心作用就是在虚拟地址与物理地址(或设备 I/O 内存)之间建立映射。
关键概念: 虚拟地址(VA)是进程看到的地址,物理地址(PA)是硬件总线上的真实地址。mmap 通过操作进程的页表,将一段连续的虚拟地址映射到一段物理内存或设备内存区域。
驱动开发者视角: 在字符设备驱动中实现 mmap 回调函数时,本质上是告诉内核:“当用户访问这个虚拟地址范围时,请将其重定向到这块物理内存或 DMA 缓冲区。”
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 标志提前预分配页表,避免运行时缺页抖动。
1.3 缺页中断处理流程
当用户进程首次访问 mmap 映射的虚拟地址时,由于页表项(PTE)尚未填充有效物理地址,CPU 会触发缺页异常(Page Fault)。内核缺页中断处理程序负责解析异常地址、查找对应的 VMA、分配物理页框并填充页表。对于 mmap 驱动的 DMA 缓冲区共享场景,缺页中断处理流程尤为关键。
缺页中断处理步骤
- 异常捕获: CPU 在访问虚拟地址时,MMU 检测到 PTE 的 Present 位为 0,触发缺页异常,保存异常地址到 CR2 寄存器。
- 查找 VMA: 内核调用
find_vma()在进程的 VMA 红黑树中查找异常地址所属的虚拟内存区域。 - 权限检查: 验证访问类型(读/写/执行)是否与 VMA 的权限标志匹配,不匹配则发送 SIGSEGV。
- 调用 mmap 回调: 如果 VMA 关联了文件或设备驱动,内核调用驱动注册的
vm_ops->fault()或vm_ops->page_mkwrite()回调。 - 物理页分配与映射: 驱动回调中分配物理页框(或返回设备 DMA 缓冲区的物理页),调用
vm_insert_page()或remap_pfn_range()填充 PTE。 - 恢复执行: 页表更新完成后,CPU 重新执行触发异常的指令,此时 TLB 命中,正常访问物理内存。
| 缺页类型 | 触发条件 | 处理方式 | mmap 驱动关注点 |
|---|---|---|---|
| Major Fault | 页不在内存中,需要从磁盘/设备读取 | 阻塞等待 I/O 完成 | DMA 缓冲区通常已在内存中,应避免 Major Fault |
| Minor Fault | 页在内存中但 PTE 未建立映射 | 仅填充页表,不涉及 I/O | mmap 驱动典型场景,快速建立映射 |
| Write Fault | 写操作触发写时复制(COW)或只读页 | 复制页框或升级权限 | DMA 共享缓冲区需注意写权限管理 |
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 缓冲区的前提。