3. mmap系统调用原理:mmap在内核中的入口函数、文件映射与匿名映射、VMA(虚拟内存区域)结构体
mmap 是 Linux 驱动中实现用户态与内核态高效数据传输的核心系统调用。它允许用户程序将设备内存或内核缓冲区直接映射到进程的虚拟地址空间,从而避免 read/write 系统调用带来的数据拷贝开销。理解 mmap 的内核实现原理,是掌握 mmap 与 DMA 协同工作的基础。
3.1 mmap 系统调用的内核入口函数
用户态调用 mmap() 后,最终会进入内核的 do_mmap() 函数。该函数是 mmap 在内核中的统一入口,负责完成地址空间分配、VMA 创建以及文件/设备的映射操作。
| 函数/路径 | 层级 | 主要职责 |
|---|---|---|
sys_mmap() / ksys_mmap_pgoff() |
系统调用层 | 从用户态参数解析出 addr、length、prot、flags、fd、offset,转换为内核页偏移 |
do_mmap() |
核心层 | 检查权限、计算映射大小、分配虚拟地址、创建 VMA 对象、调用文件或设备的 mmap 回调 |
mmap_region() |
VMA 层 | 实际创建并初始化 struct vm_area_struct,建立虚拟地址与文件/设备的关联 |
file->f_op->mmap() |
驱动层 | 设备驱动实现的 mmap 回调,负责建立 VMA 与设备物理内存或 DMA 缓冲区的映射关系 |
关键路径: 用户态 mmap → sys_mmap → do_mmap → mmap_region → 驱动 mmap 回调。驱动开发者主要关注最后一个环节:实现 file_operations 中的 .mmap 方法。
3.2 文件映射与匿名映射
mmap 支持两种基本映射类型,它们在 VMA 的创建和物理内存管理上存在本质差异。
- 文件映射(File-backed Mapping): 将文件或设备内存映射到进程地址空间。VMA 的
vm_file指向对应的struct file,内核通过文件系统的address_space_operations或驱动提供的mmap回调来管理页面的换入换出。设备驱动通常使用此类型将物理内存(如 DMA 缓冲区)暴露给用户态。 - 匿名映射(Anonymous Mapping): 不与任何文件关联,用于进程间共享内存(如
MAP_ANONYMOUS | MAP_SHARED)或进程私有内存(如MAP_ANONYMOUS | MAP_PRIVATE)。VMA 的vm_file为 NULL,物理页面由内核的页分配器直接分配,通过shmem或swap机制管理。
| 特性 | 文件映射 | 匿名映射 |
|---|---|---|
| vm_file | 指向 struct file | NULL |
| 物理内存来源 | 文件页缓存 / 设备物理内存 | 内核页分配器(伙伴系统) |
| 页面换出 | 支持(写回文件) | 支持(交换到 swap 分区) |
| 驱动典型用途 | 映射 DMA 缓冲区、帧缓冲、寄存器 | 进程间共享内存(较少用于驱动) |
| mmap 回调 | 驱动必须实现 .mmap | 不需要(内核通用处理) |
驱动开发提示: 设备驱动几乎总是使用文件映射(MAP_SHARED)来映射 DMA 缓冲区。使用 MAP_PRIVATE 会导致写时复制(COW),破坏 DMA 缓冲区的物理连续性,应避免。
3.3 VMA(虚拟内存区域)结构体
VMA 是 Linux 内核管理进程虚拟地址空间的基本单元,由 struct vm_area_struct 描述。每个 VMA 代表一段连续的虚拟地址区间,具有相同的权限和映射属性。mmap 的核心工作就是创建并初始化一个 VMA。
struct vm_area_struct {
unsigned long vm_start; // VMA 起始虚拟地址
unsigned long vm_end; // VMA 结束虚拟地址(不包含)
struct mm_struct *vm_mm; // 所属进程的内存描述符
unsigned long vm_flags; // VMA 标志位(VM_READ, VM_WRITE, VM_SHARED 等)
struct rb_node vm_rb; // 红黑树节点,用于快速查找
struct list_head vm_list; // 链表节点,用于遍历
const struct vm_operations_struct *vm_ops; // VMA 操作函数集
struct file *vm_file; // 映射的文件对象(匿名映射为 NULL)
unsigned long vm_pgoff; // 文件偏移(页为单位)
struct anon_vma *anon_vma; // 匿名映射反向映射
/* ... 其他字段 ... */
};
VMA 的核心字段及其在驱动 mmap 中的作用:
| 字段 | 驱动 mmap 中的角色 |
|---|---|
vm_start / vm_end |
用户态可见的虚拟地址范围,驱动通过 remap_pfn_range() 将物理页映射至此区间 |
vm_flags |
决定映射的读写权限和共享属性;驱动需检查 VM_SHARED 和 VM_WRITE 以决定是否允许映射 |
vm_file |
指向设备文件,驱动可通过 file->private_data 获取设备结构体 |
vm_pgoff |
用户指定的偏移量(页对齐),驱动可据此选择映射不同的 DMA 缓冲区或寄存器区域 |
vm_ops |
驱动可设置 open/close 回调,在 VMA 创建/销毁时管理引用计数或资源 |
常见错误: 驱动 mmap 回调中未检查
vm_flags 中的 VM_SHARED 标志。如果用户使用 MAP_PRIVATE 映射 DMA 缓冲区,写操作会触发 COW,导致用户态写入的页面与驱动实际使用的物理页面不一致,造成数据损坏。
3.4 mmap 内核流程与 VMA 创建
下图展示了从用户态 mmap 调用到驱动 mmap 回调执行完毕、VMA 创建完成的完整内核流程。
3.5 驱动 mmap 回调的典型实现模式
设备驱动在 file_operations 中实现 .mmap 方法,核心任务是调用 remap_pfn_range() 将设备物理内存映射到用户空间。以下是标准实现模板:
static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct my_device *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. 检查映射类型:必须为 MAP_SHARED
if (!(vma->vm_flags & VM_SHARED)) {
pr_err("mmap: only MAP_SHARED is supported\n");
return -EINVAL;
}
// 2. 根据偏移选择映射区域(例如 DMA 缓冲区或寄存器)
if (offset == 0) {
// 映射 DMA 缓冲区
pfn = virt_to_phys(dev->dma_buf) >> PAGE_SHIFT;
} else if (offset == MY_REGISTER_OFFSET) {
// 映射设备寄存器
pfn = dev->regs_phys >> PAGE_SHIFT;
} else {
return -EINVAL;
}
// 3. 设置 VMA 操作(可选,用于 open/close 回调)
vma->vm_ops = &my_device_vm_ops;
vma->vm_private_data = dev;
// 4. 建立页表映射
if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot)) {
pr_err("remap_pfn_range failed\n");
return -EAGAIN;
}
return 0;
}
核心 API:
remap_pfn_range() 是驱动 mmap 中最关键的函数,它负责在 VMA 覆盖的虚拟地址范围内,逐页建立虚拟地址到物理页框号(PFN)的映射。该函数会直接操作进程的页表,不经过页缓存,因此映射后的内存访问具有确定性延迟,非常适合 DMA 缓冲区。
3.6 VMA 与页表的关系
VMA 是进程虚拟地址空间的“元数据”描述,而页表是 MMU 实际使用的“硬件映射表”。mmap 完成后,两者协同工作:
- VMA 层面: 记录映射的虚拟地址范围、权限、文件偏移等属性。当发生缺页异常时,内核根据 VMA 决定如何处理。
- 页表层面:
remap_pfn_range()直接填充页表项(PTE),将虚拟页映射到物理页。对于设备内存,页表项通常设置为 不可缓存(uncacheable) 或 写合并(write-combine),以保证 DMA 操作的一致性。
驱动开发者需要理解:VMA 的 vm_page_prot 字段决定了页表项的缓存属性。在 mmap 回调中,可以通过 pgprot_noncached() 或 pgprot_writecombine() 修改该字段,以匹配 DMA 缓冲区的硬件要求。
性能要点: 对于 DMA 缓冲区映射,建议使用
pgprot_writecombine() 而非 pgprot_noncached()。写合并允许 CPU 对连续写操作进行合并,显著提升写性能,同时保证 DMA 操作的正确性。仅当设备要求强序访问(如 FIFO 寄存器)时才使用非缓存映射。