mmap 源码分析
准备
内核版本: 4.20.1
上一篇Linux环境写文件如何稳定跑满磁盘I-O带宽我们使用了mmap来帮助我们写文件稳定的跑满了磁盘I/O,这篇我们来详细介绍一下mmap()的细节和源码分析. 虽然我们使用mmap()只是简单的映射文件至内存中,而mmap()的设计实现主要涉及内核中的虚拟内存空间和内存映射等细节.
函数原型
|
这是mmap的函数原型,而系统调用的接口在mm/mmap.c中的:
|
虚拟内存区域管理
这里我们先介绍两个关于虚拟内存的数据结构。虚拟内存概念的相关资料网上已经足够的丰富,这里我们从内核的角度来分析。虚拟空间的管理是以进程为基础的,每个进程都有各自的虚存空间,除此之外,每个进程的“内核虚拟空间”是为所有的进程所共享的。一个进程的虚拟地址空间主要由两个数据结构来描述: mm_struct(内存描述符) 和vm_area_struct(虚拟内存区域描述符)。
The Memory Descriptor(内存描述符)
mm_struct包括进程中虚拟地址空间的所有信息,mm_struct定义在include/linux/mm_types.h:
|
结合mm_struct和下图32位系统典型的虚拟地址空间分布更能直观的理解(来自《深入理解计算机系统》):

Virtual Memory Area(虚拟内存区域描述符)

vm_area_struct描述了虚拟地址空间的一个区间, 一个进程的虚拟空间中可能有多个虚拟区间, vm_area_struct同样定义在include/linux/mm_types.h:
|
下图是某个进程的虚拟内存简化布局以及相应的几个数据结构之间的关系:

mmap映射执行流程
- 检查参数,并根据传入的映射类型设置
vma的flags. - 进程查找其虚拟地址空间,找到一块空闲的满足要求的虚拟地址空间.
- 根据找到的虚拟地址空间初始化
vma. - 设置
vma->vm_file. - 根据文件系统类型,将
vma->vm_ops设为对应的file_operations. - 将
vma插入mm的链表中.
源码分析
我们接下来进入mmap的代码分析:
do_mmap()
do_mmap()是整个mmap()的具体操作函数, 我们跳过系统调用来直接看具体实现:
|
mmap_region()
do_mmap()根据用户传入的参数做了一系列的检查,然后根据参数初始化vm_area_struct的标志vm_flags,vma->vm_file = get_file(file)建立文件与vma的映射, mmap_region()负责创建虚拟内存区域:
|
mmap_region()调用了call_mmap(file, vma): call_mmap根据文件系统的类型选择适配的mmap()函数,我们选择目前常用的ext4:
ext4_file_mmap()是ext4对应的mmap, 功能非常简单,更新了 file 的修改时间(file_accessed(flie)),将对应的 operation 赋给vma->vm_flags:
三个操作函数的意义:
.fault: 处理 Page Fault.map_pages: 映射文件至 Page Cache.page_mkwrite: 修改文件的状态为可写
|
通过分析mmap的源码我们发现在调用mmap()的时候仅仅申请一个vm_area_struct来建立文件与虚拟内存的映射,并没有建立虚拟内存与物理内存的映射。假如没有设置MAP_POPULATE标志位,Linux 并不在调用mmap()时就为进程分配物理内存空间,直到下次真正访问地址空间时发现数据不存在于物理内存空间时,触发Page Fault即缺页中断,Linux 才会将缺失的 Page 换入内存空间. 后面的文章我们会介绍 Linux 的缺页(Page fault)处理和请求 Page 的机制.
匿名映射
mmap()设置参数MAP_ANONYMOUS即可指定匿名映射,mmap的匿名映射并不执行文件或设备为映射地址,实际上映射的文件为/dev/zero,匿名页的物理内存一般分配用来作为进程的栈或堆的虚拟内存映射.
总结
常用的read()首先从文件的 Page 读取至内核页缓存 (Page Cache),Page Cache 位于内核内存空间, 所以需要再从内核态的内存空间拷贝到用户态的内存空间,而mmap直接建立了文件与虚拟地址空间的映射, 可以直接通过MMU根据虚拟地址空间的地址映射从用户物理内存区域读取数据, 省去了内核态拷贝数据至用户态的开销. 因为mmap的修改直接反映在物理内存时,所以kill -9进程也不会丢数据.
Q&A
vm_area_struct如何寻找对应的物理内存页?
vm_area_struct结构中并没有直接的存放Page指针的结构体,但包含虚拟地址的起始地址和结束地址vm_start和vm_end, 通过虚拟地址转换物理地址的方法可以直接寻找到指定的Page.
- 如何处理变长的文件?
RocksDB 使用了
mmap的方式写文件, 首先fallocate固定长度len的文件,然后通过mmap建立映射,使用一个base指针来滑动写入位置,写满长度len之后,调用munmap. 假如Close文件时写不够长度len, 即mummap写入的长度,然后使用ftruncate()将多余的映射部分截去.
mmap()之后memcpy()出现SIGBUS错误:
SIGBUS出现在缺页中断处理的过程中,即前面我们提到的ext4_file_vm_ops的ext4_file_vm_ops():do_mmap()有一行len = PAGE_ALIGN(len), 即根据传入的参数len进行页对齐后的长度来映射文件,但这里并没有考虑文件 size. 而缺页中断后真正的文件映射读取会考虑文件长度,即读取的 offset 假如超过了文件 size 页对齐后的长度,即会返回SIGBUS.
|
mmap()之后memcpy()出现SIGSEGV错误: (mm/memory.c:handle_mm_fault())
|
mmap是银弹吗?
不是, 随机写频繁触发的
Page Fault和脏页回写使得mmap避免在内核态与用户态之间的拷贝的优势减弱,下图是Linux环境写文件如何稳定跑满磁盘I-O带宽中方案三的mmap顺序写入的火焰图,我们可以更直观的看到mmap的瓶颈所在:
- mmap 设置
MAP_SHARED, 这部分使用的内存会计算在 RSS 中吗?
会,RSS(Resident set size)意为常驻使用内存,一般理解为真正使用的物理内存,当这部分设置了
MAP_SHARED的内存触发了Page Fault,被 OS 真正分配了物理内存,就会在 RSS 的数值上体现.
- mmap 设置
MAP_SHARED的匿名共享内存可以被 swap 吗?
可以, 设置 swap file, 匿名共享内存就可以被置换.
