应用程序保持文件锁定时的ReplaceFile替代

时间:2015-09-30 07:33:36

标签: winapi file-io atomicity

编辑器FooEdit(让我们称之为)在保存时使用ReplaceFile()以确保保存操作实际上是原子的,并且如果出现任何错误,则保留光盘上的原始文件。 (ReplaceFile()的另一个重要好处是文件标识的连续性 - 创建日期和其他元数据。)

FooEdit还使用FILE_SHARE_READ的共享模式保持打开文件句柄,这样其他进程就可以打开文件,但在FooEdit打开文件时无法写入文件。

“显然”,当ReplaceFile操作发生时,此句柄必须短暂关闭,这允许在FooEdit重新建立FILE_SHARE_READ锁定句柄之前,另一个进程可能使用写访问权限打开文件的竞赛。

(如果FooEdit在调用ReplaceFile()之前没有关闭其FILE_SHARE_READ句柄,则ReplaceFile()会因共享冲突而失败。)

我想知道解决这场比赛的最简单方法是什么。选项似乎要么找到另一种方法来锁定与ReplaceFile()兼容的文件(我看不出这是怎么可能的)或者复制ReplaceFile()的所有行为,但是使用现有的文件句柄来访问目标文件而不是路径。我有点担心如何从用户代码原子地执行ReplaceFile()的所有操作(并且重新实现ReplaceFile()无论如何都是一个坏主意。)

这一定是一个常见问题,所以可能有一个我错过的明显解决方案。

(这个问题似乎有关,但没有答案:Transactionally write a file change on Windows。)

这是一个最小的可验证示例,显示了我想要实现的目标(更新时间为13:18 30/9/2015 UTC)。您必须提供三个文件名作为命令行参数,所有这些都在同一个卷上。第一个必须已经存在。

我总是从ReplaceFile()获得共享冲突。

#include <Windows.h>
#include <stdio.h>
#include <assert.h>
int main(int argc, char *argv[])
{
  HANDLE lock;
  HANDLE temp;
  DWORD  bytes;

  if (argc != 4)
  {
    puts("First argument is the project file. Second argument is the temporary file.");
    puts("The third argument is the backup file.");
  }

  /* Open and lock the project file to make sure no one else can modify it */
  lock = CreateFile(argv[1], GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0);
  assert(lock != INVALID_HANDLE_VALUE);

  /* Save to the temporary file. */
  temp = CreateFile(argv[2], GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, CREATE_ALWAYS, 0, 0);
  assert(temp != INVALID_HANDLE_VALUE);
  WriteFile(temp, "test", 4, &bytes, NULL);
  /* Keep temp open so that another process can't modify the file. */

  if (!ReplaceFile(argv[1], argv[2], argv[3], 0, NULL, NULL))
  {
    if (GetLastError() == ERROR_SHARING_VIOLATION)
      puts("Sharing violation as I expected");
    else
      puts("Something went wrong");
  }
  else
    puts("ReplaceFile worked - not what I expected");

  /* If it worked the file referenced by temp would now be called argv[1]. */
  CloseHandle(lock);
  lock = temp;

  return EXIT_SUCCESS;
}

感谢Hans Passant,他在一个现在删除的答案中提供了一些有价值的澄清想法。这是我在跟进他的建议时发现的:

似乎ReplaceFile()允许 lpReplacedFileName 打开FILE_SHARE_READ | FILE_SHARE_DELETE,但 lpReplacementFileName 不能。 (而且这种行为似乎并不依赖于是否提供了 lpBackupFileName 。)因此,即使其他进程不允许FILE_SHARE_WRITE,也完全有可能替换另一个进程已打开的文件,这是汉斯的观点。

但是FooEdit正试图确保没有其他进程可以首先使用GENERIC_WRITE 打开文件。为了确保在FooEdit中没有其他进程可以使用GENERIC_WRITE打开替换文件的竞争,似乎FooEdit必须持续保持连续的FILE_SHARE_READ | FILE_SHARE_DELETE处理 lpReplacementFileName ,然后排除使用ReplaceFile()。

2 个答案:

答案 0 :(得分:1)

实际上,我认为可能存在不涉及事务的解决方案(尽管据我所知,事务仍然可用)。我自己还没有尝试过,但是我认为在NTFS上应该可以创建一个新的文件流(使用长的随机名称以确保没有冲突),写入数据,然后将该流重命名为您的流真的想写信。

FILE_RENAME_INFORMATION建议这样做应该可行,因为它谈到了重命名数据流。

但是,这仅适用于NTFS。对于其他文件系统,我认为您别无选择。

答案 1 :(得分:0)

  

我想知道解决这场比赛的最简单方法是什么。

没有简单的方法来解决这场比赛。它是文件系统的固有部分,不是事务性的。 MS在Vista中引入了一个事务性文件API,但现在强烈建议开发人员不要使用它,因为它可能会在将来的版本中被删除。

我有ReplaceFile的一些经验,但我认为它造成了比它值得更多的麻烦。我的回忆是,在保存元数据的同时,创建了一个新文件。这样做的结果是保存在桌面上的文件非常烦人。由于此类文件的位置已保留,因此创建新文件会导致使用默认位置。因此,您要保存文件,将其拖动到桌面上您想要保留的位置,然后当您再次保存文件时,它会移回默认位置。