Winapi 当应用程序保持文件锁定时替换文件

Winapi 当应用程序保持文件锁定时替换文件,winapi,file-io,atomicity,Winapi,File Io,Atomicity,编辑器FooEdit(我们称之为)在保存时使用(),以确保保存操作是有效的原子操作,并且如果出现任何错误,则保留光盘上的原始文件。(ReplaceFile()的另一个重要优点是文件标识(创建日期和其他元数据)的连续性。) FooEdit还以文件共享读取的共享模式保持打开文件句柄,以便其他进程可以打开文件,但不能在FooEdit打开文件进行写入时对其进行写入 “显然”,在执行ReplaceFile操作时,必须短暂关闭此句柄,这允许在FooEdit重新建立其文件共享读取锁定句柄之前,另一个进程可能以

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

FooEdit还以文件共享读取的共享模式保持打开文件句柄,以便其他进程可以打开文件,但不能在FooEdit打开文件进行写入时对其进行写入

“显然”,在执行ReplaceFile操作时,必须短暂关闭此句柄,这允许在FooEdit重新建立其文件共享读取锁定句柄之前,另一个进程可能以写访问权限打开该文件

(如果FooEdit在调用ReplaceFile()之前未关闭其文件\u共享\u读取句柄,则ReplaceFile()将因共享冲突而失败。)

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

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

(这个问题似乎相关,但没有答案:)


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

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

#包括
#包括
#包括
int main(int argc,char*argv[])
{
把手锁;
手柄温度;
双字字节;
如果(argc!=4)
{
puts(“第一个参数是项目文件,第二个参数是临时文件。”);
puts(“第三个参数是备份文件。”);
}
/*打开并锁定项目文件,以确保其他人无法修改它*/
lock=CreateFile(argv[1],GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ | FILE_SHARE_DELETE,NULL,OPEN_EXISTING,0,0);
断言(lock!=无效的\u句柄\u值);
/*保存到临时文件*/
temp=CreateFile(argv[2],GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ | FILE_SHARE_DELETE,NULL,CREATE_ALWAYS,0,0);
断言(temp!=无效的\u句柄\u值);
WriteFile(临时,“测试”,4字节和字节,NULL);
/*保持temp打开,以便其他进程无法修改该文件*/
如果(!ReplaceFile(argv[1],argv[2],argv[3],0,NULL,NULL))
{
如果(GetLastError()==错误\u共享\u冲突)
puts(“如我所料共享违规”);
其他的
看跌期权(“出了问题”);
}
其他的
puts(“ReplaceFile工作-不是我期望的”);
/*如果工作正常,temp引用的文件现在将被称为argv[1]*/
关闭手柄(锁);
锁定=温度;
返回退出成功;
}

感谢汉斯·帕桑,他在一份现已删除的答复中提供了一些有价值的澄清想法。以下是我在跟进他的建议时发现的:

似乎ReplaceFile()允许lpReplacedFileName打开文件共享读取文件共享删除,但lpReplacementFileName不能。(这种行为似乎并不取决于是否提供了lpBackupFileName。)因此,完全可以替换另一个进程打开的文件,即使该进程不允许文件共享写入,这是Hans的观点

但是FooEdit试图确保没有其他进程可以首先使用泛型_WRITE打开文件。为了确保在FooEdit中,没有其他进程可以使用GENERIC_WRITE打开替换文件的争用,FooEdit似乎必须连续保留lpReplacementFileName的文件共享|读取|文件共享|删除句柄,从而禁止使用ReplaceFile()

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

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


我有过使用
ReplaceFile
的一些经验,但我认为这会带来更多的麻烦。我记得在保留元数据的同时,创建了一个新文件。这样做的结果是,对于保存在桌面上的文件来说,这种行为非常恼人。因为这样的文件保留了它们的位置,所以创建新文件会导致使用默认位置。因此,您可以保存一个文件,将它拖到桌面上您想要保存它的位置,然后再次保存文件时,它会移回默认位置。

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

建议这应该是可能的,因为它讨论了重命名数据流


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

传统的安全保存包括使用临时名称保存到新文件;写入成功后,删除旧文件并重命名新文件。这就是ReplaceFile的用途,但最重要的是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;
}