当应用程序保持文件锁定时,使用ReplaceFile替代方法

Ian*_*dby 5 file-io winapi atomicity

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

FooEdit还使用仅FILE_SHARE_READ的共享模式来打开文件的句柄,以便其他进程可以打开文件,但在FooEdit可以打开文件的同时不能写入文件。

“显然”,在执行ReplaceFile操作时必须短暂关闭此句柄,这允许在FooEdit重新建立它的FILE_SHARE_READ锁句柄之前,另一个进程可以打开具有写访问权的文件的竞争。

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

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

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

(此问题似乎相关,但是没有答案:在Windows上以事务方式写入文件更改。)


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

我总是从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;
}
Run Code Online (Sandbox Code Playgroud)

感谢Hans Passant,他在答案中提供了一些有价值的澄清思想,现在已将其删除。这是我在遵循他的建议时发现的:

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

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

Dav*_*nan 0

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

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

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