资源管理器具有共享删除/重命名访问权限的目录的开放句柄。这允许rmdir
成功,而通常打开不会共享删除/重命名访问权限,并且rmdir
会因共享违规而失败 (32)。然而,尽管rmdir
成功后,该目录实际上不会取消链接,直到资源管理器关闭其句柄。它正在监视目录的更改,因此它会收到目录已被删除的通知,但即使它立即关闭其句柄,也存在与脚本的竞争条件os.mkdir
call.
你应该重试os.mkdir
循环中,超时时间不断增加。您还需要一个onerror
处理程序shutil.rmtree
它处理尝试删除不为空的目录,因为它包含“已删除”的文件或目录。
例如:
import os
import time
import errno
import shutil
def onerror(function, path, exc_info):
# Handle ENOTEMPTY for rmdir
if (function is os.rmdir
and issubclass(exc_info[0], OSError)
and exc_info[1].errno == errno.ENOTEMPTY):
timeout = 0.001
while timeout < 2:
if not os.listdir(path):
return os.rmdir(path)
time.sleep(timeout)
timeout *= 2
raise
def clean_dir_safe(path):
shutil.rmtree(path, onerror=onerror)
# rmtree didn't fail, but path may still be linked if there is or was
# a handle that shares delete access. Assume the owner of the handle
# is watching for changes and will close it ASAP. So retry creating
# the directory by using a loop with an increasing timeout.
timeout = 0.001
while True:
try:
return os.mkdir(path)
except PermissionError as e:
# Getting access denied (5) when trying to create a file or
# directory means either the caller lacks access to the
# parent directory or that a file or directory with that
# name exists but is in the deleted state. Handle both cases
# the same way. Otherwise, re-raise the exception for other
# permission errors, such as a sharing violation (32).
if e.winerror != 5 or timeout >= 2:
raise
time.sleep(timeout)
timeout *= 2
讨论
在常见情况下,此问题是“避免”的,因为现有打开不共享删除/重命名访问权限。在这种情况下,尝试删除文件或目录会因共享冲突而失败(winerror 32)。例如,如果一个目录作为进程的工作目录打开,则它不共享删除/重命名访问权限。对于常规文件,大多数程序仅共享读/执行和写/追加访问权限。
临时文件通常通过删除/重命名访问共享打开,尤其是通过删除/重命名访问打开时(例如,使用关闭时删除标志打开)。这是导致“已删除”文件仍然链接但无法访问的最常见原因。另一种情况是打开一个目录来观察更改(例如,参见ReadDirectoryChangesW https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw)。通常,此打开将共享删除/重命名访问权限,这就是本问题中资源管理器的情况。
对于 Unix 开发人员来说,声明文件在不取消链接的情况下被删除可能听起来很奇怪(至少可以这么说)。在Windows中,删除文件(或目录)只是在其文件控制块(FCB)上设置删除配置。当文件系统清理文件的最后一个内核文件对象引用时,设置了删除处置的文件会自动取消链接。文件对象通常由以下方式创建CreateFileW
,它返回对象的句柄。当文件对象的最后一个句柄关闭时,会触发文件对象的清理。由于子进程中的句柄继承或显式的句柄继承,可能存在文件对象的多个句柄引用DuplicateHandle
calls.
重申一下,一个文件或目录可以被多个内核文件对象引用,其中每个文件或目录可以被多个句柄引用。通常,使用经典的 Windows 删除语义,在文件取消链接之前必须关闭所有句柄。此外,设置删除处置不一定是最终的。如果任何打开的句柄具有删除/重命名访问权限,则实际上可以通过清除删除配置来恢复对文件的访问权限(例如,请参阅SetFileInformationByHandle https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileinformationbyhandle: FileDispositionInfo
).
在 Windows 10 中,内核还支持 POSIX 删除语义,一旦删除句柄关闭,文件或目录就会立即取消链接(请参阅 NTAPI 的详细信息)FileDispositionInformationEx https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information_ex)。 NTFS 已更新为支持 POSIX 删除语义。最近WINAPIDeleteFileW
(即Pythonos.remove
)如果文件系统支持它,则已切换为使用它,但是RemoveDirectoryW
(即Pythonos.rmdir
)仍然仅限于经典的 Windows 删除。
对于 NTFS 来说,实现 POSIX 语义相对容易。它只是设置删除配置,并将文件重命名为 NTFS 保留目录“\$Extend\$Deleted”,名称基于其文件 ID。实际上,文件似乎已取消链接,同时继续允许现有文件对象访问该文件。与经典删除相比,一个显着区别是原始名称丢失,因此具有删除/重命名访问权限的现有句柄无法取消设置删除配置。