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的区别。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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上的运行情况: