4. mmap系统调用原理:文件映射与匿名映射、VMA结构体详解
mmap(memory map)是Linux内核中最核心的内存映射机制之一,它允许用户空间程序直接操作内核或设备的物理内存,而无需通过传统的read/write系统调用。理解mmap的原理,是掌握高性能驱动开发、DMA缓冲区分配以及零拷贝技术的基础。
4.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);
// 返回值:成功返回映射区域的起始地址,失败返回 MAP_FAILED (即 (void *) -1)
参数含义与常用取值:
| 参数 | 含义 | 常用取值 |
|---|---|---|
addr |
建议的映射起始地址(通常传NULL由内核选择) | NULL 或指定地址 |
length |
映射区域的长度(字节) | 必须为页大小(4096)的整数倍 |
prot |
映射区域的访问权限 | PROT_READ | PROT_WRITE | PROT_EXEC |
flags |
映射类型与行为标志 | MAP_SHARED | MAP_PRIVATE | MAP_ANONYMOUS |
fd |
文件描述符(文件映射时有效) | 有效fd(文件映射)或 -1(匿名映射) |
offset |
文件内的偏移量(文件映射时有效) | 必须为页大小的整数倍 |
4.2 文件映射与匿名映射
根据 flags 参数中是否包含 MAP_ANONYMOUS,mmap分为两种基本类型:
4.2.1 文件映射(File-backed Mapping)
当 flags 不包含 MAP_ANONYMOUS 时,mmap将文件内容映射到进程的虚拟地址空间。对映射区域的读写,最终会同步回文件(如果使用 MAP_SHARED)。
- 典型用途: 文件I/O加速、共享库加载、设备内存映射(如DMA缓冲区)。
- 关键标志:
MAP_SHARED(修改写回文件)、MAP_PRIVATE(写时复制,修改不写回文件)。
4.2.2 匿名映射(Anonymous Mapping)
当 flags 包含 MAP_ANONYMOUS 且 fd 为 -1 时,mmap分配一段由物理内存(或交换空间)支持的虚拟内存区域,不与任何文件关联。
- 典型用途: 进程间共享内存(配合
MAP_SHARED)、动态内存分配(类似malloc的底层实现)。 - 关键标志:
MAP_ANONYMOUS | MAP_SHARED(共享匿名映射,可用于进程间通信)。
4.3 VMA结构体详解
VMA(Virtual Memory Area)是Linux内核中描述进程虚拟地址空间的基本数据结构。每个VMA对应一段连续的虚拟地址区间,具有相同的访问权限和映射属性。内核使用红黑树(rb_tree)和双向链表管理所有VMA。
4.3.1 VMA结构体核心字段
VMA结构体定义在 include/linux/mm_types.h 中,其核心字段如下:
struct vm_area_struct {
unsigned long vm_start; // VMA起始虚拟地址
unsigned long vm_end; // VMA结束虚拟地址(不包含)
struct mm_struct *vm_mm; // 所属进程的内存描述符
unsigned long vm_flags; // VMA标志位(读/写/执行/共享等)
struct rb_node vm_rb; // 红黑树节点,用于快速查找
struct list_head vm_list; // 双向链表节点,用于遍历
const struct vm_operations_struct *vm_ops; // VMA操作函数集
unsigned long vm_pgoff; // 文件映射时的文件偏移(页为单位)
struct file *vm_file; // 映射的文件对象(匿名映射为NULL)
void *vm_private_data; // 私有数据指针(驱动常用)
};
4.3.2 VMA标志位(vm_flags)
| 标志 | 含义 | 对应mmap参数 |
|---|---|---|
VM_READ |
允许读取 | PROT_READ |
VM_WRITE |
允许写入 | PROT_WRITE |
VM_EXEC |
允许执行 | PROT_EXEC |
VM_SHARED |
共享映射(修改写回文件或共享内存) | MAP_SHARED |
VM_IO |
映射I/O内存空间(设备内存) | 驱动设置 |
VM_PFNMAP |
映射物理页帧号(非page结构管理的内存) | 驱动设置 |
VM_DONTEXPAND |
禁止通过mremap扩展 | 驱动设置 |
VM_DONTDUMP |
禁止在coredump中包含该区域 | 驱动设置 |
4.3.3 VMA操作函数集(vm_operations_struct)
驱动开发者最关注的是 vm_operations_struct,它定义了内核在操作VMA时调用的回调函数:
struct vm_operations_struct {
void (*open)(struct vm_area_struct *vma); // VMA被复制或引用时调用
void (*close)(struct vm_area_struct *vma); // VMA被解除映射时调用
vm_fault_t (*fault)(struct vm_fault *vmf); // 缺页异常处理(核心函数)
vm_fault_t (*page_mkwrite)(struct vm_fault *vmf); // 页面写保护触发
int (*access)(struct vm_area_struct *vma, unsigned long addr,
void *buf, int len, int write); // 跨进程访问(ptrace等)
};
VM_IO 和 VM_PFNMAP 标志,并实现 fault 回调(或使用 remap_pfn_range 直接建立页表)。open 和 close 回调用于管理引用计数,防止缓冲区被意外释放。
4.4 mmap在内核中的完整流程
下图展示了从用户态调用mmap到内核完成映射的完整流程:
4.5 驱动中的mmap回调实现
在字符设备驱动中,实现mmap的核心是填充 struct file_operations 中的 mmap 回调函数。典型的实现步骤如下:
- 验证参数: 检查offset和length是否合法。
- 设置VMA标志: 清除
VM_MAYWRITE(只读映射)或设置VM_IO、VM_PFNMAP。 - 建立页表: 使用
remap_pfn_range()或实现fault回调。 - 保存私有数据: 将设备结构体指针存入
vma->vm_private_data。
// 驱动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 size = vma->vm_end - vma->vm_start;
unsigned long pfn = dev->dma_phys_addr >> PAGE_SHIFT;
// 1. 设置VMA标志:标记为I/O内存,非page管理
vma->vm_flags |= VM_IO | VM_PFNMAP;
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
// 2. 禁止coredump包含该区域
vma->vm_flags |= VM_DONTDUMP | VM_DONTEXPAND;
// 3. 建立物理地址到用户虚拟地址的页表映射
if (remap_pfn_range(vma, vma->vm_start, pfn,
size, vma->vm_page_prot)) {
return -EAGAIN;
}
// 4. 保存私有数据
vma->vm_private_data = dev;
return 0;
}
// 填充file_operations
static const struct file_operations my_device_fops = {
.owner = THIS_MODULE,
.mmap = my_device_mmap,
// ... 其他操作
};
pgprot_noncached() 或 pgprot_writecombine() 设置页属性,避免CPU缓存导致的数据一致性问题。如果DMA缓冲区需要被CPU频繁读取,可以考虑 pgprot_writecombine() 以获得更好的读性能。
4.6 VMA与缺页异常
当用户程序首次访问mmap映射的虚拟地址时,会触发缺页异常(page fault)。内核通过VMA的红黑树快速定位到对应的VMA,然后调用VMA的 fault 回调(如果使用 remap_pfn_range 则直接建立页表,不会触发fault回调)。
下图展示了缺页异常处理流程中VMA的作用:
4.7 本章小结
| 知识点 | 关键内容 | 驱动开发要点 |
|---|---|---|
| 文件映射 | 将文件或设备内存映射到用户空间 | 实现file_operations.mmap回调,使用remap_pfn_range |
| 匿名映射 | 分配物理内存支持的虚拟区域,不与文件关联 | 用于进程间共享内存(MAP_SHARED | MAP_ANONYMOUS) |
| VMA结构体 | 描述进程虚拟地址空间的基本单元 | 设置VM_IO、VM_PFNMAP标志,保存私有数据 |
| 缺页异常 | 访问未建立物理映射的虚拟地址时触发 | 实现fault回调或使用remap_pfn_range预建立映射 |
| 页属性 | 控制缓存策略(non-cached / write-combine) | 使用pgprot_noncached或pgprot_writecombine |