如何枚举磁盘卷名称?

6
如何列举磁盘上所有逻辑卷的列表?我想要一个用CreateFile打开的卷名。
你尝试过什么?
我使用了FindFirstVolume/FindNextVolume API来列举卷的列表。它返回一系列名称,例如:
  • \\?\Volume{0b777018-3313-11e2-8ccd-806e6f6e6963}\
  • \\?\Volume{0b777019-3313-11e2-8ccd-806e6f6e6963}\
  • \\?\Volume{758a2cf2-cf3a-11e4-8dce-c86000d0b92a}\
  • \\?\Volume{4f81d34b-34f4-11e2-9f6e-c86000d0b92a}\
但是这些卷名都不是有效的卷名。也就是说,这些名称都无法传递给CreateFile来打开卷:
0x00000003 (The system cannot find the path specified)

问题可能是如何将FindFirstVolume返回的对象转换为卷名?

但真正的问题是如何首先枚举卷?

为什么不直接使用\\.\C:

我不是在问如何硬编码卷名;我是在问如何枚举卷名。

此外,并非每个卷都有驱动器号,例如:

  • \\?\Volume{0b777019-3313-11e2-8ccd-806e6f6e6963}\ ==> \\.\C:
  • \\?\Volume{758a2cf2-cf3a-11e4-8dce-c86000d0b92a}\ ==> \\.\D:
  • \\?\Volume{0b777018-3313-11e2-8ccd-806e6f6e6963}\ ==> 没有驱动器号的系统保留卷
  • \\?\Volume{4f81d34b-34f4-11e2-9f6e-c86000d0b92a}\ ==> 安装在文件夹中的光盘驱动器

我发誓有一个API可以枚举卷。

GetLogicalDriveStrings

GetLogicalDriveStrings函数的问题在于它只返回逻辑驱动器:
  • C:\
  • D:\
而不是卷。在我的情况下,它遗漏了两个卷:
  • 系统保留
  • D:\CDROM
FindFirstVolume确实能够正确返回这两个卷。

将卷转换为驱动器编号

  1. 使用CreateFile打开卷\\?\Volume{0b777019-3313-11e2-8ccd-806e6f6e6963}
  2. 使用控制码IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS调用DeviceIoControl
  3. 这将返回一系列DISK_EXTENT结构
   DISK_EXTENT = record
       DiskNumber: DWORD;
       StartingOffset: Int64;
       ExtentLength: Int64;
   end;

您可以从中获取DiskNumber

function GetVolumeDiskExtents(VolumeName: string): TArray<DISK_EXTENT>;
var
    hVolume: THandle;
    vde:  PVolumeDiskExtents;
    bufferSize: Integer;
    bytesReturned: DWORD;
    le: DWORD;
begin
    SetLength(Result, 0);

    {
        CreateFile requires the trailing backslash of a volume name removed.
        While Volume API functions require the trailing backslash.
    }
//  VolumeName := ExcludeTrailingBackslash(VolumeName);

    bufferSize := SizeOf(vde)+2048*SizeOf(DISK_EXTENT);
    GetMem(vde, bufferSize);
    try
        hVolume := CreateFile(PChar(VolumeName),
                GENERIC_READ,
                FILE_SHARE_READ or FILE_SHARE_WRITE,
                nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
        if (hVolume = INVALID_HANDLE_VALUE) then
            RaiseLastOSError;
        try
            if not (DeviceIoControl(hVolume, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
                    nil, 0,
                    vde, bufferSize,
                    {var}bytesReturned, nil)) then
            begin
                le := GetLastError;
                if le = ERROR_INVALID_FUNCTION then //ie. CD-ROM
                    Exit;

                raise Exception.CreateFmt('Could not get volume disk extents for volume "%s": %s (0x%s)', [VolumeName, SysErrorMessage(le), IntToHex(le, 8)]);
            end;
        finally
            CloseHandle(hVolume);
        end;

        SetLength(Result, vde^.NumberOfDiskExtents);
        Move(vde^.Extents[0], Result[0], SizeOf(DISK_EXTENT)*vde^.NumberOfDiskExtents);
    finally
        FreeMem(vde);
    end;
end;

额外阅读


你知道如何通过驱动器字母获取它所属的磁盘编号吗?例如,C: -> PhysicalDrive0Disk 0(在我的情况下,Disk 0 是 SSD),D: -> PhysicalDrive1Disk 1(在我的情况下,Disk 1 是 HDD)。 - huang
@JoeHuang 请注意,一个物理磁盘(例如 PhysicalDisk0)可以包含多个卷(例如 C:D:)。同样,一个卷(例如 C:)也可以存在于多个物理磁盘上(例如 PhysicalDisk0PhysicalDisk1),这是因为使用了 RAID 等技术。 - Ian Boyd
谢谢您的回复,但我已经找到了解决方案。通过CreateFile打开卷,然后使用IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS调用DeviceIoControl句柄。 - huang
@JoeHuang 那也是我在问题中添加的内容。 - Ian Boyd
1个回答

12
你的问题的答案在命名卷中。当使用卷 GUID 路径时,规则稍有不同:

所有接受卷 GUID 路径作为输入参数的卷和装载文件夹函数都需要尾部反斜杠。[...] 但这并不适用于CreateFile函数。你可以调用 CreateFile 并从指定的卷名称中省略尾部反斜杠来打开一个卷。 CreateFile 将带有附加反斜杠的卷 GUID 路径处理为卷的根目录。

解决方案很简单:从卷 GUID 路径中删除尾部反斜杠以使用CreateFile打开卷。

换句话说,在使用诸如以下功能的卷管理函数时:
  • GetVolumeInformation
  • GetVolumePathNamesForVolumeName

需要使用FindFirstVolume/FindNextVolume返回的完整卷名称,但是使用CreateFile时需要删除返回的尾部反斜杠:

  • \\?\Volume{0b777018-3313-11e2-8ccd-806e6f6e6963}
  • \\?\Volume{0b777019-3313-11e2-8ccd-806e6f6e6963}
  • \\?\Volume{758a2cf2-cf3a-11e4-8dce-c86000d0b92a}
  • \\?\Volume{4f81d34b-34f4-11e2-9f6e-c86000d0b92a}

5
如果请求FILE_FLAG_BACKUP_SEMANTICS,则CreateFile实际上将成功打开根目录。否则,“ERROR_PATH_NOT_FOUND”并不是很有帮助,但是从“ntdll!RtlGetLastNtStatus”获取的NT状态代码会显示真正的错误为“STATUS_FILE_IS_A_DIRECTORY”,也就是说,该调用尝试将一个目录作为常规文件打开。 - Eryk Sun
1
有趣的是,Windows 只有在 lpFileName 以反斜杠结尾时将 NT 的 STATUS_FILE_IS_A_DIRECTORY 翻译为 ERROR_PATH_NOT_FOUND。否则,在没有尾随反斜杠的情况下尝试打开目录会导致 ERROR_ACCESS_DENIED - Eryk Sun
1
@eryksun:有趣的信息。现在一切都说得通了(除了不太具体的错误代码)。CreateFile 的文档确实指出需要使用 FILE_FLAG_BACKUP_SEMANTICS 标志才能获取目录句柄,但直到你解释清楚之前,我还没有想到它与 ERROR_PATH_NO_FOUND 错误代码有关。 - IInspectable
1
@IInspectable:虽然FILE_FLAG_BACKUP_SEMANTICS对于目录句柄是必需的,但备份特权却不是。由于我看到过几个开发人员在使用FILE_FLAG_BACKUP_SEMANTICS之前尝试启用备份特权,因此似乎存在一些混淆。 - 0xC0000022L

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