何时应该使用mmap进行文件访问?

350

POSIX环境至少提供两种文件访问方式。一种是标准的系统调用open()read()write()等,另一种是使用mmap()将文件映射到虚拟内存中。

何时最好使用其中一种方法而不是另一种方法?它们各自的优点有哪些,值得包括这两个接口呢?


25
请参阅mmap()与读取块以及其中一个答案中引用的Linus Torvalds的这篇文章 - MvG
6个回答

361
mmap非常适用于在同一文件中以只读方式访问数据的多个进程,这在我编写的服务器系统中很常见。mmap允许所有这些进程共享相同的物理内存页,节省了大量内存。
mmap还允许操作系统优化分页操作。例如,考虑两个程序:程序A将一个1MB文件读入使用malloc创建的缓冲区,而程序B将这个1MB文件映射到内存中。如果操作系统需要将A的一部分内存交换出去,它必须先将缓冲区的内容写入交换空间,然后才能重新使用这块内存。而对于B来说,任何未修改的mmap页面都可以立即重用,因为操作系统知道如何从原始文件中恢复它们(这是因为操作系统可以通过最初将可写的mmap页面标记为只读,并捕获段错误(seg faults),类似于写时复制(Copy on Write)策略,来检测哪些页面是未修改的)。

mmap 对于进程间通信也非常有用。您可以在需要通信的进程中将文件mmap为读/写,并在mmap'd区域中使用同步原语(这就是MAP_HASSEMAPHORE标志的作用)。

mmap 的一个不方便之处是在 32 位机器上处理非常大的文件时。这是因为 mmap 必须在您的进程地址空间中找到一个足够大的连续地址块,以容纳整个文件范围。如果您的地址空间变得碎片化,可能会出现问题,即使您有 2 GB 的地址空间可用,但没有一个单独的范围可以容纳 1 GB 的文件映射。在这种情况下,您可能需要将文件分成较小的块进行映射,以使其适应。

另一个使用`mmap`替代读取/写入的潜在尴尬之处是,您必须从页面大小的偏移量开始映射。如果您只想在偏移量`X`处获取一些数据,您需要调整该偏移量以使其与`mmap`兼容。
最后,读取/写入是您唯一可以使用的方式来处理某些类型的文件。`mmap`不能用于像pipesttys这样的东西。

11
在文件不断增大的情况下,你能否使用mmap()函数?或者说在分配mmap()内存/文件时,大小是否被固定下来了? - Jonathan Leffler
30
当你调用mmap时,需要指定一个大小。因此,如果你想进行类似于tail操作的操作,那么它并不是非常适合。 - Don Neufeld
5
据我所知,MAP_HASSEMAPHORE 是 BSD 系统特有的。 - Patrick Schlüter
7
@JonathanLeffler 您可以在不断增长的文件上使用 mmap(),但是当文件达到您最初分配的空间限制时,您必须再次使用新的大小调用 mmap()。LevelDB 的 PosixMmapFile 给出了一个很好的示例。但从1.15开始它停止使用 mmap。您可以从Github获取旧版本。 - baotiao
4
如果需要对一个文件进行多次处理,mmap也可能会很有用:分配虚拟内存页的成本只需支付一次。 - Jib
显示剩余2条评论

79

mmap()不具有优势的一个领域是读取小文件(小于16K)。与只执行单个read()系统调用相比,通过页面错误读取整个文件的开销非常高。这是因为内核有时可以在您的时间片中完全满足读取,这意味着您的代码不会被切换。使用页面错误,似乎更有可能安排另一个程序,使文件操作具有更高的延迟。


5
我可以确认。对于小文件,使用malloc分配一块内存并将1个read读入其中更快。这样可以使用相同的处理内存映射的代码来处理malloc分配的内存。 - Patrick Schlüter
41
虽然如此,您对其的理由并不正确。调度程序与差异完全无关。差异来自于对页面表的写访问,这是内核的全局结构,保存进程持有哪些内存页面及其访问权限。这个操作可能非常昂贵(可能会使缓存失效,可以丢弃TLB,该表是全局的,因此必须受到并发访问的保护等)。您需要一定大小的映射,以使“读”访问的开销高于虚拟内存操作的开销。 - Patrick Schlüter
2
@PatrickSchlüter 好的,我明白mmap()在开始时有开销,涉及修改页表。假设我们将文件的16K映射到内存中。对于4K的页面大小,mmap必须更新页表中的4个条目。但是使用read将16K复制到缓冲区中也需要更新4个页表条目,更不用说它还需要将16K复制到用户地址空间中。所以您能详细说明一下对页表的操作的差异,以及为什么对于mmap来说更加昂贵吗? - flow2k
mmap 重新加载一些页面,因此读取较小的文件不一定会出现页面错误。 - FloweyTF

52

mmap具有在大文件上进行随机访问的优势。另一个优点是你可以通过内存操作(memcpy、指针算术)访问它,而不需要打扰缓冲。当使用缓冲区时,普通I/O在处理比缓冲区更大的结构时有时会非常困难。处理这种情况的代码通常很难正确编写,但使用mmap通常要容易得多。但是,在使用mmap时也存在一些陷阱。

正如人们已经提到的,mmap的设置成本相当高,因此仅在给定大小(因机器而异)时值得使用。

对于纯顺序访问文件,它也并不总是更好的解决方案,但适当调用madvise可以缓解问题。

你必须注意你的体系结构(SPARC、Itanium)的对齐限制,在读/写IO中,缓冲区通常被正确对齐,并且在引用强制转换指针时不会陷入错误。

你还必须小心不要访问地图之外的内容。如果在映射上使用字符串函数,并且你的文件末尾没有包含\0,那么这种情况很容易发生。当你的文件大小不是页大小的倍数时,它会大多数时间工作正常(映射区域总是页大小的倍数)。


49

除了其他很好的答案外,来自Google专家Robert Love所写的Linux系统编程一书的引言:

mmap( )的优点

通过mmap( )操纵文件比使用标准的read( )write( )系统调用有很多优点,其中包括:

  • 从内存映射文件中读取和写入避免了使用read( )write( )系统调用时出现的额外复制,该数据必须复制到和从用户空间缓冲区复制。

  • 除了任何可能的页面故障之外,从内存映射文件中读取和写入不会产生任何系统调用或上下文切换开销。它就像访问内存一样简单。

  • 当多个进程将相同的对象映射到内存中时,数据在所有进程之间共享。只读和共享可写映射被完全共享;私有可写映射共享其未进行COW(写时复制)的页面。

  • 在映射中寻找涉及微不足道的指针操作。无需lseek( )系统调用。

因为这些原因,对于许多应用程序来说,mmap( )是一个明智的选择。

mmap( )的缺点

在使用mmap( )时需要记住以下几点:

  • 内存映射始终是整数页大小。因此,备份文件大小与整数页面之间的差异为"浪费"作为松弛空间。对于小文件,映射的大部分可能被浪费。例如,使用4KB页面,7字节映射浪费了4089字节。

  • 内存映射必须适合进程的地址空间。具有32位地址空间的情况下,大量各种大小的映射可能导致地址空间的碎片化,使其难以找到大的自由连续区域。当然,这个问题在64位地址空间下要少得多。

  • 创建和维护内核内存映射和相关数据结构存在开销。这个开销通常可以通过消除前一节中提到的双重复制来消除,特别是对于较大和经常访问的文件。

因为这些原因,mmap( )的好处最大地体现在映射文件较大(因此任何浪费的空间都是总映射的一小部分)或映射文件的总大小可以被页面大小整除(因此没有浪费空间)的情况下。


4
对于小文件,映射的很大一部分可能会被浪费。而使用mmap则不会浪费任何空间。内核会将文件映射到一个页面中,保存在文件缓存中,无论应用程序是否使用mmap。使用mmap,至少可以避免使用本地缓冲区来复制数据。实际上,mmap只是访问文件缓存。 - Patrick Schlüter
2
文件I/O也存在映射开销,因为内核在任何情况下都会将文件映射到其文件缓存中。mmap开销仅在向请求进程授予映射页面的访问权限时发生。页面构建和磁盘读取在所有情况下都会完成。 - Patrick Schlüter

15

内存映射相对于传统的IO具有巨大的速度优势。它允许操作系统在内存映射文件的页面被触摸时从源文件中读取数据。这通过创建故障页面来实现,操作系统检测到后自动从文件中加载相应的数据。

这与分页机制的工作方式相同,并且通常针对高速I/O进行优化,通过在系统页面边界和大小(通常为4K)上读取数据 - 大多数文件系统缓存都是针对此大小进行优化的。


18
请注意,mmap() 并不总是比 read() 更快。在顺序读取时,mmap() 不会给你带来明显的优势 - 这基于实证和理论证据。如果您不相信,请自己编写测试。 - Tim Cooper
1
我可以为我们项目中的数字提供一种短语数据库的文本索引。该索引有数千兆字节大,键保存在三叉树中。该索引仍在不断增长,同时进行读取访问,映射部分之外的访问通过pread进行。在Solaris 9 Sparc(V890)上,pread访问速度比从mmap复制的速度慢2到3倍。但是你说得对,顺序访问并不一定更快。 - Patrick Schlüter
21
有一点小问题需要指出。它不是像分页机制那样工作,而是分页机制本身。映射文件就是将内存区域分配给文件,而不是匿名交换文件。 - Patrick Schlüter

4
一个尚未列出的优点是 mmap() 函数能够将只读映射保持为 clean 页面。如果在进程的地址空间中分配一个缓冲区,然后使用read()从文件中填充该缓冲区,那么对应于该缓冲区的内存页面现在是dirty的,因为它们已经被写入。 Dirty 页面无法被内核从 RAM 中删除。如果有交换空间,则可以将其换出到交换空间中。但这是很耗费性能的,在某些系统(例如只有闪存的小型嵌入式设备)上根本没有交换空间。在这种情况下,缓冲区将一直占用 RAM,直到进程退出,或者可能通过madvise()返回给系统。
未被写入的 mmap() 页面是clean的。如果内核需要 RAM,它可以简单地丢弃这些页面并使用这些页面所在的 RAM 。如果拥有该映射的进程再次访问它,会导致页错误,内核会从最初的文件中重新加载页面。就像第一次填充它们一样。
这不需要多个进程使用映射文件才能获得优势。

内核不能通过先将“脏”的mmap'd页面的内容写入底层文件来丢弃它吗? - Jeremy Friesner
3
当使用read()时,最终放置数据的页面与它们可能来自的文件没有关系。因此,除了交换空间外,它们不能被写出。如果一个文件被mmap()映射,并且该映射是可写的(而不是只读的),并且被写入,则取决于映射是MAP_SHARED还是MAP_PRIVATE。共享映射可以/必须写入文件,但私有映射则不能。 - TrentP
这是被接受的答案第二段所描述的同样优势。 - Nemo

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