在Windows中使用共享属性内存映射文件(以使文件不被锁定防止删除)

3
有没有一种在Windows中将文件内容映射到内存的方法,而不会对文件进行锁定(特别是在文件仍然被映射时可以删除文件)?
Java NIO库以这样的方式在Windows中mmap文件,使得只要堆中有任何未被垃圾回收的MappedByteBuffer引用,映射的文件就不能被删除。JDK团队声称这是Windows的限制,但仅适用于文件被映射,而不是作为常规文件打开的情况:

https://mail.openjdk.java.net/pipermail/nio-dev/2019-January/005698.html

(显然,如果一个文件在mmap的时候被删除了,在Windows文件语义的世界中对于mmap区域应该发生什么是有争议的,但在Linux中已经定义明确。)
(参考资料,无法在内存映射期间(或尚未垃圾收集)删除文件会在Java中引起许多问题:)

http://www.mapdb.org/blog/mmap_files_alloc_and_jvm_crash/

有安全原因导致不支持取消映射操作:

https://bugs.openjdk.java.net/browse/JDK-4724038

更新:另请参阅:如何通过将映射替换为空页面来取消映射mmap'd文件


1
@RbMm,我们可以通过使用CreateFile / CloseHandleNtDeleteFile进行删除关闭映射文件。在这种情况下,文件系统只检查MmFlushImageSection(SectionObjectPointer, MmFlushForWrite),而不是MmFlushForDelete。可能最初是个错误,但现在已经成为标准了。 - Eryk Sun
@eryksun - 是的,你是正确的。如果创建的节没有SEC_IMAGE属性,我们可以用这种方式删除文件。 - RbMm
1个回答

2

@eryksun 所述,如果文件映射的区域(file mapping)没有使用 SEC_IMAGE 属性创建,则我们可以通过以下2种方式删除映射的文件:

  • 使用打开文件时加上 FILE_FLAG_DELETE_ON_CLOSE 标记来打开文件。这意味着“当所有与该文件相关的句柄都关闭之后,包括指定的句柄和其他任何已打开或复制的句柄时,将立即删除该文件”。或者我们可以使用 NtOpenFileNtCreateFile 调用,并附带标记 FILE_DELETE_ON_CLOSE
  • 调用 ZwDeleteFile。实际上,内部NtDeleteFile 打开了带有 FILE_DELETE_ON_CLOSE 标记和特殊内部配置项DeleteOnly = TRUE的文件。这使得该调用比正常打开文件再关闭句柄更加高效。

代码示例:

#ifndef FILE_SHARE_VALID_FLAGS
#define FILE_SHARE_VALID_FLAGS 0x00000007
#endif

NTSTATUS Delete1(PCWSTR FileName)
{
    HANDLE hFile = CreateFile(FileName, DELETE, FILE_SHARE_VALID_FLAGS, 0, OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, 0);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        return RtlGetLastNtStatus();
    }
    CloseHandle(hFile);
    return 0;
}

NTSTATUS Delete2(PCWSTR FileName)
{
    UNICODE_STRING ObjectName;

    if (RtlDosPathNameToNtPathName_U(FileName, &ObjectName, 0, 0))
    {
        OBJECT_ATTRIBUTES oa = { sizeof(oa), 0, &ObjectName };

        NTSTATUS status = ZwDeleteFile(&oa);

        RtlFreeUnicodeString(&ObjectName);

        return status;
    }

    return STATUS_UNSUCCESSFUL;
}

请注意,在此处调用DeleteFileW会失败,并显示状态为STATUS_CANNOT_DELETE。我建议在这里调用RtlGetLastNtStatus(),而不是GetLastError(),因为Win32将NTSTATUS映射到错误代码时不是单射的,并且经常会丢失有价值的信息。例如,STATUS_CANNOT_DELETE被映射到ERROR_ACCESS_DENIED。但是还有许多其他的NTSATUS代码也被映射到ERROR_ACCESS_DENIEDERROR_ACCESS_DENIED不仅代表STATUS_ACCESS_DENIED(真正的拒绝访问)。相比ERROR_ACCESS_DENIEDSTATUS_CANNOT_DELETE更具信息量。 RtlGetLastNtStatusGetLastError具有完全相同的签名,并且从ntdll.dll中导出(因此请包含ntdll.libntdllp.lib)。
extern "C" NTSYSCALLAPI NTSTATUS NTAPI RtlGetLastNtStatus();

1
盲目地从CreateFile调用中返回RtlGetLastNtStatus()是可疑的。CreateFile可能会失败而不设置最后的NT状态值。例如,如果路径太长,则转换将以STATUS_NAME_TOO_LONG失败,在这种情况下,CreateFile只需调用RtlSetLastWin32Error(ERROR_PATH_NOT_FOUND)。状态值未保存在TEB LastStatusValue中。 - Eryk Sun
1
在第二种情况下,我建议使用 RtlDosPathNameToNtPathName_U_WithStatus 函数,这样我们可能就可以知道为什么调用失败了。该函数在 NT 5.2+(Server 2003 SP1 及以上)中可用,因此除非你必须支持古老版本,否则不是问题。 - Eryk Sun
@eryksun 关于 RtlGetLastNtStatus(),是的,您描述的情况可能存在,但从另一个角度来看,GetLastError() 经常会丢失信息。为了调试目的,调用 RtlGetLastNtStatus() 至少有一定意义。也许这两个 API 都可以使用。 - RbMm
1
是的,当从WinAPI中获取时,我们可能需要两者,但我们必须小心处理,并将其留给调用者。我们可以将CreateFile的最后状态值捕获为输出参数并返回Windows错误代码。调用者可以决定它是否相关。例如,调用者可以忽略ERROR_PATH_NOT_FOUND的状态。这对于澄清获取ERROR_ACCESS_DENIED的原因非常有用(一个常见而令人烦恼的例子是STATUS_FILE_IS_A_DIRECTORY)。 - Eryk Sun
1
@LukeHutchison - 当您可以在使用文件(映射)时删除文件时,文件不会被完全删除,而是从目录中取消映射并且不再可见。我们可以说创建具有此名称的新文件。但是,只有在所有使用它结束后,文件数据最终才会被删除。FILE_DISPOSITION_POSIX_SEMANTICS - 但这需要Win10的最新版本。 - RbMm
显示剩余7条评论

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