C++/Win32:如何等待挂起的删除操作完成

39

已解决:

问题描述:

我们的软件主要是用来解释一种专有脚本语言的引擎。该脚本语言具有创建文件、处理文件和删除文件的功能,这些都是单独的操作,并且在这些操作之间没有保持文件句柄处于打开状态。

(即在文件创建期间,会创建一个句柄,用于写入,然后关闭。在文件处理部分,另一个文件句柄打开文件,从中读取内容,当达到文件末尾时关闭。最后,删除使用的是 ::DeleteFile 函数,只需要传入文件名,不需要传入任何文件句柄)。

最近,我们发现一个特定的宏(脚本)有时无法在随机的后续时间创建文件(即在“创建、处理、删除”的前一百次迭代中成功,但当它再次尝试创建第一百零一次时,Windows 会报“访问被拒绝”的错误)。

更深入地研究了这个问题后,我编写了一个非常简单的程序来循环执行以下操作:

while (true) {
    HANDLE hFile = CreateFileA(pszFilename, FILE_ALL_ACCESS, FILE_SHARE_READ,
                               NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
        return OpenFailed;

    const DWORD dwWrite = strlen(pszFilename);
    DWORD dwWritten;

    if (!WriteFile(hFile, pszFilename, dwWrite, &dwWritten, NULL) || dwWritten != dwWrite)
        return WriteFailed;

    if (!CloseHandle(hFile))
        return CloseFailed;

    if (!DeleteFileA(pszFilename))
        return DeleteFailed;
}

从代码可以看出,这是直接使用Win32 API并且十分简单。我创建一个文件,写入内容,关闭句柄,删除文件,如此反复......

但是在某个时候,在CreateFile()调用期间会出现访问被拒绝(5)的错误。通过查看sysinternal的ProcessMonitor,我可以看到根本问题是当我试图再次创建文件时,文件处于挂起删除状态。

问题:

  • 是否有一种方法来等待删除完成?
  • 是否有一种方法来检测文件是否处于挂起删除状态?

我们已经尝试了第一种选项,即简单地在HFILE上等待WaitForSingleObject()。 但是在WaitForSingleObject执行之前,HFILE总是被关闭,因此WaitForSingleObject始终返回WAIT_FAILED。 显然,等待已关闭的句柄是无效的。

我可以等待文件所在文件夹的更改通知。但是,这看起来像是一种极其消耗资源的解决方法,只会在偶尔出现的问题上使用(例如:在我的Windows 7 x64 E6600 PC上测试时,通常在第12000+次迭代中失败--在其他机器上,可能发生在第7或15或56次迭代中,也可能永远不会发生)。

我无法确定有任何CreateFile()参数可以显式地允许这种情况。 无论CreateFile具有什么参数,当文件处于挂起删除状态时,它都不能打开文件以进行任何访问操作。

由于我在Windows XP和x64 Windows 7上都看到了这种行为,因此我非常确定这是Microsoft的核心NTFS行为“按照意图”。 因此,我需要一种解决方案,允许操作系统在继续之前完成删除,最好不会不必要地占用CPU周期,并且不会像观察此文件的文件夹那样产生极大的开销(如果可能)。

1 是的,这个循环返回失败或关闭失败时会泄漏,但由于这是一个简单的控制台测试应用程序,应用程序本身退出,而Windows保证所有句柄在进程完成时由操作系统关闭。 所以这里没有泄漏。

bool DeleteFileNowA(const char * pszFilename)
{
    // Determine the path in which to store the temp filename
    char szPath[MAX_PATH];
    strcpy(szPath, pszFilename);
    PathRemoveFileSpecA(szPath);

    // Generate a guaranteed to be unique temporary filename to house the pending delete
    char szTempName[MAX_PATH];
    if (!GetTempFileNameA(szPath, ".xX", 0, szTempName))
        return false;

    // Move the real file to the dummy filename
    if (!MoveFileExA(pszFilename, szTempName, MOVEFILE_REPLACE_EXISTING))
        return false;

    // Queue the deletion (the OS will delete it when all handles (ours or other processes) close)
    if (!DeleteFileA(szTempName))
        return false;

    return true;
}

3
你确定所有句柄都已关闭吗?因为你写的内容正是在MSDN上的描述:"如果你调用CreateFile打开一个文件,而该文件正处于先前调用DeleteFile后等待删除的状态,则函数会失败。操作系统会延迟文件删除直到文件的所有句柄都关闭。GetLastError返回ERROR_ACCESS_DENIED。" - Kra
2
我创建了一个控制台应用程序,它在循环中执行上述代码。它最终会失败...通常在第12000-15000次迭代左右。所以,除非您发现上述8行代码泄漏了某个句柄,否则我认为这是相当不可能的(至少对于这个测试应用程序)。 - Mordachai
3
@Mordachai: 我已经看到过这个并发表了我的评论,但被删除了。 - sbi
4
由于Windows搜索服务的问题,我遇到了这个情况。当我创建一个目录时,它会锁定该目录并对其进行索引,同时我试图删除它,但会收到错误5的提示。使用启用索引器的循环创建和删除目录/文件,可以轻松重现此问题。 - paulm
4
在我的情况下,是 MsMpEng 阻止了这个文件。我已经发现了微软的确认,索引/ 杀毒软件可以阻止该文件。因此,我现在习惯于在重试循环中将其删除,或者最好创建新的临时文件而不是重用单个名称,希望单个删除最终会生效。现在,我遇到了一个类似的问题。在 gcc p.c > p.exe && p && gcc p2.c > p.exe && p ... 中,p.exe 刚刚终止,被阻止了,但是 filemon 中没有任何东西,只有 p 进程访问该文件。 - Val
显示剩余8条评论
13个回答

22

在Windows中,还有其他的进程想要使用该文件。其中,搜索索引器是一个明显的候选者。或者是病毒扫描器。它们会以完全共享的方式打开文件,包括FILE_SHARE_DELETE,以便其他进程不会因为它们打开文件而受到重大影响。

通常情况下,这通常效果很好,除非您以高速率创建/写入/删除。删除操作将成功,但是直到最后一个句柄关闭之前,文件才不能从文件系统中消失。例如,由搜索索引器持有的句柄。任何试图打开正在等待删除的文件的程序都会遭遇错误5。

否则,在多任务操作系统中,这是一个通用问题,您无法知道其他进程可能要使用哪些文件。首先请查看您的使用模式是否异常。一种解决方法是捕获错误,休眠一段时间后再次尝试。或使用SHFileOperation()将文件移至回收站。


是的,避免这种模式可能是明智的选择!哈哈 - 不幸的是,我扮演的角色类似于一位语言开发人员,他有使用该语言的程序员。我可以告诉他们某些模式最好避免使用,但最终,他们会编写自己的软件(脚本),而我必须尽力应对他们有限的关注“最佳实践”的能力。 - Mordachai
试图隐藏真正的操作系统行为,使脚本程序员看不到会是一个严重的错误。改进你的 API,提供一种让这些低下的灵魂自己处理问题的方式。错误代码、异常或其他方法都可以。避免上帝态度,否则你只会创造出每个人都认为很糟糕的东西。因为他们在处理脚本时遇到了问题,没有诊断结果而上帝又不回复他们的电子邮件。 - Hans Passant
1
在C/C++/Java或其他主要编程语言的层面上,我认为你是正确的。我的直觉也完全同意你的看法。然而,在“简化脚本语言”的层面上,我们通常遵循的信条是“如果这是脚本编写者可以合理完成的事情,那么引擎应该让它发生”。涉及生成给定文件的子组件、处理并删除文件的另一个组件,然后将这两个块循环执行以实现某些更大的收益的循环,可以说是“合理的”。因此,在这种情况下,我们可能会将这种复杂性隐藏起来,不让脚本编写者感知到。 - Mordachai
8
公平地说,这是一个特定于Windows而非通用操作系统的问题。其他操作系统通过允许其等效的DeleteFile删除名称而不删除文件来轻松处理此问题,因此索引程序不会阻止文件被删除。UNIX系统就是这种行为的一个例子。该问题存在是因为Win32(而非NTFS)需要对每个打开的文件都有一个名称。 - Remember Monica
1
这样的服务(如搜索索引器)是否也会尝试读取临时文件? - foresightyj
额外的解释从CppCon 2015这个演讲的大约7:30开始:https://www.youtube.com/watch?v=uhRWMGBjlO8 - Adrian McCarthy

20

首先将要删除的文件重命名,然后再删除它。

使用GetTempFileName()获取一个唯一的名称,然后使用MoveFile()来重命名文件。接着删除已重命名的文件。如果实际的删除操作是异步的,并且可能会与创建同一文件发生冲突(正如您的测试所显示的那样),这样做就能解决问题。

当然,如果您的分析正确,文件操作确实是有点异步的话,这可能会引入一种问题,即在重命名完成之前尝试删除该文件。但此时您总可以将删除操作放在后台线程中不断尝试。

如果Hans是对的(我倾向于相信他的分析),那么移动文件可能并不会真正帮助,因为您可能无法实际重命名正在被其他进程打开的文件(但也可能可以,我不确定)。如果确实是这种情况,我能想到的另外一种方法只有“持续尝试”。您需要等待几毫秒并进行重试,同时设置一个超时机制,以便在无济于事时放弃。


很赞的想法。虽然我们的实际场景比我的问题复杂得多,但这个想法也许可行! :) - Mordachai
是的,我也认为汉斯是对的。:( 这只是把问题推迟了一步,但没有解决任何问题。唉。 - Mordachai
3
即使汉斯是正确的,也可能并不重要。你可以重新命名打开的文件。这是没有问题的,因为文件句柄大多独立于文件名。文件名只在创建文件句柄时才重要。已有打开文件句柄的假设过程不会受到影响。 - MSalters
1
这似乎有效!基本上,我正在创建一个临时文件(实际上是通过GetTempFileName创建),然后使用MoveFileEx将唯一的临时文件覆盖为真实文件,然后发出DeleteFile删除临时文件名。到目前为止,我无法让它出错。我预计会有性能损失-做所有这些额外的开销肯定不会很快,但为了使我们的脚本编写者免受多任务文件系统的微妙影响,这可能是值得的。 - Mordachai
1
@Mordachai:GetTempFileName()必须创建文件,否则在您获取唯一文件名和实际创建文件之间,其他人可能会创建同名文件。无论如何,很高兴您已经解决了问题。 - sbi
显示剩余7条评论

5
愚蠢的建议 - 由于失败的情况非常少,只需在失败后等待几毫秒并重试。
或者,如果延迟很重要,则切换到另一个文件名,将旧文件留待稍后删除。

4

这可能不是你特别遇到的问题,但有可能出现,建议你使用 Process Monitor(Sysinternals)进行检查。

我曾经遇到过完全相同的问题,并发现 Comodo Internet Securitycmdagent.exe)是问题的一部分。之前我使用的是双核处理器,但当我升级到英特尔i7时,我的工作软件(Perfore软件的jam.exe)不再工作,因为它具有相同的模式(删除然后创建,但没有检查)。在调试问题后,我发现GetLastError()返回访问被拒绝,但Process Monitor显示“删除挂起”。以下是跟踪:

10:39:10.1738151 AM jam.exe 5032    CreateFile  C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS Desired Access: Read Attributes, Delete, Disposition: Open, Options: Non-Directory File, Open Reparse Point, Attributes: n/a, ShareMode: Read, Write, Delete, AllocationSize: n/a, OpenResult: Opened
10:39:10.1738581 AM jam.exe 5032    QueryAttributeTagFile   C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS Attributes: ANCI, ReparseTag: 0x0
10:39:10.1738830 AM jam.exe 5032    SetDispositionInformationFile   C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS Delete: True
10:39:10.1739216 AM jam.exe 5032    CloseFile   C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS 
10:39:10.1739438 AM jam.exe 5032    IRP_MJ_CLOSE    C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS 
10:39:10.1744837 AM jam.exe 5032    CreateFile  C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   DELETE PENDING  Desired Access: Generic Write, Read Attributes, Disposition: OverwriteIf, Options: Synchronous IO Non-Alert, Non-Directory File, Attributes: N, ShareMode: Read, Write, AllocationSize: 0
10:39:10.1788811 AM jam.exe 5032    CreateFile  C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   DELETE PENDING  Desired Access: Generic Write, Read Attributes, Disposition: OverwriteIf, Options: Synchronous IO Non-Alert, Non-Directory File, Attributes: N, ShareMode: Read, Write, AllocationSize: 0
10:39:10.1838276 AM jam.exe 5032    CreateFile  C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   DELETE PENDING  Desired Access: Generic Write, Read Attributes, Disposition: OverwriteIf, Options: Synchronous IO Non-Alert, Non-Directory File, Attributes: N, ShareMode: Read, Write, AllocationSize: 0
10:39:10.1888407 AM jam.exe 5032    CreateFile  C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   DELETE PENDING  Desired Access: Generic Write, Read Attributes, Disposition: OverwriteIf, Options: Synchronous IO Non-Alert, Non-Directory File, Attributes: N, ShareMode: Read, Write, AllocationSize: 0
10:39:10.1936323 AM System  4   FASTIO_ACQUIRE_FOR_SECTION_SYNCHRONIZATION  C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS SyncType: SyncTypeOther
10:39:10.1936531 AM System  4   FASTIO_RELEASE_FOR_SECTION_SYNCHRONIZATION  C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS 
10:39:10.1936647 AM System  4   IRP_MJ_CLOSE    C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS 
10:39:10.1939064 AM jam.exe 5032    CreateFile  C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   DELETE PENDING  Desired Access: Generic Write, Read Attributes, Disposition: OverwriteIf, Options: Synchronous IO Non-Alert, Non-Directory File, Attributes: N, ShareMode: Read, Write, AllocationSize: 0
10:39:10.1945733 AM cmdagent.exe    1188    CloseFile   C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS 
10:39:10.1946532 AM cmdagent.exe    1188    IRP_MJ_CLOSE    C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS 
10:39:10.1947020 AM cmdagent.exe    1188    IRP_MJ_CLOSE    C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS 
10:39:10.1948945 AM cfp.exe 1832    QueryOpen   C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   FAST IO DISALLOWED  
10:39:10.1949781 AM cfp.exe 1832    CreateFile  C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   NAME NOT FOUND  Desired Access: Read Attributes, Disposition: Open, Options: Open Reparse Point, Attributes: n/a, ShareMode: Read, Write, Delete, AllocationSize: n/a
10:39:10.1989720 AM jam.exe 5032    CreateFile  C:\Users\Dad\AppData\Local\Temp\jam5032t1.bat   SUCCESS Desired Access: Generic Write, Read Attributes, Disposition: OverwriteIf, Options: Synchronous IO Non-Alert, Non-Directory File, Attributes: N, ShareMode: Read, Write, AllocationSize: 0, OpenResult: Created

正如您所见,有一个删除请求,然后是由jam.exe(在循环中的fopen)打开文件的多次尝试。您可以看到cmdagent.exe可能已经打开了该文件,因为它关闭了句柄,然后突然jam.exe现在能够打开该文件。
当然,建议的解决方案是等待并重试,这很有效。

1
是的,这是相同的模式。在我的情况下,是NOD32防病毒软件,但通常情况下,这个问题将(可能)存在于所有Windows机器上(以及可能存在于其他操作系统上,取决于它们如何处理其文件系统)。这不一定是操作系统的设计缺陷(尽管我不知道为什么他们没有像我一样[即使在其上仍有打开句柄的情况下立即释放目录条目])。要点:要么软件需要始终编写为“等待并重试”,要么标记为“已回答”重命名然后删除。 - Mordachai
2
顺便提一下 - 这将是Raymond Chen的一个很好的话题。太遗憾了,我无法将其放入他的队列中:( - Mordachai
https://devblogs.microsoft.com/oldnewthing/20120907-00/?p=6663 - jacobsee

4

1
额,我不认为这在我们的情况下是可能的。坦白地说,无法获取待删除文件的句柄。因此,在CreateFile()执行时,文件已经处于待删除状态,但无法获取其句柄进行检查。 :( - Mordachai
一旦文件被标记为删除,您将无法再次打开该文件。除非在文件被删除之前获取了另一个句柄,否则不可能打开该文件。 - Benjamin
好的 - 因此,任何有用的解决方案都需要回答当我调用CreateFile()时“为什么访问被拒绝”的问题。如果在那个时间点上有一种方法可以询问操作系统“文件是否正在等待删除”,那么这将是有用的。或者,如果有一种方法可以使CreateFile()更加详细而不是空白的“拒绝”,那么这将更加有用。 - Mordachai
最终,除非有人能够让我明白为什么微软要么不报告更详细的信息,要么将这些花招隐藏在操作系统的掩护下,否则我必须得出结论:这是微软非常愚蠢、思考不周的机制。我并不反对被驳斥——如果有一个真正好的理由,我会听取意见。但到目前为止,这似乎只是NTFS和Win32API团队在设计决策上不充分思考的汇合。 - Mordachai
8
在内核模式下,NtCreateFile返回STATUS_DELETE_PENDING。这可能是您想要的。但I/O管理器在返回CreateFile之前将该错误转换为ERROR_ACCESS_DENIED。 http://support.microsoft.com/kb/113996我不知道为什么他们没有将其翻译为ERROR_DELETE_PENDING。您可以在打开文件并在关闭您打开的句柄之前删除并再次删除时看到ERROR_DELETE_PENDING。 - Benjamin
由于Benjamin发布的链接失效了,这是链接网站的存档版本:https://web.archive.org/web/20150317121919/http://support.microsoft.com/en-us/kb/113996 - riQQ

3

由于您正在创建新文件、处理它,然后删除它,所以似乎您并不真正关心文件名是什么。如果确实如此,您应该考虑始终创建一个临时文件。这样,每次进行处理时,您都不必担心文件尚未被删除。


1
-1 他的问题指定了他无法控制导致这种情况发生的脚本内容。 - Elemental
1
@Elemental,没错。他说的是脚本的内容,而不是脚本的位置,所以这个踩是无效的。 - Jacob
@Jocab - 不幸的是,脚本本身包含命令,包括直接使用文件名(字面上)的删除命令,他无法更改脚本以使用临时文件名。 - Elemental
1
这是个好主意,Jacob,但实际上我们的脚本撰写者掌控文件名。尽管这种情况确实出现了,但通常并非如此。在语言层面(脚本解释器)无法假设文件的性质,并强制执行任何特定规则(对解释器而言,它只是一个输出文件名,在另一个情境中是一个输入文件名,在另一个情境中是删除命令的目标,所有这些都是不相关的)。 - Mordachai
2
脚本解释器可以在文件系统上创建具有唯一名称的文件,并维护脚本中使用的名称与实际文件系统名称之间的映射。在脚本完成后,解释器可以将剩余的文件重命名为脚本所需的名称,以创建适当的副作用。 - Adrian McCarthy

2
我在使用LoadLibrary(path)时遇到了相同的问题,无法删除path中的文件。
解决方法是“关闭句柄”或使用FreeLibrary(path)方法。
请注意:请阅读有关FreeLibrary()的“备注”:MSDN

1
根据[1],你可以使用NtDeleteFile来避免DeleteFile的异步性质。此外,[1]还介绍了DeleteFile的一些细节。
不幸的是,关于NtDeleteFile[2]的官方文档没有提及这个问题的任何特定细节。
[1]NTDLL的未记录函数 [2]ZwDeleteFile函数

2
这是错误的。NtDeleteFile 只是通过跳过两步 NtOpenFileNtSetInformationFile 序列和不需要 I/O 管理器分配真实文件对象来使设置删除处理更有效。当然,它仍然需要为设置信息请求分配 IRP 并调用文件系统驱动程序,并且当最后一个文件引用关闭/清除时,该文件仍然不会被取消链接。 - Eryk Sun

1
最佳答案是sbi提供的, 但为了完整起见,一些人可能也想知道现在Windows 10 RS1/1603中可用的新方法。
它涉及使用类FileDispositionInfoEx调用SetFileInformationByHandle API,并设置标志FILE_DISPOSITION_DELETE | FILE_DISPOSITION_POSIX_SEMANTICS。请参见RbMm提供的完整答案

1
如果CreateFile返回INVALID_HANDLE_VALUE,则应确定GetLastError在您特定情况下(待删除)返回什么,并仅基于该错误代码循环回CreateFile。
FILE_FLAG_DELETE_ON_CLOSE标志可能会为您带来一些好处。

1
我认为“访问被拒绝”是GetLastError()返回的内容,表示Windows系统拒绝了该操作。 - sbi
1
GetLastError() 总是返回“(5) 拒绝访问”——这是操作系统相当无聊、有限的响应。如果它返回“(91929) 等待删除”,那就太好了! :) - Mordachai
感谢你的想法,Pineapple。不幸的是,脚本引擎无法知道脚本何时会删除目标文件(如果有的话),因此它无法在脚本要求执行该操作之前标记该文件进行删除。一旦发生这种情况,我们就要看操作系统是否能够完成该操作。如果脚本尝试执行另一个生成该文件的命令,并且之前的命令还没有完成,那么他们将会收到这个错误。我无法控制脚本编写者的行为(从表面上看,该脚本是合理的)。 - Mordachai

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接