POSIX 环境至少提供两种访问文件的方式。有标准的系统调用open()
, read()
, write()
, 和朋友,但也有mmap()
用于将文件映射到虚拟内存的选项。
什么时候最好使用其中一种?包括两个接口的优点是什么?
mmap
如果您有多个进程以只读方式从同一个文件访问数据,那就太好了,这在我编写的那种服务器系统中很常见。 mmap
允许所有这些进程共享相同的物理内存页面,从而节省大量内存。
mmap
还允许操作系统优化分页操作。例如,考虑两个程序;程序A
将文件读1MB
入创建的缓冲区malloc
,程序 Bmmaps
将 1MB 文件读入内存。如果操作系统必须将A
's 的部分内存换出,它必须先将缓冲区的内容写入交换,然后才能重用内存。在B
' 的情况下,任何未修改mmap
的 'd 页面都可以立即重用,因为操作系统知道如何从它们所在的现有文件中恢复它们mmap
。(操作系统可以通过最初将可写页面标记mmap
为只读并捕获seg faults来检测哪些页面未修改,类似于Copy on Write策略)。
mmap
对于进程间通信也很有用。您可以mmap
在需要通信的进程中将文件作为读/写,然后在mmap'd
区域中使用同步原语(这就是MAP_HASSEMAPHORE
标志的用途)。
mmap
如果您需要在 32 位机器上处理非常大的文件,可能会有些尴尬。这是因为mmap
必须在进程的地址空间中找到一个连续的地址块,该地址空间大到足以容纳被映射文件的整个范围。如果您的地址空间变得碎片化,这可能会成为一个问题,您可能有 2 GB 的可用地址空间,但没有一个单独的范围可以适合 1 GB 的文件映射。在这种情况下,您可能必须将文件映射成比您想要的更小的块。
作为读/写替代品的另一个潜在尴尬mmap
是您必须在页面大小的偏移量上开始映射。如果您只想在偏移量处获取一些数据,X
则需要修复该偏移量以使其与mmap
.
我发现 mmap() 不是优势的一个领域是读取小文件(低于 16K)时。与仅执行一次 read() 系统调用相比,读取整个文件的页面错误开销非常高。这是因为内核有时可以在您的时间片中完全满足读取,这意味着您的代码不会切换。由于页面错误,似乎更有可能安排另一个程序,从而使文件操作具有更高的延迟。
mmap
当您对大文件进行随机访问时具有优势。另一个优点是您可以通过内存操作(memcpy、指针算法)访问它,而无需担心缓冲。当结构大于缓冲区时,使用缓冲区时,普通 I/O 有时会非常困难。要处理的代码通常很难正确处理,mmap 通常更容易。这就是说,使用mmap
. 正如人们已经提到mmap
的那样,设置成本非常高,因此仅对给定尺寸(因机器而异)才值得使用。
对于对文件的纯顺序访问,它也不总是更好的解决方案,尽管适当的调用madvise
可以缓解问题。
您必须小心您的架构(SPARC、安腾)的对齐限制,通过读/写 IO,缓冲区通常会正确对齐,并且在取消引用强制转换的指针时不会陷入陷阱。
您还必须注意不要在地图之外访问。如果您在地图上使用字符串函数,并且文件末尾不包含 \0,则很容易发生这种情况。大多数情况下,当您的文件大小不是页面大小的倍数时,它将起作用,因为最后一页填充为 0(映射区域的大小始终为页面大小的倍数)。
除了其他不错的答案,引用谷歌专家罗伯特·洛夫 (Robert Love) 所写的Linux 系统编程的话:
的优点
mmap( )
与标准和系统调用相比,通过操作文件
mmap( )
有一些优势。其中有:read( )
write( )
读取和写入内存映射文件避免了使用
read( )
orwrite( )
系统调用时发生的无关复制,其中数据必须复制到用户空间缓冲区和从用户空间缓冲区复制。除了任何潜在的页面错误之外,读取和写入内存映射文件不会产生任何系统调用或上下文切换开销。就像访问内存一样简单。
当多个进程将同一个对象映射到内存中时,数据在所有进程之间共享。只读和共享可写映射完全共享;私有可写映射共享它们的尚未 COW(写时复制)页面。
寻找映射涉及微不足道的指针操作。不需要
lseek( )
系统调用。由于这些原因,
mmap( )
它是许多应用程序的明智选择。的缺点
mmap( )
使用时有几点需要注意
mmap( )
:
内存映射始终是整数页大小。因此,后备文件的大小与整数页数之间的差异被“浪费”为松弛空间。对于小文件,可能会浪费很大比例的映射。例如,对于 4 KB 页面,7 字节映射会浪费 4,089 字节。
内存映射必须适合进程的地址空间。对于 32 位地址空间,大量不同大小的映射会导致地址空间碎片化,从而难以找到大的空闲连续区域。当然,这个问题在 64 位地址空间中不太明显。
在内核中创建和维护内存映射和相关数据结构是有开销的。这种开销通常可以通过消除上一节中提到的双重副本来消除,特别是对于较大且经常访问的文件。
由于这些原因,
mmap( )
当映射文件很大时(因此任何浪费的空间只占总映射的一小部分),或者当映射文件的总大小可以被页面大小整除时(因此没有浪费的空间)。
与传统 IO 相比,内存映射具有巨大的速度优势。它让操作系统在内存映射文件中的页面被触摸时从源文件中读取数据。这通过创建故障页面来工作,操作系统检测到这些页面,然后操作系统自动从文件中加载相应的数据。
这与分页机制的工作方式相同,通常通过读取系统页面边界和大小(通常为 4K)上的数据来针对高速 I/O 进行优化——大多数文件系统缓存都针对该大小进行了优化。
尚未列出的一个优点是将mmap()
只读映射保持为干净页面的能力。如果在进程的地址空间中分配一个缓冲区,然后使用read()
从文件中填充缓冲区,则与该缓冲区对应的内存页面现在是脏的,因为它们已被写入。
内核不能从 RAM 中删除脏页。如果有交换空间,则可以将它们调出进行交换。但这很昂贵,并且在某些系统上,例如只有闪存的小型嵌入式设备,根本没有交换。在这种情况下,缓冲区将被卡在 RAM 中,直到进程退出,或者可能以madvise()
.
非写入mmap()
页面是干净的。如果内核需要 RAM,它可以简单地删除它们并使用页面所在的 RAM。如果具有映射的进程再次访问它,则会导致页面错误,内核从它们最初来自的文件重新加载页面. 与他们最初的填充方式相同。
这不需要多个使用映射文件的进程即可成为优势。