应用程序保持文件锁定时的 ReplaceFile 替代方案
ReplaceFile alternative when application keeps file locked
编辑器 FooEdit(我们这样称呼它)在保存时使用 ReplaceFile() 以确保保存操作有效地原子化,并且如果出现任何问题,则保留光盘上的原始文件。 (ReplaceFile() 的另一个重要好处是文件标识的连续性 - 创建日期和其他元数据。)
FooEdit 还以 FILE_SHARE_READ 的共享模式保持打开文件句柄,以便其他进程可以打开文件但不能写入文件,而 FooEdit 打开文件进行写入.
"Obviously",在执行 ReplaceFile 操作时必须短暂关闭此句柄,这会导致竞争,在 FooEdit 重新建立文件之前,另一个进程可能会使用写访问权限打开文件 FILE_SHARE_READ 锁柄.
(如果 FooEdit 在调用 ReplaceFile() 之前没有关闭其 FILE_SHARE_READ 句柄,则 ReplaceFile() 将因共享冲突而失败。)
我想知道解决这场比赛的最简单方法是什么。这些选项似乎要么找到另一种方法来锁定与 ReplaceFile() 兼容的文件(我不明白这是怎么可能的),要么复制 ReplaceFile() 的所有行为,但使用现有的文件句柄来访问目标文件而不是路径。我对如何从用户代码自动执行 ReplaceFile() 的所有操作有点困惑(无论如何重新实现 ReplaceFile() 似乎不是一个好主意)。
这一定是一个常见问题,所以可能有一个明显的解决方案我错过了。
(这个问题似乎相关但没有答案:。)
这是一个最小的可验证示例,展示了我正在努力实现的目标(更新 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()。
I'd like to know what is the simplest way to resolve this race.
没有解决这场比赛的简单方法。它是文件系统的固有部分,不是事务性的。 MS 在 Vista 中引入了一个事务文件 API,但现在强烈建议开发人员不要使用它,因为它可能会在未来的版本中被删除。
我有一些使用 ReplaceFile
的经验,但我认为它造成的麻烦超过了它的价值。我的回忆是,在保留元数据的同时,创建了一个新文件。其结果是保存在桌面上的文件的行为非常烦人。因为此类文件保留了它们的位置,所以创建新文件会导致使用默认位置。所以你会保存一个文件,你会把它拖到桌面上你想保留它的地方,然后当你再次保存文件时,它会移回默认位置。
其实我觉得可能有一个不涉及交易的解决方案(虽然据我所知交易仍然可用)。我自己还没有尝试过,但我认为在 NTFS 上应该可以创建一个新的文件流(使用一个长的随机名称来确保没有冲突),写入你的数据,然后将该流重命名为你的流其实想写给.
FILE_RENAME_INFORMATION
建议这应该是可能的,因为它谈到重命名数据流。
但是,这只适用于 NTFS。对于其他文件系统,我认为您别无选择。
编辑器 FooEdit(我们这样称呼它)在保存时使用 ReplaceFile() 以确保保存操作有效地原子化,并且如果出现任何问题,则保留光盘上的原始文件。 (ReplaceFile() 的另一个重要好处是文件标识的连续性 - 创建日期和其他元数据。)
FooEdit 还以 FILE_SHARE_READ 的共享模式保持打开文件句柄,以便其他进程可以打开文件但不能写入文件,而 FooEdit 打开文件进行写入.
"Obviously",在执行 ReplaceFile 操作时必须短暂关闭此句柄,这会导致竞争,在 FooEdit 重新建立文件之前,另一个进程可能会使用写访问权限打开文件 FILE_SHARE_READ 锁柄.
(如果 FooEdit 在调用 ReplaceFile() 之前没有关闭其 FILE_SHARE_READ 句柄,则 ReplaceFile() 将因共享冲突而失败。)
我想知道解决这场比赛的最简单方法是什么。这些选项似乎要么找到另一种方法来锁定与 ReplaceFile() 兼容的文件(我不明白这是怎么可能的),要么复制 ReplaceFile() 的所有行为,但使用现有的文件句柄来访问目标文件而不是路径。我对如何从用户代码自动执行 ReplaceFile() 的所有操作有点困惑(无论如何重新实现 ReplaceFile() 似乎不是一个好主意)。
这一定是一个常见问题,所以可能有一个明显的解决方案我错过了。
(这个问题似乎相关但没有答案:
这是一个最小的可验证示例,展示了我正在努力实现的目标(更新 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()。
I'd like to know what is the simplest way to resolve this race.
没有解决这场比赛的简单方法。它是文件系统的固有部分,不是事务性的。 MS 在 Vista 中引入了一个事务文件 API,但现在强烈建议开发人员不要使用它,因为它可能会在未来的版本中被删除。
我有一些使用 ReplaceFile
的经验,但我认为它造成的麻烦超过了它的价值。我的回忆是,在保留元数据的同时,创建了一个新文件。其结果是保存在桌面上的文件的行为非常烦人。因为此类文件保留了它们的位置,所以创建新文件会导致使用默认位置。所以你会保存一个文件,你会把它拖到桌面上你想保留它的地方,然后当你再次保存文件时,它会移回默认位置。
其实我觉得可能有一个不涉及交易的解决方案(虽然据我所知交易仍然可用)。我自己还没有尝试过,但我认为在 NTFS 上应该可以创建一个新的文件流(使用一个长的随机名称来确保没有冲突),写入你的数据,然后将该流重命名为你的流其实想写给.
FILE_RENAME_INFORMATION
建议这应该是可能的,因为它谈到重命名数据流。
但是,这只适用于 NTFS。对于其他文件系统,我认为您别无选择。