1. mmap 基础概念
mmap(memory map)是 Linux 系统提供的一个关键系统调用,它允许用户态程序将文件或设备直接映射到进程的虚拟地址空间。映射完成后,应用程序可以像访问普通内存一样通过指针读写文件内容,而无需显式调用 read() 或 write()。这一机制是高性能 I/O、共享内存和 DMA 缓冲区用户态暴露的基石。
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 |
void * |
建议的映射起始地址,通常传 NULL 由内核选择 |
length |
size_t |
映射的字节长度,必须是系统页大小的整数倍 |
prot |
int |
内存保护标志:PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE |
flags |
int |
映射类型与行为:MAP_SHARED、MAP_PRIVATE、MAP_ANONYMOUS 等 |
fd |
int |
文件描述符,匿名映射时传 -1 |
offset |
off_t |
文件偏移量,必须是页大小的整数倍 |
关键约束:
offset 和 length 必须按页对齐(通常为 4096 字节)。若传入非对齐值,内核会向下取整对齐,可能导致映射范围超出预期。
1.2 内存映射文件原理
mmap 的核心机制是将文件内容与进程虚拟地址空间建立直接映射关系,整个过程分为三个阶段:
- 建立映射:内核在进程的虚拟地址空间中分配一段连续的虚拟地址区间,并创建对应的 VMA(虚拟内存区域)结构体,记录映射的文件、偏移、权限等信息。
- 按需调页:映射建立时并不立即加载文件内容到物理内存。当进程首次访问某个虚拟页面时,触发缺页异常(page fault),内核从磁盘读取对应文件块到物理页框,并建立页表映射。
- 回写与同步:对于
MAP_SHARED映射,对内存的修改会在内核回写机制下同步回文件;MAP_PRIVATE则采用写时复制(Copy-on-Write)策略,修改不会影响原文件。
性能优势: 相比传统
read()/write() 需要用户态缓冲区与内核缓冲区之间的数据拷贝,mmap 允许内核直接通过缺页机制将磁盘数据映射到用户空间,消除了多余的内存拷贝,尤其适合大文件随机访问。
1.3 虚拟内存与物理内存映射关系
mmap 的底层依赖 Linux 的分页内存管理机制。每个进程拥有独立的虚拟地址空间,通过多级页表(x86-64 下为 4 级页表)将虚拟地址转换为物理地址。mmap 映射的本质就是在页表中插入或修改特定虚拟地址范围到物理页框的映射条目。
下图展示了 mmap 文件映射场景下,虚拟地址、物理页框与磁盘文件之间的三层关系:
1.4 映射类型对比
根据 flags 参数的不同,mmap 支持多种映射类型,其行为差异直接影响内存共享与数据持久化:
| 映射类型 | 共享性 | 修改可见性 | 回写行为 | 典型用途 |
|---|---|---|---|---|
MAP_SHARED |
多进程共享 | 对其他进程可见 | 内核自动回写文件 | 文件 I/O、进程间共享内存 |
MAP_PRIVATE |
写时复制 | 仅本进程可见 | 不回写文件 | 程序加载、调试、安全隔离 |
MAP_ANONYMOUS |
无文件后端 | 视共享标志而定 | 无回写 | 动态内存分配、匿名共享 |
MAP_FIXED |
固定地址映射 | 覆盖已有映射 | 取决于共享标志 | 特殊地址需求、JIT 编译器 |
常见陷阱: 使用
MAP_FIXED 时若指定地址已被占用,内核会直接覆盖原有映射,可能导致进程崩溃。建议优先使用 MAP_FIXED_NOREPLACE(Linux 4.17+)来避免意外覆盖。
1.5 页表与缺页异常流程
mmap 映射的按需调页机制依赖缺页异常处理流程。当进程访问尚未建立物理映射的虚拟地址时,CPU 触发缺页异常,内核执行以下步骤:
- 异常捕获:CPU 将缺页虚拟地址存入 CR2 控制寄存器,跳转到内核缺页处理函数
do_page_fault()。 - VMA 查找:内核通过红黑树查找该虚拟地址所属的 VMA,确认访问权限是否合法。
- 物理页框分配:从伙伴系统分配一个空闲物理页框,或从页框缓存中获取。
- 数据加载:对于文件映射,调用文件系统的
address_space_operations中的readpage方法,从磁盘读取数据到页框。 - 页表更新:在进程页表中插入虚拟页到物理页框的映射条目,设置权限位和脏位。
- 返回用户态:恢复被中断的指令重新执行,此时访问成功。
内核关键数据结构: 每个 VMA 由
struct vm_area_struct 描述,包含起始地址 vm_start、结束地址 vm_end、保护标志 vm_flags、文件操作集 vm_ops 等字段。mmap 系统调用的核心工作就是创建并初始化这个结构体。
下图展示了缺页异常处理的完整流程:
1.6 关键数据结构关系
mmap 映射在 Linux 内核中涉及多个核心数据结构的协同工作,下表总结了它们的作用与关联:
| 数据结构 | 所属子系统 | 作用 | 与 mmap 的关系 |
|---|---|---|---|
task_struct |
进程管理 | 描述进程,包含 mm 指针 |
每个进程的 mmap 映射列表挂载在此 |
mm_struct |
内存管理 | 描述进程地址空间 | 包含 mmap 红黑树根节点和 pgd 页表基址 |
vm_area_struct |
内存管理 | 描述一段连续的虚拟内存区域 | mmap 每次调用创建一个 VMA |
file |
VFS | 描述打开的文件 | VMA 通过 vm_file 指向映射的文件 |
address_space |
页缓存 | 管理文件页缓存树 | 缺页时通过 a_ops->readpage 读取数据 |
page |
物理内存 | 描述一个物理页框 | 映射的最终物理载体 |