POSIX环境至少提供两种文件访问方式。一种是标准的系统调用open()
、read()
、write()
等,另一种是使用mmap()
将文件映射到虚拟内存中。
何时最好使用其中一种方法而不是另一种方法?它们各自的优点有哪些,值得包括这两个接口呢?
mmap
非常适用于在同一文件中以只读方式访问数据的多个进程,这在我编写的服务器系统中很常见。mmap
允许所有这些进程共享相同的物理内存页,节省了大量内存。mmap
对于进程间通信也非常有用。您可以在需要通信的进程中将文件mmap
为读/写,并在mmap'd
区域中使用同步原语(这就是MAP_HASSEMAPHORE
标志的作用)。
mmap
的一个不方便之处是在 32 位机器上处理非常大的文件时。这是因为 mmap
必须在您的进程地址空间中找到一个足够大的连续地址块,以容纳整个文件范围。如果您的地址空间变得碎片化,可能会出现问题,即使您有 2 GB 的地址空间可用,但没有一个单独的范围可以容纳 1 GB 的文件映射。在这种情况下,您可能需要将文件分成较小的块进行映射,以使其适应。
MAP_HASSEMAPHORE
是 BSD 系统特有的。 - Patrick Schlütermmap()不具有优势的一个领域是读取小文件(小于16K)。与只执行单个read()系统调用相比,通过页面错误读取整个文件的开销非常高。这是因为内核有时可以在您的时间片中完全满足读取,这意味着您的代码不会被切换。使用页面错误,似乎更有可能安排另一个程序,使文件操作具有更高的延迟。
malloc
分配一块内存并将1个read
读入其中更快。这样可以使用相同的处理内存映射的代码来处理malloc
分配的内存。 - Patrick Schlütermmap
必须更新页表中的4个条目。但是使用read
将16K复制到缓冲区中也需要更新4个页表条目,更不用说它还需要将16K复制到用户地址空间中。所以您能详细说明一下对页表的操作的差异,以及为什么对于mmap
来说更加昂贵吗? - flow2kmmap
具有在大文件上进行随机访问的优势。另一个优点是你可以通过内存操作(memcpy、指针算术)访问它,而不需要打扰缓冲。当使用缓冲区时,普通I/O在处理比缓冲区更大的结构时有时会非常困难。处理这种情况的代码通常很难正确编写,但使用mmap通常要容易得多。但是,在使用mmap
时也存在一些陷阱。
正如人们已经提到的,mmap
的设置成本相当高,因此仅在给定大小(因机器而异)时值得使用。
对于纯顺序访问文件,它也并不总是更好的解决方案,但适当调用madvise
可以缓解问题。
你必须注意你的体系结构(SPARC、Itanium)的对齐限制,在读/写IO中,缓冲区通常被正确对齐,并且在引用强制转换指针时不会陷入错误。
你还必须小心不要访问地图之外的内容。如果在映射上使用字符串函数,并且你的文件末尾没有包含\0,那么这种情况很容易发生。当你的文件大小不是页大小的倍数时,它会大多数时间工作正常(映射区域总是页大小的倍数)。
除了其他很好的答案外,来自Google专家Robert Love所写的Linux系统编程一书的引言:
mmap( )
的优点通过mmap( )
操纵文件比使用标准的read( )
和write( )
系统调用有很多优点,其中包括:
从内存映射文件中读取和写入避免了使用read( )
或write( )
系统调用时出现的额外复制,该数据必须复制到和从用户空间缓冲区复制。
除了任何可能的页面故障之外,从内存映射文件中读取和写入不会产生任何系统调用或上下文切换开销。它就像访问内存一样简单。
当多个进程将相同的对象映射到内存中时,数据在所有进程之间共享。只读和共享可写映射被完全共享;私有可写映射共享其未进行COW(写时复制)的页面。
在映射中寻找涉及微不足道的指针操作。无需lseek( )
系统调用。
因为这些原因,对于许多应用程序来说,mmap( )
是一个明智的选择。
mmap( )
的缺点在使用mmap( )
时需要记住以下几点:
内存映射始终是整数页大小。因此,备份文件大小与整数页面之间的差异为"浪费"作为松弛空间。对于小文件,映射的大部分可能被浪费。例如,使用4KB页面,7字节映射浪费了4089字节。
内存映射必须适合进程的地址空间。具有32位地址空间的情况下,大量各种大小的映射可能导致地址空间的碎片化,使其难以找到大的自由连续区域。当然,这个问题在64位地址空间下要少得多。
创建和维护内核内存映射和相关数据结构存在开销。这个开销通常可以通过消除前一节中提到的双重复制来消除,特别是对于较大和经常访问的文件。
因为这些原因,mmap( )
的好处最大地体现在映射文件较大(因此任何浪费的空间都是总映射的一小部分)或映射文件的总大小可以被页面大小整除(因此没有浪费空间)的情况下。
内存映射相对于传统的IO具有巨大的速度优势。它允许操作系统在内存映射文件的页面被触摸时从源文件中读取数据。这通过创建故障页面来实现,操作系统检测到后自动从文件中加载相应的数据。
这与分页机制的工作方式相同,并且通常针对高速I/O进行优化,通过在系统页面边界和大小(通常为4K)上读取数据 - 大多数文件系统缓存都是针对此大小进行优化的。
pread
进行。在Solaris 9 Sparc(V890)上,pread
访问速度比从mmap复制的速度慢2到3倍。但是你说得对,顺序访问并不一定更快。 - Patrick Schlütermmap()
函数能够将只读映射保持为 clean 页面。如果在进程的地址空间中分配一个缓冲区,然后使用read()
从文件中填充该缓冲区,那么对应于该缓冲区的内存页面现在是dirty的,因为它们已经被写入。
Dirty
页面无法被内核从 RAM 中删除。如果有交换空间,则可以将其换出到交换空间中。但这是很耗费性能的,在某些系统(例如只有闪存的小型嵌入式设备)上根本没有交换空间。在这种情况下,缓冲区将一直占用 RAM,直到进程退出,或者可能通过madvise()
返回给系统。mmap()
页面是clean
的。如果内核需要 RAM,它可以简单地丢弃这些页面并使用这些页面所在的 RAM 。如果拥有该映射的进程再次访问它,会导致页错误,内核会从最初的文件中重新加载页面。就像第一次填充它们一样。read()
时,最终放置数据的页面与它们可能来自的文件没有关系。因此,除了交换空间外,它们不能被写出。如果一个文件被mmap()
映射,并且该映射是可写的(而不是只读的),并且被写入,则取决于映射是MAP_SHARED
还是MAP_PRIVATE
。共享映射可以/必须写入文件,但私有映射则不能。 - TrentP