内存映射文件用于读取输入文件,安全性如何?

13

将输入文件映射到内存中,然后直接从映射的内存页解析数据,是从文件中读取数据的一种便捷且高效的方法。

但是,除非您能确保没有其他进程对映射文件进行写操作,否则这种做法似乎基本上是不安全的,因为即使在私有只读映射中的数据也可能会因为底层文件被另一个进程写入而发生更改。(例如 POSIX 未指定“建立MAP_PRIVATE映射之后对底层对象所做的修改是否通过MAP_PRIVATE映射可见”)

如果你想让你的代码在映射文件发生外部更改的情况下变得安全,那么你必须只通过volatile指针访问映射内存,然后极其小心地读取和验证输入,这似乎对于许多用例来说是不切实际的。

这个分析正确吗?关于内存映射API的文档通常只是顺带提到这个问题,如果有的话,所以我想知道是否遗漏了什么。


在您使用文件时,另一个进程可能会修改它。内存映射是使用文件的众多方法之一 - 它并不比其他任何方法更不安全。 - Casey
1
将文件内容使用普通的读取文件API复制到内存缓冲区中,即使文件正在同时被修改,也是安全的。相比之下,如果通过普通指针访问内存映射文件,并且外部进程同时修改该文件,就会导致未定义的行为,因为这超出了C(++)内存模型的限制。一般来说,如果您或编译器期望的内存在您操作过程中发生了变化,将会导致糟糕的结果。您在某个时刻验证的输入可能在下一个时刻变得无效。 - Stephan Tolksdorf
你误解了MAP_PRIVATE的目的。它并不意味着“给我一个私有副本”,而是意味着“由我进行的修改对我私有”。它具有与访问文件的任何其他方法相同的并发问题。 - Anya Shenanigans
@Petesh,我在哪里写或暗示MAP_PRIVATE意味着“给我一个私有副本”?实际上,我引用了规范中说明相反的部分。不过,如果有一个选项确实可以确保一旦访问了映射页面,其他进程就不能更改它们,那就太好了。 - Stephan Tolksdorf
@StephanTolksdorf,这似乎是你问题的核心。而且,“确保映射的页面在访问后不会被其他进程更改”的选项是通过写入页面来实现的。mmap只是相对于读写的一种便利方式。 - Anya Shenanigans
在Windows中,您通常会独占地打开文件,或者使用FILE_SHARE_READ,这样其他进程在您使用文件时就无法修改它。 - Harry Johnston
2个回答

3

这并不是一个问题。

没错,当你映射了一个文件时,另一个进程可能会修改它,而且你有可能看到那些修改。事实上,几乎所有操作系统都拥有统一的虚拟内存系统,所以除非请求未缓冲写入,否则没有途径可以不经过缓存高速缓存地写入,也没有途径可以让某个持有映射的人看不到变更。这并不是什么坏事。如果你看不到变化就更加糟糕了。因为文件在被映射时,几乎就成为了你地址空间的一部分,所以你看到文件变化是很合理的。

如果你使用常规 I/O(如read),当你正在读取文件时还是有人能够修改它。换句话说,“将文件内容复制到内存缓冲区在存在修改时并不总是安全的”。从read的角度来看是“安全”的,因为read不会崩溃,但是它并不能保证你的数据是一致的。如果你不使用readv,你就完全没有原子性保障(即使使用了readv,你也无法保证内存中的内容与磁盘上的一致,也无法保证在两次调用readv之间不发生变化)。有人可能会在两个read操作之间修改文件,或者甚至在你正在读取文件时修改它。这不仅仅是没有正式保障,但“可能仍然有效”,相反,在 Linux 下,写入显然是不原子的,即使是偶然的。

好消息:
通常情况下,进程不会随意打开一个文件并开始写入。当这种情况发生时,通常要么是属于该进程的一个众所周知的文件(如日志文件),要么是你明确告诉该进程要写入的文件(如在文本编辑器中保存),要么是该进程创建了一个新文件(如编译器创建对象文件),或者该进程仅附加到现有文件(如DB记录和当然,日志文件)。或者,进程可以原子地将一个文件替换为另一个文件(或取消链接它)。

在每种情况下,整个可怕的问题都归结为“没有问题”,因为要么你非常清楚会发生什么(所以这是你的责任),要么它可以毫不影响地无缝运行。

如果你真的不喜欢在映射文件时另一个进程可能会写入它,那么在Windows下创建文件句柄时可以简单地省略FILE_SHARE_WRITE。在POSIX中,需要使用fcntl对描述符进行强制锁定,但这并不是每个系统都支持或100%可靠的(例如,在Linux下)。


我不关心读取或写入的原子性。当你使用read将文件内容复制到内存缓冲区时,可以在之后验证读取的输入,然后确保缓冲区中的内容保持有效。当一个输入文件被映射到内存中后,通常被视为常量缓冲区,并且通过普通指针直接访问和解析。如果这个被认为是常量的内存被外部进程更改,就会破坏编译器和开发者的期望,这可能导致未定义的行为。 - Stephan Tolksdorf
没错,如果这确实是一个问题,你仍然可以将memcpy复制到匿名映射(或堆分配块)。当然,这样你就失去了内存映射的主要优势之一:数据不再“神奇地”和“免费地”可用。不过我不会费心去复制。事实上,你通常知道何时修改文件(或代表你修改程序),这不会在随机文件上随机发生。 - Damon
@Damon 这是一个源文件。人们会在意想不到的时候打开它们,而且当你最不希望的时候,人们会将它们写回去。 - James Kanze
@JamesKanze:你怎么知道这个问题是关于源文件的?我错过了什么吗? - Damon
@Damon谈到了解析。所以这是他程序的一个来源。(但事实上,人们可以解析许多东西...我以前曾经解析过日志文件。) - James Kanze

1
理论上,如果有人在你读取文件时修改了它,那么你可能真的会遇到麻烦。但实际上,你只是在读取字符,没有指针或其他可能会导致问题的内容。实际上...严格来说,我认为这仍然是未定义行为,但我认为你不必担心。除非修改非常小,否则你将得到许多编译器错误,但这就是全部。
唯一可能引起问题的情况是文件被缩短。我不确定当你读取超出文件末尾时会发生什么。
最后:系统不会任意打开和修改文件。这是一个源文件;只会有某个白痴程序员这样做,他应该自食其果。在任何情况下,你的未定义行为都不会破坏系统或其他人的文件。
请注意,大多数编辑器都在私有副本上工作;当它们写回时,会通过重命名原始文件并创建新文件来完成。在Unix下,一旦您将文件映射到mmap,唯一重要的是inode号。当编辑器重命名或删除文件时,您仍然保留您的副本。修改后的文件将获得一个新的inode。唯一需要担心的是如果有人打开文件进行更新,然后继续修改它。除了在文本文件末尾附加其他数据之外,没有多少程序会对此进行操作。
因此,虽然从正式意义上讲存在一定风险,但我认为您不必担心。 (如果您真的很偏执,可以在mmaped时关闭写入权限。如果确实有敌对特工想要获取您的信息,他可以立即将其打开。)

我认为OP并不是在谈论阅读源文件本身,而是程序可能在运行时读取的任何文件。因此,你关于编译器错误的观点似乎不太合适。 - Marc Claesen
@MarcClaesen提到了解析。编译器错误可能有点严重,但是每当您解析文本时,都需要以某种方式处理输入中的错误。 - James Kanze

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