mmap初探

缘起

前几日某业内一线程序员电面我,被问及了mmap(memory-mapped file)内容,虽然我以前就听说过这个概念,尤其是在Tachyon这个分布式内存系统中,被广泛使用,官方doc在Current Features第一条还特地强调了一下:“Tachyon’s native API is similar to that of the java.io.File class, providing InputStream and OutputStream interfaces and efficient support for memory-mapped I/O.”,可惜我以前对这个概念真的不是很熟悉,最近看了些文档,简单跑了点实验,所以这里写个短文,讲一讲mmap。

概念

下面是一张来自MSDN的图片, 很好地阐释了磁盘上的文件,物理内存中的file mapping object和各个进程所看到的file view这三者之间的关系。 核心概念看,mmap就是利用操作系统的虚拟内存技术,将文件映射到内存中(并不是copy到内存,所以mmap又有“lazy loading”的优势),当某一个进程需要读取该文件的某一部分时,不是使用传统的系统调用read(),而是让内存利用内存管理机制将所需要的内容载入到内存中。因为避免了相对缓慢的系统调用,mmap能够在大多数情况下明显的提升IO性能。另外,因为在用户进程看来,这个文件仿佛存在内存一般,所以可以用来当作文件共享给各个进程使用,在多进程环境下尤其能体现IO性能(这也就是为什么Tachyon会强调这一特性的原因)

一些小缺点
  1. 对于很小的文件不太合适,因为mmap需要进行内存对齐来提高性能,而一般操作系统的page size是4KB,所以5KB的文件就会占据8KB的空间。
  2. tradeoff between I/O and page faults。相关页已经加载到物理内存,但是尚未在MMU注册,操作系统需要在MMU中注册修改,这种情况容易发生在多进程读写环境中,又称之为软性页中断(minor page fault),从而导致在某些特定情况下,mmap的读写性能要差于标准I/O
简单实验

别人那里借来了点Python代码,做了些简单修改,分别在Win8.1 和CentOS 6上面跑了一下,很明显的能看到mmap在读写性能上与标准读写I/O的区别。

import mmap  
import os  
import time  
import sys

if len(sys.argv) > 1:  
    s = sys.argv[1]
else:  
    s = "test.txt" #size of the file should better larger than 50MB

def normal_mmap(mode):  
    f = open(s, 'r')
    buffer_size = 64
    retract_size = -32
    threshold = 1024 * 1024 * 50;
    start_time = time.time()
    if mode is 'normal':
        while True:
            f.seek(buffer_size, os.SEEK_CUR)
            f.seek(retract_size, os.SEEK_CUR)
            if f.tell() > threshold:
                break
        end_time = time.time()
        f.close()
        print('normal Time elapsed: {0}'.format(end_time - start_time))
    elif mode is 'mmap':
        m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
        while True:
            m.seek(buffer_size, os.SEEK_CUR)
            m.seek(retract_size, os.SEEK_CUR)
            if m.tell() > threshold:
                break
        end_time = time.time()
        m.close()
        f.close()
        print('mmap Time elapsed: {0}'.format(end_time - start_time))
    else:
        print('illegal input, please choose "normal" or "mmap" mode')

def main():  
    for i in range(1, 5):
        normal_mmap('normal')
        time.sleep(3)
        normal_mmap('mmap')

if __name__ == '__main__':  
    main()

在CentOS 6上的运行情况: