内存映射文件有哪些优势?

102

我正在为一个项目研究内存映射文件,并希望得到那些曾经使用过内存映射文件或者决定不使用它们的人的想法。

特别是,我关注以下几点,按重要性排序:

  • 并发性
  • 随机访问
  • 性能
  • 易用性
  • 可移植性
4个回答

61

我认为使用内存映射文件的优点在于相较于传统的文件读取方法,可以减少需要进行数据复制的量。

如果您的应用程序可以在“原地”使用内存映射文件中的数据,那么数据可以直接使用,无需复制; 如果您使用系统调用(例如Linux的pread()),则通常需要内核将数据从自己的缓冲区复制到用户空间。这种额外的复制不仅需要时间,而且通过访问该数据的额外副本来降低了CPU高速缓存的效率。

如果实际上必须从磁盘(即物理I/O)读取数据,则操作系统仍然必须将它们读入内存,页面错误可能从性能上讲并不比系统调用更好,但是如果它们不存在(即已经在操作系统缓存中),理论上性能应该更好。

缺点是内存映射文件没有异步接口-如果尝试访问未映射的页面,则会生成页面错误,然后使线程等待I/O。


内存映射文件的明显缺点是在32位操作系统上-您很容易耗尽地址空间。


4
иҮіе°‘еңЁWindowsдёҠпјҢжӮЁеҸҜд»Ҙжҳ е°„дёҖдёӘиҫғеӨ§зҡ„mmapж–Ү件зҡ„еӨҡдёӘ32дҪҚи§ҶеӣҫвҖ”вҖ”иҝҷеҸҜиғҪжҜ”дҪҝ用常规CRTеҮҪж•°еӨ„зҗҶйқһеёёеӨ§зҡ„ж–Ү件жӣҙжңүж•ҲгҖӮ - Martin Beckett
@MarkR,你写道:“他的额外复制不仅需要时间,而且通过访问数据的这个额外副本降低了CPU缓存的有效性。”(强调是我的)。你能否解释一下内核中的额外缓冲复制如何妨碍CPU缓存的有效性? - Geek
4
访问两倍内存 = 浪费大约两倍的缓存空间。 - user253751
1
内存映射文件的一个主要缺点是错误处理。在段违规时恢复到一个明确定义的状态的后勤工作真的会影响易用性。通过纪律性地访问内存、适当的展开点和防止重新排序的屏障,这是可行的,但却破坏了传递本机指针的便利性。这实际上就像C++异常的便利性和(sig)longjmp的陷阱之间的区别。我建议在用户拔掉USB驱动器或网络共享崩溃不可接受的应用程序中避免使用内存映射文件。 - doynax

55

我使用了内存映射文件来实现用户输入时的“自动完成”功能。 我在一个单独的索引文件中存储了超过100万个产品零件编号。 文件具有一些典型的头信息,但大部分内容是根据键字段排序的固定大小记录的巨大数组。

运行时,将文件映射到内存,转换为一个C风格的结构体 数组,并进行二进制搜索以查找与用户输入匹配的零件编号。 实际从磁盘读取的只有少量内存页 - 二进制搜索期间命中的任何页。

  • 并发性 - 我曾经遇到一个实现问题,即有时会在同一进程空间中多次映射文件到内存。 因为系统有时找不到足够大的空闲虚拟内存块来映射文件,所以这是一个问题。 解决方案是只映射文件一次并 thunk 所有对它的调用。 回想起来,使用一个完整的Windows服务可能更好。
  • 随机访问 - 这个二进制搜索的确是随机访问且速度非常快。
  • 性能 - 查找非常快。 当用户输入时,弹出窗口显示匹配的产品零件编号列表,随着用户继续输入,列表会缩小。 输入时没有明显的延迟。

2
二分查找每次尝试读取页面时会变慢,操作系统是否足够智能以高效的方式处理这个问题? - jjxtra
1
我认为对于二分查找来说,使用内存映射 I/O 有点浪费,因为搜索只会访问相对较远的几个单个键,但操作系统将为每个此类请求加载 4k 页面。但另一方面,零件文件并不经常更改,所以缓存有助于弥补这一点。但严格来说,在这里传统的寻址/读取可能更好。最后,现在 100 万并不算太多。为什么不把它们全部放在 RAM 中呢? - the swine
6
@猪和PsychoDad,我的原始回答是在2008年,这个内存映射的自动完成功能的实际实现大约在2004-2005左右。使用800-1000MB的物理内存来加载整个文件对我们的用户群不是一个好的解决方案。内存映射的解决方案非常快速和高效。它很棒,我从我的初级开发人员时代就一直记得它。 :) - Brian Ensink
@BrianEnsink:好的,这很有道理。我没想到每个条目会多达1kB。那么分页方法当然更有效率。不错 :) - the swine
消耗物理内存来加载整个文件并不是一个好的解决方案。我不确定你为什么关心物理内存(将文件加载到byteBuffer中是否需要更多的物理内存?这是操作系统的细节)。mmap将占用所有虚拟内存空间,而按照你需要的特定字节读取文件则不会。"the swine"提到"但严格来说,我认为传统的寻址/读取在这里更好。"这是一个有趣的想法,因为这样你可以在需要时直接读取所需的字节。 - Ben Butterworth
我认为Brian的性能(增加的速度,减少的延迟)优势是通过使用mmap引起的,但这也意味着他实际上消耗了800-1000MB的虚拟内存而非物理内存。这是内存与延迟之间的权衡。另一种解决方案(正常读取文件)比mmap更节省内存,但速度较慢。我并不完全理解文件系统到100%,但我认为在2013年存在混淆,这里https://unix.stackexchange.com/questions/367982/are-files-opened-by-processes-loaded-into-ram可以提供帮助。 - Ben Butterworth

22

内存映射文件可以用来替代读/写访问,或支持并发共享。当您将其用于一个机制时,您也会得到另一个机制。

与在文件中进行lseek、读取和写入不同,您可以将文件映射到内存中,并简单地访问您希望它们存在的位置的位。

这非常方便,并且根据虚拟内存接口的不同可能会提高性能。性能的提升可能是因为操作系统现在可以处理这个以前的“文件I/O”以及所有其他程序化内存访问,可以(理论上)利用页面算法等等,这些算法已经用于支持虚拟内存的剩余部分。但是,这取决于您底层虚拟内存系统的质量。我听到的轶事说,Solaris和*BSD虚拟内存系统可能比Linux的VM系统表现出更好的性能提升-但我没有实证数据来支持这一点。你的情况可能有所不同。

当您考虑多个进程通过映射内存使用相同的“文件”时,并发性就成了一个问题。在读/写模型中,如果两个进程都写入文件的同一个区域,则可以非常肯定地认为一个进程的数据将到达文件,覆盖另一个进程的数据。您将获得其中之一,而不是一些奇怪的混合。我必须承认我不确定这是否是任何标准规定的行为,但这是您可以非常依赖的事情。(这实际上是一个很好的后续问题!)

相比之下,在映射世界中,想象两个进程都在“写入”。它们通过进行“内存存储”来这样做,导致操作系统将数据分页到磁盘-最终。但是,在此期间,可以预期会发生重叠的写入。

这里是一个例子。假设我有两个进程都在偏移量1024处写入8个字节。进程1正在写入'11111111',进程2正在写入'22222222'。如果它们使用文件I/O,则可以想象,在操作系统的深处,存在一个缓冲区,其中一个充满1,另一个充满2,两者都指向磁盘上的同一位置。其中一个将首先到达那里,另一个将第二个到达那里。在这种情况下,第二个将获胜。然而,如果我使用基于内存映射文件的方法,进程1将会进行4个字节的内存存储,然后是另外4个字节的内存存储(假设这是最大的内存存储大小)。进程2也将执行同样的操作。根据进程运行的时间,您可以预期看到以下任何一个:

11111111
22222222
11112222
22221111
这个问题的解决方法是使用显式互斥,这在任何情况下都是一个好主意。无论如何,在读/写文件I/O情况下,您都有点依赖操作系统来做“正确的事情”。互斥的经典原语是互斥量。对于内存映射文件,我建议您查看使用(例如)pthread_mutex_init()可用的内存映射互斥量。
注意一个问题:当您使用映射文件时,有一种诱惑将指向文件中数据的指针嵌入文件本身(考虑存储在映射文件中的链接列表)。您不想这样做,因为文件可能在不同时间或不同进程中在不同的绝对地址处映射。而是使用映射文件中的偏移量。

2

并发可能是一个问题。 随机访问更容易。 性能良好至极佳。 易用性不太好。 可移植性 - 不太理想。

我很久以前在Sun系统上使用过它们,这些是我的想法。


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