内存映射文件的原理和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
14
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传输文件时,如果文件比较大,使用内存映射文件发送文件比较节省内存。代码如下:

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
import mmap
import os

from http.server import SimpleHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn

class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
pass

class HTTPRequestHandler(SimpleHTTPRequestHandler):

def do_GET(self):
f = self.send_head()
# send file with mmap
if hasattr(f, 'name'):
try:
with mmap.mmap(f.fileno(), os.path.getsize(f.name),
access=mmap.ACCESS_READ) as fm:
file_iter = iter(lambda: fm.read(10240), b'')
for data in file_iter:
self.wfile.write(data)
finally:
f.close()
else:
try:
self.copyfile(f, self.wfile)
finally:
f.close()
if __name__ == '__main__':
server = ThreadingHTTPServer(('', 7744), HTTPRequestHandler)
server.serve_forever()

小结

通过mmap模块将文件映射到内存后,我们就能高效又优雅的方式对文件进行随机访问。比如条用seekread等方法。

应该注意的是,对一个文件的内存映射并不会整个文件加载到内存中。相反,操作系统只是为文件内容保留一段虚拟内存而已。当访问文件的不同区域时,文件的这些区域会按照预期被读取并映射到内存区域中。

内存映射文件对文件的修改会自动反映到其他Python解析器上,使用这种特性可以作为进程间通信的一种方式。

转载请包括本文地址:https://allenwind.github.io/blog/5355
更多文章请参考:https://allenwind.github.io/blog/archives/