如何从文件 HANDLE 获取包含目录的 HANDLE?

Posted

技术标签:

【中文标题】如何从文件 HANDLE 获取包含目录的 HANDLE?【英文标题】:How do I get a HANDLE to the containing directory from a file HANDLE? 【发布时间】:2019-06-11 06:11:26 【问题描述】:

给定一个文件的句柄(例如C:\\FolderA\\file.txt),我想要一个函数,它将一个句柄返回到包含目录(在前面的例子中,它是一个句柄到C:\\FolderA)。例如:

HANDLE hFile = CreateFileA(
                  "C:\\FolderA\\file.txt",
                  GENERIC_READ,
                  FILE_SHARE_READ,
                  NULL,
                  OPEN_EXISTING,
                  FILE_ATTRIBUTE_NORMAL,
                  NULL);
HANDLE hDirectory = somefunc(hFile);

someFunc 的可能实现:

HANDLE someFunc(HANDLE h)

    char *path = getPath(h);             // "C:\\FolderA\\file.txt"
    char *parent = getParentPath(path);  // "C:\\FolderA"
    HANDLE hFile = CreateFileA(
              parent,
              GENERIC_READ,
              FILE_SHARE_READ,
              NULL,
              OPEN_EXISTING,
              FILE_ATTRIBUTE_NORMAL,
              NULL);
    free(parent);
    free(path);
    return hFile;

但是有没有办法在没有getParentPath 的情况下实现someFunc 或者不让它查看字符串并删除最后一个目录分隔符之后的所有内容(因为从性能的角度来看这很糟糕)?

【问题讨论】:

为什么要这样做?您试图解决的真正问题是什么? 我还没有完全理解你想要做什么。所以也许 FindFirstFile 和 FindNextFile 是你应该看看的函数。 我真的不认为在最常见的用例中需要多个内存分配......对于正常路径(如您展示的示例),它的长度最多可以是@ 987654330@ 个字符。您可以拥有一个该大小的数组并使用GetFinalPathNameByHandle 来获取路径名。然后在最后一个反斜杠处截断字符串(使用例如strrchr 来查找它,并将其替换为'\0'),您就有了父目录的路径。 您打算使用目录的句柄做什么?这听起来像XY Problem 你的结论是错误的。内存分配不是瓶颈。话虽如此,坚持使用 ANSI API 会导致许多虚假分配。并拒绝您访问在 ANSI 字符集之外命名的对象。使用 Unicode API。 【参考方案1】:

我不知道getParentPath 是什么。我假设它是一个在字符串中搜索尾随反斜杠并使用它来剥离文件规范的函数。您不必自己定义这样的函数; Windows 已经为您提供了一个—PathCchRemoveFileSpec。 (请注意,这假定指定的路径实际上包含要删除的文件名。如果路径不包含文件名,它将删除尾随目录名。您可以使用其他函数来验证路径是否包含文件规范。)

这个函数的旧版本是PathRemoveFileSpec,你可以在没有更新、更安全的函数的低级操作系统上使用它。

在 Windows API 之外,还有其他方法可以做同样的事情。如果您的目标是 C++17,则有 filesystem::path 类。 Boost 提供了类似的东西。或者,如果您绝对需要,您可以使用std::string 类的find_last_of 成员函数自己编写它。 (但最好不要重新发明***。在涉及到路径操作时,有很多边缘情况是您可能不会想到的,而且您的测试也可能不会揭示。)

您对这种方法的性能表示担忧。这是无稽之谈。 从字符串中删除一些字符并不是一个缓慢的操作。如果您从字符串的开头开始搜索,然后在找到文件规范后复制第二个副本,这甚至不会很慢字符串,再次从字符串的开头开始。这是一个简单的循环搜索合理长度字符串的字符,然后是一个简单的memcpy此操作绝对不可能成为执行文件 I/O 的代码的性能瓶颈。

但是,实现可能甚至不会那么幼稚。您可以通过从路径字符串的 end 开始搜索来优化它,减少您必须遍历的字符数,并且如果允许,您可以完全避免任何类型的内存复制操纵原始字符串。使用 C 风格的字符串,您只需用 NUL 字符 (\0) 替换尾随路径分隔符(分隔路径规范开头的分隔符)。使用 C++ 风格的字符串,您只需调用 erase 成员函数。

事实上,如果您真的关心性能,这实际上保证比进行系统调用从文件对象中检索包含文件夹要快。 系统调用比一些编译器生成的、可内联的代码来遍历字符串并去除子字符串要慢得多。

一旦你有了目录的路径,你就可以通过调用带有FILE_FLAG_BACKUP_SEMANTICS标志的CreateFile函数来获得一个HANDLE(必须传递那个标志如果你想检索一个目录的句柄。


我测得这很慢,正在寻找更快的方法。

您的测量结果有误。要么你犯了对调试版本进行基准测试的常见错误,其中标准库功能(例如,std::string)没有优化,和/或真正的性能瓶颈是文件 I/O。 CreateFile 不是任何想象中的快速功能。我几乎可以保证这将成为您的热点。


请注意,如果您还没有路径,则可以直接获取从 HANDLE 到文件的路径。 正如 cmets 中所指出的,在 Windows Vista 和稍后,您只需调用GetFinalPathNameByHandle 函数。在 MSDN 上的this article 中提供了更多详细信息,包括示例代码和用于 Windows 低级版本的替代方法。

正如问题的 cmets 中已经提到的,您可以通过在堆栈上分配长度为 MAX_PATH(或者甚至更大)的缓冲区来进一步优化这一点。这编译为一条指令来调整堆栈指针,因此它也不会成为性能瓶颈。 (好吧,我撒谎了:你实际上需要 两条 指令——一条用于在堆栈上创建空间,另一条用于释放堆栈上分配的空间。这仍然不是性能问题。)这样,您甚至不必进行任何动态内存分配。

请注意,为了获得最大的稳健性,尤其是在 Windows 10 上,您需要处理路径长于 MAX_PATH 的情况。在这种情况下,您的堆栈分配缓冲区将太小,您调用填充它的函数将返回错误。处理该错误,并在空闲存储区分配更大的缓冲区。这会慢一些,但这是一个边缘情况,可能不值得优化。 99% 的常见情况将使用堆栈分配的缓冲区。

此外,eryksun 指出(在此答案的 cmets 中),尽管很方便,GetFinalPathNameByHandle 需要多个系统调用来映射 NT 和 DOS 命名空间之间的文件对象并规范化路径。我没有拆解这个功能,所以我无法证实他的说法,但我没有理由怀疑他们。在正常情况下,您不会担心这种开销或可能的性能成本,但由于这似乎是您的应用程序的一个大问题,您可以使用 eryksun 的替代建议,即调用 GetFileInformationByHandleEx 并请求 FileNameInfo 类. GetFileInformationByHandleEx 是一个通用的多用途函数,可以检索有关文件的所有不同类型的信息,包括路径。它的实现更简单,直接调用原生的NtQueryInformationFile函数。我原以为GetFinalPathNameByHandle 只是提供此服务的用户模式包装器,但 eryksun 的研究表明,如果这确实是一个性能热点,您可能希望避免它做额外的工作。我必须通过注意GetFileInformationByHandleEx 来稍微限定这一点,以便检索FileNameInfo,将不得不创建一个 I/O 请求数据包 (IRP) 并调用底层设备驱动程序。这不是一个便宜的操作,所以我不确定规范化路径的额外开销是否真的很重要。但在这种情况下,使用 GetFileInformationByHandleEx 方法并没有真正的危害,因为它是一个文档化的函数。


如果您已按照所述编写代码,但仍存在可衡量的性能问题,请发布该代码以供他人审核并帮助您优化。 Code Review Stack Exchange 站点是在工作代码方面获得类似帮助的好地方。请随时在此答案下的评论中给我留下指向此类问题的链接,以免我错过。

无论您做什么,停止调用 ANSI 版本的 Windows API 函数(以 A 后缀结尾的函数)。字符 (Unicode) 版本。这些以W 后缀结尾,并使用由WCHAR (== wchar_t) 字符组成的字符串。除了 ANSI 版本由于不提供 Unicode 支持(2000 年后编写的任何应用程序在路径中支持 Unicode 字符不是可选的)而已被弃用几十年这一事实之外,就像您关心性能一样,您应该知道所有A-suffixed API 函数只是将传入的 ANSI 字符串转换为 Unicode 字符串然后委托给W-suffixed 版本的存根。如果函数返回一个字符串,则第二次转换也必须由A-suffixed 版本完成,因为所有本机 API 都使用 Unicode 字符串。性能并不是您应该避免调用 ANSI 函数的真正原因,但也许它会让您觉得更有说服力。

可能有一种方法来做你想做的事(通过HANDLE 将文件对象映射到它的包含目录),但它需要未记录的 NT 原生 API 使用。我在记录的函数中根本看不到任何可以让您获取此信息的内容。它当然不能通过GetFileInformationByHandleEx 函数访问。无论好坏,用户模式文件系统 API 几乎完全基于路径。据推测,它在内部跟踪的,但即使是记录在案的 NT 本机 API 函数,它们采用根目录 HANDLE(例如,NtDeleteFile 通过 OBJECT_ATTRIBUTES 结构)允许此字段为 NULL,在这种情况下使用完整路径字符串。

与往常一样,如果您提供了有关大局的更多详细信息,我们可能会提供更合适的解决方案。这就是评论者在提到 XY 问题时所追求的。是的,人们质疑您的动机,因为这是我们提供最适当帮助的方式。

【讨论】:

假设我们还没有文件路径,我将从 1024 个字符的堆栈缓冲区开始,而不是 MAX_PATH。这是一个合理的大小,几乎可以处理所有情况。 GetFinalPathNameByHandleW 需要多个系统调用来将设备从 NT 解析回 DOS 并规范化路径。我不会用它。对于这种情况,我会使用GetFileInformationByHandleEx:FileNameInfo,它只需要一个NtQueryInformationFile 系统调用。 至于在 NT 中使用 RootDirectory 句柄,Windows 文件 API 仅将其用于相对于工作目录的打开,例如“spam.txt”或“./eggs/spam.txt”。 (相比之下,注册表 API 总是使用它。)注意 NT 不支持“.”。和“..”组件(目录列表中的伪造条目除外),因此在进行系统调用之前,必须首先将带有“..”的相对路径解析为完整路径(例如NtCreateFile)。因此,Windows 同时在 PEB 进程参数中存储工作目录的句柄和路径。 @eryksun 感谢您的 cmets。我不知道GetFinalPathNameByHandle 正在做额外的工作;我认为这是一个简单的用户模式包装器,完全按照您的建议作为替代方案。我已经更新了答案以反映该性能问题和您的替代建议。关于你的第二条评论,我完全清楚这一点。我看不出这与将文件 HANDLE 映射到其父目录有何相关性。正如您所说,PEB 包含工作目录的句柄。问题是,原生 I/O 子系统文件对象是否包含对其目录的引用? 文件系统file object 没有指向包含目录的文件对象的指针。这将需要在每次创建或打开文件系统文件时创建内核文件对象备份到根目录。在相关说明中,但不是 OP 想要的,文件对象确实通过卷参数块 (VPB) 引用了已安装的文件系统设备。因此,它可用于在支持的文件系统(如 NTFS)上通过OpenFileById 按 ID 打开文件。 关于GetFinalPathNameByHandleW带默认标志,首先调用NtQueryObject获取设备名,但其中包含文件系统路径(文件名),所以调用NtQueryInformationFile拆分打开的文件名。然后调用NtCreateFile打开挂载点管理器,调用NtDeviceIoControlFile查询目标设备的DOS名称。然后它第二次调用NtQueryInformationFile 来查询规范化的文件名。如果是 UNC 路径(“\Device\Mup”),规范化需要GetLongPathNameW,它在短路径组件上调用NtQueryDirectoryFile

以上是关于如何从文件 HANDLE 获取包含目录的 HANDLE?的主要内容,如果未能解决你的问题,请参考以下文章

licode学习之erizo篇--Pipeline_handle

(14)go-micro微服务服务层Handle开发

如何以透明的方式从另一个 git 分支获取目录的副本?

Handle的原理代码实现

delphi中获取memo鼠标所在位置的行和列(通过EM_GETRECT消息取得Rect后,自己算一下)

Handle的原理(LooperHandlerMessage三者关系)