内存映射MMAP
介绍
mmap 是一种操作系统提供的系统调用,用于在进程的虚拟地址空间中创建内存映射区域,实现文件和内存之间的直接映射。
一般来说,修改一个文件的内容需要如下3个步骤:
- 把文件内容读入到内存中。
- 修改内存中的内容。
- 把内存的数据写入到文件中。
如果使用代码来实现上面的过程,代码如下:从图中可以看出,1
2
3read(fd, buf, 1024); // 读取文件的内容到buf
... // 修改buf的内容
write(fd, buf, 1024); // 把buf的内容写入到文件页缓存(page cache)
是读写文件时的中间层,内核使用页缓存
与文件的数据块关联起来。所以应用程序读写文件时,实际操作的是页缓存
。
从传统读写文件的过程中,我们可以发现有个地方可以优化:如果可以直接在用户空间读写 页缓存
,那么就可以免去将 页缓存
的数据复制到用户空间缓冲区的过程。
那么,有没有这样的技术能实现上面所说的方式呢?答案是肯定的,就是 mmap
。
使用 mmap
系统调用可以将用户空间的虚拟内存地址与文件进行映射(绑定),对映射后的虚拟内存地址进行读写操作就如同对文件进行读写操作一样。原理如图所示:
前面我们介绍过,读写文件都需要经过 页缓存
,所以 mmap
映射的正是文件的 页缓存
,而非磁盘中的文件本身。由于 mmap
映射的是文件的 页缓存
,所以就涉及到同步的问题,即 页缓存
会在什么时候把数据同步到磁盘。
Linux 内核并不会主动把 mmap
映射的 页缓存
同步到磁盘,而是需要用户主动触发。
同步 mmap
映射的内存到磁盘的 4 个时机:
- 调用
msync
函数主动进行数据同步(主动)。 - 调用
munmap
函数对文件进行解除映射关系时(主动)。 - 进程退出时(被动)。
- 系统关机时(被动)。
相关函数
函数原型
1 | void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); |
用于将文件或匿名内存映射到进程的虚拟地址空间中
返回说明
成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1], error被设为以下的某个值:
1 | 1 EACCES:访问出错 |
参数
start:映射区的开始地址
length:映射区的长度
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
1 | 1 PROT_EXEC :页内容可以被执行 |
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
1 | 1 MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。 |
fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
offset:被映射对象内容的起点
相关函数
1 | int munmap( void * addr, size_t len ) |
成功执行时,munmap()
返回0。失败时,munmap
返回-1,error
返回标志和mmap一致;
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小;
当映射关系解除后,对原来映射地址的访问将导致段错误发生。
1 | int msync( void *addr, size_t len, int flags ) |
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()
后才执行该操作。
可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
优缺点
优点
1、对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。
2、实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
3、提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
4、可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。
缺点
- 由于 mmap 使用时必须实现指定好内存映射的大小,因此 mmap 并不适合变长文件;
- 如果更新文件的操作很多,mmap 避免两态拷贝的优势就被摊还,最终还是落在了大量的脏页回写及由此引发的随机 I/O 上,所以在随机写很多的情况下,mmap 方式在效率上不一定会比带缓冲区的一般写快;
- 读/写小文件(例如 16K 以下的文件),mmap 与通过 read 系统调用相比有着更高的开销与延迟;同时 mmap 的刷盘由系统全权控制,但是在小数据量的情况下由应用本身手动控制更好;
- mmap 受限于操作系统内存大小:例如在 32-bits 的操作系统上,虚拟内存总大小也就 2GB,但由于 mmap 必须要在内存中找到一块连续的地址块,此时你就无法对 4GB 大小的文件完全进行 mmap,在这种情况下你必须分多块分别进行 mmap,但是此时地址内存地址已经不再连续,使用 mmap 的意义大打折扣,而且引入了额外的复杂性;
使用场景
- 多个线程以只读的方式同时访问一个文件,这是因为 mmap 机制下多线程共享了同一物理内存空间,因此节约了内存。案例:多个进程可能依赖于同一个动态链接库,利用 mmap 可以实现内存仅仅加载一份动态链接库,多个进程共享此动态链接库。
- mmap 非常适合用于进程间通信,这是因为对同一文件对应的 mmap 分配的物理内存天然多线程共享,并可以依赖于操作系统的同步原语;
- mmap 虽然比 sendfile 等机制多了一次 CPU 全程参与的内存拷贝,但是用户空间与内核空间并不需要数据拷贝,因此在正确使用情况下并不比 sendfile 效率差;