【问题标题】:Is an atomic file rename (with overwrite) possible on Windows?Windows 上是否可以进行原子文件重命名(带覆盖)?
【发布时间】:2010-09-15 02:36:59
【问题描述】:

在 POSIX 系统上,rename(2) 提供原子重命名操作,包括在目标文件存在且权限允许的情况下覆盖目标文件。

有没有办法在 Windows 上获得相同的语义?我知道 Vista 和 Server 2008 上的 MoveFileTransacted(),但我需要它来支持 Win2k 及更高版本。

这里的关键词是原子...解决方案不能以任何方式失败,导致操作处于不一致状态。

我看到很多人说这在win32上是不可能的,但我问你,真的吗?

如果可能,请提供可靠的引用。

【问题讨论】:

  • @Adam Davis - 如果您可以控制阅读器程序以及编写器,则可以这样解决。阅读器执行 io.Directory("FileDone_*.dat") 并选择最高的 # 代替 *。 Write 创建名为“FileWriting.dat”的文件并将其重命名为“FileDone_002.dat”..003、004 等。这不仅解决了非原子删除/重命名的问题,而且单个重命名是原子的,并且,如果旧文件保持打开状态,仍然可以更新。如果不是每次操作都重新打开,则阅读器可以基于计时器监视新文件。读者可以清理旧文件。

标签: windows winapi posix


【解决方案1】:

从 Windows 10 1607 开始,NTFS 确实支持原子取代重命名操作。为此,请调用 NtSetInformationFile(..., FileRenameInformationEx, ...) 并指定 FILE_RENAME_POSIX_SEMANTICS 标志。

或者等效地在 Win32 中调用 SetFileInformationByHandle(..., FileRenameInfoEx, ...) 并指定 FILE_RENAME_FLAG_POSIX_SEMANTICS 标志。

【讨论】:

  • DeleteFile 现在使用 POSIX 删除和 ReplaceFile 现在使用 POSIX 重命名(但仍分两步),MoveFileEx 与 MOVEFILE_REPLACE_EXISTING 是否仍执行旧式重命名?
  • 我认为对于 SetFileInformationByHandle,您的意思是 FILE_RENAME_INFO.ReplaceIfExists 标志,而不是 FILE_RENAME_FLAG_POSIX_SEMANTICS 标志,对吧?
【解决方案2】:

std::rename 并以C++17 std::filesystem::rename 开头。 如果目的地与std::rename 存在,会发生什么尚未明确:

如果 new_filename 存在,则行为是实现定义的。

POSIX rename,然而,是 required to replace existing files atomically

这个 rename() 函数对于普通文件来说是等价于定义的 根据 ISO C 标准。它包含在此处将该定义扩展为 包括对目录的操作并指定新的行为 参数命名一个已经存在的文件。该规范 要求函数的动作是原子的。

谢天谢地,std::filesystem::rename 要求它的行为就像 POSIX:

将 old_p 标识的文件系统对象移动或重命名为 new_p 为 如果通过 POSIX 重命名

但是,当我尝试调试时,似乎 VS2019(截至 2020 年 3 月)实现的 std::filesystem::rename 只是调用了 MoveFileEx,在某些情况下这不是原子的。 因此,可能在其实现中的所有错误都得到修复后,我们将看到可移植的原子std::filesystem::rename

【讨论】:

    【解决方案3】:

    MSDN 文档避免明确说明哪些 API 是原子的,哪些不是,但 Niall Douglas 在他的 Cppcon 2015 talk 中指出唯一的原子函数是

    SetFileInformationByHandle

    FILE_RENAME_INFO.ReplaceIfExists 设置为 true。它从 Windows Vista / 2008 Server 开始可用。

    Niall 是高度复杂的LLFIO library 的作者,并且是文件系统竞争条件方面的专家,所以我相信,如果你正在编写一个原子性至关重要的算法,最好安全而不是抱歉,并使用建议的函数,即使ReplaceFile 的描述中没有任何内容表明它不是原子的。

    【讨论】:

    • 取代重命名实际上是 only 类型的重命名,不能保证在 NTFS 上是原子的。它可能是非原子的原因是 NTFS 必须删除目标的所有分配,并记录删除分配。如果被取代的目标非常大,那么所有已删除的分配将无法容纳在单个 NTFS 事务中,因此 NTFS 将其拆分为多个事务。如果机器崩溃,您最终可能会处于源和目标都仍然存在但目标已被部分截断(沿事务边界)的状态。
    • 取代重命名只是您已经提到的 ReplaceIfExists,如果使用 FileRenameInformation,或者 FILE_RENAME_REPLACE_IF_EXISTS,如果使用 FileRenameInformationEx,或者 MOVEFILE_REPLACE_EXISTING,如果使用 MoveFileEx 等。它们都是相同的文件系统操作。当目标确实存在时,据说它已被取代。如果您愿意,可以使用覆盖或替换的条款。
    • 您首先要知道的是文件系统操作可以是原子的,而不是 API 本身。文件系统操作是否是原子的取决于您所谈论的文件系统以及操作。大多数情况下,我一直假设您在谈论 NTFS 作为文件系统。在 FAT 上,没有任何东西是原子的,因此没有更高级别的文件相关 API 在 FAT 上是原子的。在 NTFS 上,如果 API 将自身限制为单个文件系统操作(为什么 ReplaceFile 不是原子的),并且该文件系统操作是原子的(为什么 MoveFileEx 不是原子的),那么它可以被认为是原子的。
    • 以MoveFileEx为例,它很复杂,因为根据它的调用方式,它可能最终会做1)简单的重命名;或 2) 替代重命名(MOVEFILE_REPLACE_EXISTING 事物);或 3) 复制并删除。第一种情况实际上在 NTFS 上是原子的。第二种情况是原子的 99.99999% 的时间,唯一的例外是当被取代的目标是巨大的,正如我之前描述的那样。第三种情况绝对不是原子的,因为“复制”是一长串操作。因此,您必须先了解具体场景,然后才能尝试回答它是否是原子的。
    • Linux 并没有根本的不同。例如,几乎没有文件系统操作在 ext2 文件系统上被认为是原子的,因为(像 FAT)该文件系统不支持事务。因此,几乎没有与 Linux 文件相关的 API 本身是原子的。
    【解决方案4】:

    相当多的答案,但不是我所期望的...我有这样的理解(也许是错误的),如果正确的星星对齐,使用标志,MoveFile 可能是原子的,并且源文件系统与目标文件系统相同。否则,操作将退回到 [Copy->Delete] 文件。

    鉴于此;我还了解到,MoveFile——当它是原子的——只是设置文件信息,这也可以在这里完成:setfileinfobyhandle

    有人做了一个名为“Racing the Filesystem”的演讲,对此进行了更深入的讨论。 (大约 2/3 下降了他们谈论原子重命名)

    【讨论】:

      【解决方案5】:

      Win32 不保证原子文件元数据操作。我会提供引用,但没有 - 没有书面或书面保证这一事实同样重要。

      您将不得不编写自己的例程来支持这一点。很不幸,但你不能指望 win32 提供这种级别的服务——它根本不是为它设计的。

      【讨论】:

      • 我觉得这很难相信。这意味着即使我们正在处理诸如 NTFS 之类的可靠系统,断电也很容易损坏文件系统。
      • @mafutrct 请记住,问题不在于破坏文件系统,而是在于确保重命名成功完成或根本不发生。文件系统不会损坏,但文件名可能不会保持原始或最终状态。 NTFS 是一个日志文件系统,因此它不会(很容易)被损坏,但根据文件重命名的复杂性或操作顺序,它可能不会保持原始或所需的最终状态。跨度>
      • 说的有道理,但也很吓人。最终得到一个既不是原始文件名也不是最终文件名的文件名几乎是灾难的根源。特别是因为 (iirc) POSIX 标准已经需要原子元文件操作。
      • @mafutrct 我怀疑这不是简单的文件重命名的问题,但正如操作所暗示的那样,有更复杂的重命名操作,例如将文件重命名为已经存在的文件的名称.如果您有LOGFILELOGBACKUP 并且定期要将日志文件移动到备份并启动新的日志文件,则可以将日志文件重命名为 logbackup。操作系统必须删除 logbackup,然后重命名 logfile - 有可能是删除发生了,而不是重命名发生了,然后你丢失了两个 logfile,这不是一个在软件中解决的小问题。
      • @AdamDavis 仍然很遗憾。原子覆盖是一个关键特性。在文件系统上,这是知道您拥有旧版本或新命名 blob 的唯一方法。
      【解决方案6】:

      在 Windows Vista 和 Windows Server 2008 中添加了原子移动函数 - MoveFileTransacted()

      很遗憾,这对旧版本的 Windows 没有帮助。

      Interesting article here on MSDN.

      【讨论】:

      • 隐藏在 cmets 中:这不适用于网络共享
      • @sorin:问题要求等效于 POSIX 调用,它在网络共享上也不是原子的。
      • 但是,问题中已经提到了这个解决方案(以及它对某些 Windows 版本的限制),所以写它作为答案没有用。
      • 实际上,POSIC 调用在 NFS 上是原子的。
      • 现在好像要弃用了。
      【解决方案7】:

      在 Win32 中查看 ReplaceFile() (http://research.microsoft.com/pubs/64525/tr-2006-45.pdf)

      【讨论】:

      • 如果您阅读msdn.microsoft.com/en-us/library/aa365512(VS.85).aspx,您会发现ReplaceFile 是一个复杂的合并操作,没有任何迹象表明它是原子操作。
      • 那篇 MS 研究论文的相关段落:“在 UNIX 下,rename() 保证原子地覆盖文件的旧版本。在 Windows 下,ReplaceFile() 调用用于原子地替换一个与另一个文件。”
      • msdn.microsoft.com/en-us/library/windows/desktop/… 说 ReplaceFile 可以原子地使用:“许多处理“类文档”数据的应用程序倾向于将整个文档加载到内存中,对其进行操作,然后将其写回以保存更改。这里需要的原子性是更改完全应用或根本不应用,因为不一致的状态会导致文件损坏。一种常见的方法是将文档写入新文件,然后将原始文件替换为新的。一种方法是使用 ReplaceFile API。"
      • 请特别注意为 ReplaceFile 记录的各种返回代码,它们都对应于操作的不同程度的部分(即非原子)完成。
      • 微软实习生在这里。我遇到了这个问题,所以我问了一个在 NTFS 工作的人。数据被移动的部分是原子的,所以当文件属性被修改时它可以被中断,而数据本身被移动的部分是原子的。
      【解决方案8】:

      您仍然可以在 Windows 上调用 rename(),但我想如果不知道您正在使用的文件系统就无法做出您想要的保证 - 例如,如果您使用的是 FAT,则无法保证。

      但是,您可以使用 MoveFileEx 并使用 MOVEFILE_REPLACE_EXISTING 和 MOVEFILE_WRITE_THROUGH 选项。后者在 MSDN 中有这样的描述:

      设置此值可确保 作为复制和删除执行的移动 操作被刷新到磁盘之前 函数返回。冲洗发生 在复制操作结束时。

      我知道这不一定与重命名操作相同,但我认为这可能是您将获得的最佳保证 - 如果它针对文件移动执行此操作,则它应该用于更简单的重命名。

      【讨论】:

      • 据我所知,如果目标存在并且在数据复制步骤期间发生 I/O 错误,则此“原始”目标将丢失,因此根据您的要求,MoveFileEx 不是原子的。这就是后来添加 MoveFileTransacted 的原因。
      • MoveFileEx 应该不错。它有一个名为 MOVEFILE_COPY_ALLOWED 的标志,上面写着:“如果要将文件移动到不同的卷,则该函数通过使用 CopyFile 和 DeleteFile 函数来模拟移动。”所以只是不要传递这个标志,你应该有一些相当于 POSIX 重命名的东西,是吗?
      • 如果 windows 下已经存在新文件,重命名会失败。抛开原子性不谈,windows 版本甚至在语义上与 Unix 版本不兼容。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-11-23
      • 2019-07-09
      • 1970-01-01
      • 2011-04-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多