什么时候我应该使用mmap进行文件访问?

POSIX环境提供了至少两种访问文件的方法。有标准的系统调用open()read()write()等,但也有使用mmap()将文件映射到虚拟内存的选项。

什么时候使用一种比另一种更可取?它们各自的优势是什么?

123972 次浏览

如果你有多个进程以只读方式访问同一个文件中的数据,mmap是很好的,这在我写的服务器系统中很常见。mmap允许所有这些进程共享相同的物理内存页,节省大量内存。

mmap还允许操作系统优化分页操作。例如,考虑两个程序;程序A将一个1MB文件读入由malloc创建的缓冲区,程序B将1MB的文件mmaps读入内存。如果操作系统必须将A的部分内存交换出来,它必须在重用内存之前将缓冲区的内容写入交换。在B的情况下,任何未修改的__abc0d页面都可以立即重用,因为操作系统知道如何从__abc0d所在的现有文件中恢复它们。(操作系统可以通过最初将可写的__abc0d页标记为只读并捕获A0来检测哪些页面未被修改,类似于A1策略)。

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

mmap可能很尴尬的一个地方是如果你需要在32位机器上处理非常大的文件。这是因为mmap必须在进程的地址空间中找到一个连续的地址块,该地址块足够大,以适应被映射文件的整个范围。如果您的地址空间变得碎片化,这可能会成为一个问题,您可能有2 GB的空闲地址空间,但没有一个单独的范围可以适合1 GB的文件映射。在这种情况下,您可能必须将文件映射为比您希望的更小的块。

mmap作为读/写的替代品的另一个潜在的尴尬是,你必须在页面大小的偏移量上开始映射。如果你只是想获得一些偏移量X的数据,你需要修正这个偏移量,使它与mmap兼容。

最后,读/写是你可以处理某些类型文件的唯一方式。mmap不能用于管道tty

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

这与分页机制的工作方式相同,通常通过读取系统页面边界和大小(通常是4K)上的数据来优化高速I/O——大多数文件系统缓存都优化到4K大小。

我发现mmap()在读取小文件(小于16K)时没有优势。与仅执行单个read()系统调用相比,读取整个文件的页面故障开销非常高。这是因为内核有时可以完全满足您的时间片中的读取,这意味着您的代码不会切换。如果出现页面错误,则更有可能安排另一个程序,从而使文件操作具有更高的延迟。

mmap在随机访问大文件时具有优势。另一个优点是可以通过内存操作(memcpy,指针算术)访问它,而不需要缓冲。当结构比缓冲区大时,使用缓冲区时,正常的I/O有时会相当困难。处理这个问题的代码通常很难得到正确的处理,mmap通常比较容易。也就是说,在使用mmap时存在某些陷阱。 正如人们已经提到的,mmap的设置成本相当高,所以它只值得用于给定的大小(因机器而异)。< / p >

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

您必须小心架构的对齐限制(SPARC, itanium),对于读/写IO,缓冲区通常是正确对齐的,并且在取消引用强制转换的指针时不会捕获。

您还必须注意不要访问映射之外的地方。如果您在映射上使用字符串函数,并且文件末尾不包含\0,则很容易发生这种情况。当您的文件大小不是页面大小的倍数时,它将在大多数情况下工作,因为最后一页被填充为0(映射区域的大小始终是页面大小的倍数)。

除了其他不错的答案,谷歌的专家Robert Love引用Linux系统编程的一段话:

mmap( )的优点

通过mmap( )操作文件比 标准的read( )write( )系统调用。其中包括:

  • 读取和写入内存映射文件可以避免 使用read( )write( )系统时发生的无关复制 调用,其中数据必须从用户空间缓冲区复制到或从用户空间缓冲区复制 除了任何潜在的页错误外,读取和写入内存映射文件不会引起任何系统调用或上下文切换 开销。

  • 当多个进程将同一个对象映射到内存中时,数据被所有进程共享。只读和共享可写 映射是完全共享的;私有可写映射具有

  • 围绕映射进行搜索涉及到简单的指针操作。不需要lseek( )系统调用。

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

mmap( )的缺点

在使用mmap( )时,有几点需要记住:

  • 内存映射在大小上总是一个整数页数。因此,备份文件的大小和 整数页数被“浪费”为松弛空间。对于小文件,a 很大一部分映射可能会被浪费。例如,用 4kb的页面,一个7字节的映射浪费4089字节

  • 内存映射必须适合进程的地址空间。在32位地址空间中,有大量不同大小的映射 会导致地址空间碎片化,使之难以实现 寻找大型免费连续区域。当然,这个问题很大

  • 在内核内部创建和维护内存映射和相关的数据结构会有开销。这个开销是 一般由消除双副本所述 前面的部分,特别针对较大且经常访问的部分 李文件。< / p > < / >
由于这些原因,mmap( )的好处得到了最大程度的实现 当映射文件很大时(因此任何浪费的空间都很小) 总映射的百分比),或者当映射的总大小 文件被页面大小均匀整除(因此没有浪费 空间). < / p >

尚未列出的一个优点是mmap()能够将只读映射保持为清洁页。如果在进程的地址空间中分配了一个缓冲区,然后使用read()从文件中填充缓冲区,那么与该缓冲区对应的内存页现在是,因为它们已经被写入。

脏页不能被内核从RAM中删除。如果有交换空间,则可以将它们换出交换。但这是昂贵的,在一些系统上,例如只有闪存的小型嵌入式设备,根本没有交换。在这种情况下,缓冲区将被卡在RAM中,直到进程退出,或者可能将它与__abc0一起返回。

未写入mmap()的页面是干净的。如果内核需要RAM,它可以简单地丢弃它们并使用页面所在的RAM。如果拥有映射的进程再次访问它,就会导致页面错误,内核会从它们最初产生的文件中重新加载页面。和他们最初居住的地方一样。

这并不需要一个以上的进程使用映射文件。