内存映射文件的原理和Python实现

建立一个内存映射文件将使用操作系统虚拟内存系统直接访问文件系统中的文件,而不用使用常规的I/O函数。因此,内存映射不用每次访问都进行单独的系统调用,提高I/O性能。另外不用在缓冲区之间复制数据,因为内核和用户都可以直接访问内存。

Python模块mmap提供内存映射相关操作,通过该模块,可以把修改文件看做对普通的字符串进行修改。该模块提供的API和文件对象提供的API十分相似,可以把内存映射文件操作当做普通的文件操作,同时也可以方便地使用类似list数据结构(序列类型)的切片操作。

mmap模块

mmap.mmap 在Windows平台和Unix平台有差别。

Windows: mmap(fileno, length[, tagname[, access[, offset]]])

Unix: mmap(fileno, length[, flags[, prot[, access[, offset]]]])

access参数有如下三个选择

  • mmap.ACCESS_READ 表示只读访问

  • mmap.ACCESS_WRITE 表示“写通过”

  • mmap.ACCESS_COPY 写时复制,对内存操作不会写至文件

    length参数说明要映射的文件的大小,如果值为0,则表示映射整个文件,如果大于要映射的文件的当前大小,则扩展该文件。

    读操作

    读操作只要把access参数设置成 mmap.ACCESS_READ 即可。游戏GTA5中有一个文件 x64pack.rpf 的大小为36.7GB,我们以访问这个文件为例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import mmap
    import os

    file = r'F:\game\GTA5\x64pack.rpf'

    with open(file, 'r') as fd:
    with mmap.mmap(fd.fileno(), os.path.getsize(file), access=mmap.ACCESS_READ) as map:
    print(map.read(10))
    print(map[-1024:])
    print(map[1024*1024])

    内存映射文件对象还可以进行指针位移操作。要注意的是,分片操作不影响指针位置。

    1
    2
    3
    >>> map.tell()
    >>> map.seek(10000)
    >>> map.read(1024)

    写操作

    写操作使用的文件打开模式不是’w’,而是’r+’。且mmap使用的访问模式为 mmap.ACCESS_WRITE

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import mmap
    import shutil
    import os

    os.chdir("F:")
    file = 'Alan Walker,马里奥赛德 - Fade(钢琴版).mp3'
    string = b'hello, world'

    shutil.copyfile(file, 'copy_' + file)

    with open('copy_' + file, 'rb+') as fd:
    with mmap.mmap(fd.fileno(), 0, access=mmap.ACCESS_WRITE) as map:
    map[1024:1024+len(string)] = string

    再次打开文件看看修改,这次只用只读即可。

    1
    2
    3
    4
    5
    6
    7
    >>> fd = open('copy_' + file, 'rb+')
    >>> m = mmap.mmap(fd.fileno(), 0)
    >>> m[1024:1030]
    b'hello,'
    >>> m[1024:1024+len(b'hello, world')]
    b'hello, world'
    >>> m.close()

    复制操作

    访问模式设置为 mmap.ACCESS_COPY 文件修改不会写入到磁盘。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import mmap
    import shutil
    import os
    import random

    os.chdir("F:")
    file = 'Alan Walker,马里奥赛德 - Fade(钢琴版).mp3'
    string = b'hello, world'

    shutil.copyfile(file, 'copy_' + file)

    with open('copy_' + file, 'rb+') as fd:
    with mmap.mmap(fd.fileno(), 0, access=mmap.ACCESS_COPY) as map:
    random.shuffle(map)

    修改的内容不会写入到磁盘中,而是内存映射单独维护。

    使用正则表达式

    如果文件太多无法一次加载到内存中,可以使用内存映射文件。下面举一个文本搜索的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import mmap
    import re

    pattern = re.compile(r'python', re.DOTALL|re.IGNORECASE|re.MULTILINE)

    file = r'D:\database\store\algs4-data\movies.txt'

    with open(file, 'r') as fd:
    with mmap.mmap(fd.fileno(), 0, access=mmap.ACCESS_READ) as map:
    for match in pattern.findall(map):
    print(match[1])

    内存映射文件就是普通文本一样,直接供正则模块使用。

    一个简单的应用

    在使用http传输文件时,如果文件比较大,使用内存映射文件发送文件比较节省内存。代码如下: