内存映射(mmap)
一、mmap 概述
mmap
是memory map
(内存映射)的缩写,其为一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和程序虚拟地址空间具有一种虚拟对应关系。mmap()
系统调用使得进程之间通过映射同一个普通文件实现共享内存,普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问而不必再使用read()
或write()
等操作。
二、为何使用 mmap
Linux 通过内存映射机制来提供用户程序对内存直接访问的能力,内存映射的意思是把内核中特定部分的内存空间映射到用户级程序的内存空间去,这实现了用户空间和内核空间对某段内存的共享。内核在这块地址内存储变更的任何数据用户可以立即发现和使用,无须进行额外的数据拷贝。例如,使用mmap
获取磁盘上的文件信息,只需要将磁盘上的数据拷贝至共享内存,用户进程便可直接获取到信息,而传统的I/O
操作write and read
要求必须先把数据从磁盘拷贝至内核缓冲页中,然后再把数据拷贝到用户进程内存空间。两者相比,使用mmap
会减少一次拷贝操作,在高负载情况下这将带来巨大的性能提升;另外,使用内存访问来取代write
和read
能够简化一些应用程序逻辑。
从输入输出及内存空间角度分析使用mmap
的好处:
1、正常的read
和write
操作需要两次传输,一次是在文件和内核高速缓冲区之间,另一次是在高速缓冲区和用户空间缓冲区之间,使用mmap
就不需要第二次传输了。对于输入而言,一旦内核将相应的文件块映射进共享内存后,用户进程就能直接访问这些数据了;对于输出而言,用户进程仅仅需要修改内核中的内容,便可以依靠内核内存管理器来自动更新底层文件。
2、除了节省内核空间和用户空间之间的一次传输之外,mmap
还能够通过减少所需内存来减少资源消耗。使用read
和write
时数据将被保存在两个缓冲区中(用户和内核),而使用mmap
时,用户和内核共享一个缓冲区,此外如果多个进程正在同一个文件上执行I/O
,那么它们可通过使用mmap
共享一个内核缓冲区。
三、如何使用 mmap
头文件
<sys/mman.h>
函数
void* mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
参数
-
addr
:文件被映射到进程地址空间的起始地址,一般传入空指针(由内核自行选择起始地址) -
len
:文件被映射到进程地址空间的字节数,其从被映射文件开头offset
个字节开始计算 -
prot
:指定共享内存的访问权限,可选值为PROT_READ
(可读)、PROT_WRITE
(可写)、PROT_EXEC
(可执行)和PROT_NONE
(不可访问) -
flags
:MAP_FIXED
配合addr使用;MAP_HUGETLB
用于从 hugepage pool 中申请大页;MAP_ANONYMOUS
匿名映射,映射的区域将被初始化为 0;MAP_SHARED
共享映射,对被映射文件的写操作会写回 sync 源文件;MAP_PRIVATE
私有映射,对被映射文件的写不会写到源文件,而是触发 cow 机制重新分配内存写入(修改不会被其他进程察觉);MAP_POPULATE
对于文件映射,在mmap
时就会把文件内容预读到映射区(此特性只支持 private);MAP_LOCKED
效果等同于mlock()
,防止被映射的内存被交换到swap
,同时此flags
还会使mmap
阶段就产生缺页异常,为mmap
映射的地址分配物理内存 -
fd
:文件描述符,当传入 -1 时,需指定flags
为MAP_ANONYMOUS
,表明进程匿名映射(不涉及具体文件名,避免文件创建和打开,只用于具有亲缘关系的进程间通信) -
offset
:偏移量,一般为 0,表示从文件头开始映射
返回值
映射成功时函数返回映射区域的起始地址
四、两种映射方式
1、基于文件的映射
适用于任何进程之间,需要打开或创建一个文件然后再调用mmap()
// ...
fd = open(name, flag, mode);
assert(fd >= 0);
ptr = mmap(NULL, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED, fd, 0);
2、匿名映射
操作系统使用特殊文件提供的匿名内存映射,映射的分页会被初始化为 0,可以把它看成是一个内容总是被初始化为 0 的虚拟文件映射。在具有亲缘关系的进程之间(如父子进程), 当一个进程调用mmap()
之后又调用了fork()
, 则子进程会继承父进程映射后的空间,同时也继承了mmap()
返回的首地址,由此来实现父子进程之间的通信
// ...
ptr = mmap(NULL, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, fd, 0);
// ptr = mmap(NULL, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
pid = fork();
assert(pid >= 0);
if(0 == pid) // 子进程
{
lock(ptr);
handle();
unlock(ptr);
}
else // 父进程
{
lock(ptr);
handle();
unlock(ptr);
}
五、mmap 实现原理
1、进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
-
进程在用户空间调用
mmap()
-
在当前进程的虚拟地址空间中寻找一段空闲的满足要求的连续虚拟地址
-
为此虚拟区分配一个
vm_area_struct
结构,接着对这个结构的各个域进行初始化 -
将新建的
vm_area_struct
插入进程的虚拟地址区域链表或树中
2、内核空间调用mmap()
构建文件物理地址和进程虚拟地址的映射关系
-
为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符链接到内核“已打开文件集”中该文件的文件结构体
struct file
,每个文件结构体维护着和这个已打开文件相关各项信息 -
通过该文件的文件结构体,链接到
file_operations
模块,调用内核函数mmap()
,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma);
(不同于用户空间库函数) -
内核
mmap()
通过虚拟文件系统inode
模块定位到文件磁盘物理地址 -
通过
remap_pfn_range()
建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中
3、进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有任何文件数据拷贝至主存,真正的文件读取是当前进程发起read
或write
操作时。
-
进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常
-
缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程
-
调页过程先在
swap cache
中寻找需要访问的内存页,如果没有则调用nopage()
把所缺的页从磁盘装入到主存中 -
之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程
注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()
来强制同步,这样所写的内容就能立即保存到文件里。
六、mmap 优缺点
1、优点
-
对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代
I/O
读写,提高了文件读取效率 -
实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉
mmap
映射的页和其它的页并没有本质的不同所以得益于主要的 3 种数据结构的高效,其页映射过程也很高效:1)radix tree
,用于查找某页是否已在缓存;2)red black tree
,用于查找和更新vma
结构;3)双向链表,用于维护active
和inactive
链表,支持LRU
类算法进行内存回收 -
提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。同时,如果进程 A 和进程 B 都映射了区域 C,当 A 第一次读取 C 时通过缺页从磁盘复制文件页到内存中;但当 B 再读 C 的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据
-
可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件
I/O
操作,极大影响效率。这个问题可以通过mmap
映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap
都可以发挥其功效
2、缺点
-
对变长文件不适合
-
如果更新文件的操作很多,
mmap
避免两态拷贝的优势不再显著,大量的脏页回写引发频繁的随机I\O
, 所以在随机写很多的情况下,mmap
方式在效率上不一定会比带缓冲区的一般写逻辑更快