为什么Python的mmap不能处理大文件?

49

[编辑:此问题仅适用于32位系统。如果您的计算机、操作系统和Python实现均为64位,则映射大型文件可靠且极其高效。]

我正在编写一个模块,其中包括对文件进行按位读取的功能。这些文件可能很大(数百GB),因此我编写了一个简单的类,让我可以像处理字符串一样处理文件,并隐藏所有的寻址和读取细节。

在编写我的包装类时,我不知道有 mmap 模块 这个工具。阅读 mmap 的文档后,我认为“太好了 - 这正是我需要的,我将去掉自己的代码并使用 mmap 代替。它可能更有效率,而且删除代码总是好事。”

问题是 mmap 不适用于大型文件!这对我来说非常令人惊讶,因为我认为这是应用最明显的场景之一。如果文件大小超过几个GB,那么就会出现“EnvironmentError: [Errno 12] Cannot allocate memory”的错误。这只发生在32位的 Python 构建中,所以似乎是已经耗尽地址空间了,但我找不到任何相关的文档。

我的代码只有:

f = open('somelargefile', 'rb')
map = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)

所以我的问题是,我有什么明显的遗漏吗? 有没有一种方法可以使mmap在大文件上可移植工作,还是我应该回到我的天真的文件包装器?


更新:似乎有一种感觉,Python的mmap应该有与POSIX mmap相同的限制。为了更好地表达我的挫败感,这里有一个简单的类,它具有mmap部分功能。

import os

class Mmap(object):
    def __init__(self, f):
        """Initialise with a file object."""
        self.source = f

    def __getitem__(self, key):
        try:
            # A slice
            self.source.seek(key.start, os.SEEK_SET)
            return self.source.read(key.stop - key.start)
        except AttributeError:
            # single element
            self.source.seek(key, os.SEEK_SET)
            return self.source.read(1)

它是只读的,没有任何复杂的功能,但我可以像使用mmap一样使用它:

map2 = Mmap(f)
print map2[0:10]
print map2[10000000000:10000000010]

除了文件大小没有限制外,问题并不太难。


但它没有mmap的功能。 mmap公开了一个缓冲区接口,您可以对其进行正则表达式匹配。 mmap支持向文件写入,并支持共享内存。 您的代码,甚至您的方法都无法做到这一点。 - Andrew Dalke
2
它具有一些mmap的功能,但不会受到地址空间限制的影响。这只是一个玩具代码 - 我并不声称它是一个替代品!我认为这种模仿mmap功能的方法没有问题,尽管我可以理解它无法匹配性能。 - Scott Griffiths
3
因为它无法实现mmap的功能。如果使用它,你会如何实现进程间通信,以便子进程可以通过共享内存块与父进程通信?此外,你的示例不是线程安全的,因为两个不同线程中的__getitem__调用可能会发生,第二个调用的查找紧接在第一个调用之后,导致第一个读取结果出错。 - Andrew Dalke
1
@dalke:好的,我认输了!正如我已经充分证明的那样,我对POSIX mmap并不是很了解。我只需要其中的一部分功能(没有线程等),这部分我可以相当简单地实现。关于其他方面,我会听从你的建议的 :) - Scott Griffiths
8个回答

39

来自IEEE 1003.1:

mmap()函数将建立进程地址空间与文件、共享内存对象或[TYM]类型内存对象之间的映射。

mmap()需要所有虚拟地址空间,因为这正是它所做的。

它并不真正耗尽内存并不重要 - 你不能映射比可用地址空间更多的地址。既然你把结果像访问内存一样操作,那么你怎么提议能够访问超过2^32字节的文件呢?即使mmap()没有失败,在32位地址空间中,你仍然只能读取前4GB。当然,你可以通过在文件上滑动一个32位窗口来mmap(),但除非你能优化访问模式以限制你必须访问以前窗口的次数,否则这不一定会带来任何好处。


20
POSIX mmap规范是绝对相关的。 Python的mmap模块的整个目的就是为了让你直接访问操作系统的mmap,允许像内存一样使用硬件指针访问文件数据。如果你想要更多方便,可以使用Python库中的其他IO相关模块或任何其他编程语言。否则,你需要遵守底层操作系统和CPU虚拟内存架构的限制。 - Ned Deily
2
Windows 实现了 POSIX API 调用。在 Windows 上,POSIX mmap 的作用与 Linux 上相同:将文件映射到虚拟地址空间中。 - mch
2
如果你还没有阅读过http://en.wikipedia.org/wiki/Mmap,注意一下关于Windows MapViewOfFile的注释;观察python Modules/mmapmodule.c的代码,这是在Windows上使用的方法。另外,如果您有改进Python文档的建议,欢迎前往bugs.python.org分享。 - Ned Deily
1
在Windows系统上,Python将mmap封装在MapViewOfFile win32调用之上,这个操作与*nix mmap非常相似。有关mmap在unix / windows上区别的文档中有一些注意事项。mmap是Python的“可选操作系统服务”之一,其整个目的是封装常见的操作系统功能,并因此受制于底层操作系统的限制。 - nos
1
谢谢大家,我想问题的很大一部分是Python文档没有足够明确。 - Scott Griffiths
显示剩余2条评论

18

很抱歉自问自答,但我认为我真正遇到的问题是没有意识到mmap是一个带有特定特性和限制的标准POSIX系统调用,并且Python mmap只是为了暴露它的功能。

Python文档没有提到POSIX mmap,所以如果你像我一样作为一个不太了解POSIX的Python程序员来看待它,那么地址空间问题似乎相当任意和设计不良!

感谢其他张贴者教给我mmap的真正含义。不幸的是,没有人建议比我的手工编写的类更好的大文件字符串处理方法,因此我暂时得坚持使用它。也许我有机会整理并将其作为我的模块公共接口的一部分。


9
在我看来,你手工制作的类非常适合你的需求。没有必要使用不合适的机制,只是因为它们是环境的一部分。感谢分享这次学习经验。你让我免于重新发明同样的问题集。 - CyberFonic

17

32位程序和操作系统只能寻址最多32位内存,即4GB。还有其他因素使总量更小;例如Windows保留了0.5到2GB用于硬件访问,当然你的程序也会占用一些空间。

编辑:你忽略的显而易见的事情是在任何操作系统上理解mmap的机制。它允许你将文件的一部分映射到一段内存范围内--一旦你这样做了,对该部分文件的任何访问都会发生最少的开销。这是低开销的,因为映射只进行一次,不必每次访问不同的范围就要改变。缺点是,你需要足够大的地址空间来映射你想要的部分。如果你一次性映射整个文件,你需要一个在内存映射中足够大以容纳整个文件的空洞。如果这样的空洞不存在,或者比你的整个地址空间还大,那么它就会失败。


没错,但是 mmap 实际上并不需要访问所有这些内存 - 地址空间限制只是一种实现细节。当然,如果我请求一个巨大的切片,那么可能会有内存问题,但否则没有必要保留内存。 - Scott Griffiths
"如果我请求一个巨大的切片" - 由于您在第二个参数中使用了0,因此您的“切片”是整个文件。 - Mark Ransom
是的,我正在请求整个文件,但我并不希望它被读入内存,除非我引用了它的一个切片。 - Scott Griffiths
4
一个典型的mmap实现会保留您要映射的对象的地址空间。如果无法进行映射,例如没有足够的空间来映射请求的大小,则mmap将失败。直到您访问该对象,mmap才会真正读取整个对象。但它会尝试创建地址空间映射。 - nos

9

mmap模块提供了浏览大文件的所有工具,但由于其他人提到的限制,您无法一次性映射整个文件。您可以一次映射一个很好大小的块,进行一些处理,然后取消映射并映射另一个块。mmap类的关键参数是length和offset,它们恰好做您想要的事情,允许您映射从映射文件中的字节offset开始的length字节。每当您希望读取超出映射窗口的内存部分时,您必须映射新的窗口。


6
你所忽略的是,mmap是一种内存映射函数,可以将文件映射到内存中,以便通过任何方式在请求的数据范围内进行随意访问。
你需要的更像是一种数据窗口类,它提供了一个api,允许你一次查看大型数据结构的小窗口。除了调用数据窗口自己的api之外,无法超出此窗口的边界进行访问。
这很好,但它不是一个内存映射,它是一种在更严格的api成本下提供更广泛数据范围优势的东西。

4

使用64位计算机,64位操作系统和64位Python实现,或避免使用()

mmap()需要CPU硬件支持才能处理大于几GB的大文件。

它使用CPU的内存管理单元(MMU)和中断子系统,允许将数据公开为已加载的RAM。

当访问对应于物理RAM中不存在的数据的地址时,MMU会生成一个中断,操作系统会以一种在运行时有意义的方式处理中断,因此访问代码永远不知道(也无需知道)数据不适合RAM。

这使得访问代码编写变得简单。但是,要以这种方式使用mmap(),则所有涉及的内容都需要处理64位地址。

否则,最好完全避免使用mmap()并自行进行内存管理。


2

您将长度参数设置为零,这意味着映射整个文件。在32位构建上,如果文件长度超过2GB(可能是4GB),则不可能这样做。


是的,我想映射整个文件。将其限制在几GB范围内似乎不合理,特别是因为我只需要只读访问权限。对我来说,mmap立刻尝试保留GB级别的内存似乎很疯狂! - Scott Griffiths
7
使用 mmap 并不需要物理内存,而是需要虚拟地址空间来使文件可用。 - nobody
@Andrew:那么我想我的问题是,为什么它需要所有这些虚拟地址空间?如果它是只读的,没有它也很容易使文件像字符串一样运作。也许我应该强调一下,这是关于Python mmap模块的,它不必具有与Unix mmap系统调用相同的特性和限制。 - Scott Griffiths
4
因为指向虚拟地址的指针仍然只有32位。32位最多只能表示4GB的空间。Python使用本地架构的指针。 - jmucchiello

1
你可以请求操作系统将整个文件映射到一个内存范围内。在你进行读/写操作引起页面错误之前,它不会被读取,但仍然需要确保整个范围可用于你的进程,如果该范围太大,则会出现困难。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接