.NET Win32磁盘动态卷触发“函数不正确”问题排查

最近在处理Win32磁盘管理.NET 磁盘管理-技术方案选型 - 唐宋元明清2188 - 博客园-获取本地磁盘信息时,遇到一个比较隐蔽的问题。

磁盘对象获取异常,DEVICEIOCONTROL.IOCTL_STORAGE_GET_DEVICE_NUMBER FAILED, 函数不正确。(0X00000001)

当机器上出现动态卷、跨区扩展卷这类特殊卷时,GetDiskNumberByVolumeName 中执行 DeviceIoControl 会直接报错:

  • Win32异常码:1

  • Win32错误信息:函数不正确

表面上看像是权限问题,或者句柄打开方式不对

一、问题现象

当前逻辑中,代码会先枚举系统卷,再通过卷句柄去反查磁盘号。

 1         private OperateResult<uint?> GetDiskNumberByVolumeName(string volumeName)
 2         {
 3             // 打开卷设备 volumeName: \\?\Volume{GUID}\
 4             string volumePathForDevice = volumeName.TrimEnd('\\'); // \\?\Volume{GUID}
 5             IntPtr hVolume = CreateFile(
 6                 volumePathForDevice,
 7                 0, // 只需要 IOCTL,不读写
 8                 FILE_SHARE_READ | FILE_SHARE_WRITE,
 9                 IntPtr.Zero,
10                 OPEN_EXISTING,
11                 0,
12                 IntPtr.Zero);
13             IntPtr outBuf = IntPtr.Zero;
14             try
15             {
16                 // 不存在这个物理盘(或者无权限),忽略此异常
17                 if (hVolume == INVALID_HANDLE_VALUE)
18                 {
19                     return OperateResult<uint?>.ToSuccess();
20                 }
21                 // 取 STORAGE_DEVICE_NUMBER
22                 uint size = (uint)Marshal.SizeOf<STORAGE_DEVICE_NUMBER>();
23                 outBuf = Marshal.AllocHGlobal((int)size);
24                 if (!DeviceIoControl(
25                         hVolume,
26                         IOCTL_STORAGE_GET_DEVICE_NUMBER,
27                         IntPtr.Zero,
28                         0,
29                         outBuf,
30                         size,
31                         out _,
32                         IntPtr.Zero))
33                 {
34                     return OperateResult<uint?>.ToWin32Error("DeviceIoControl.IOCTL_STORAGE_GET_DEVICE_NUMBER failed", Marshal.GetLastWin32Error());
35                 }
36                 STORAGE_DEVICE_NUMBER devNum = Marshal.PtrToStructure<STORAGE_DEVICE_NUMBER>(outBuf);
37                 // DeviceType 为 FILE_DEVICE_DISK(0x07) 一般表示物理磁盘
38                 var diskNumber = devNum.DeviceNumber;
39                 return OperateResult<uint?>.ToSuccess(diskNumber);
40             }
41             catch (Exception e)
42             {
43                 return OperateResult<uint?>.ToError(e);
44             }
45             finally
46             {
47                 Marshal.FreeHGlobal(outBuf);
48                 CloseInPtr(hVolume);
49             }
50         }

核心调用点大致如下:

  • 枚举卷:FindFirstVolumeW / FindNextVolumeW
  • 打开卷句柄:CreateFile("\\?\Volume{GUID}")
  • 查询设备号:IOCTL_STORAGE_GET_DEVICE_NUMBER

在普通基础磁盘、普通分区场景下,这套逻辑是正常的。

但只要本地存在动态磁盘卷、跨区卷、条带卷或镜像卷,如下图:

image

就可能在 IOCTL_STORAGE_GET_DEVICE_NUMBER 这里失败,并返回 ERROR_INVALID_FUNCTION(1)

二、根因分析

IOCTL_STORAGE_GET_DEVICE_NUMBER 更适合“一个卷能明确映射到一个底层设备号”的场景。

而动态卷、跨区卷这类卷,本质上已经不是简单的“一个卷对应一个物理盘分区”模型。它们可能:

  • 一个卷对应多个磁盘 extent
  • 一个卷跨越多个物理磁盘
  • 卷设备背后由卷管理器做了抽象

这时再去对卷句柄直接调用 IOCTL_STORAGE_GET_DEVICE_NUMBER,驱动栈可能根本不支持,于是直接返回 ERROR_INVALID_FUNCTION

也就是说,不是调用方式写错了,而是调用的接口选错了。即:当前调用的 IOCTL 并不适用于这类卷

1. 原接口的局限

这个 IOCTL 返回的是 STORAGE_DEVICE_NUMBER,核心是:

  • DeviceType
  • DeviceNumber
  • PartitionNumber

它适合基础磁盘、普通分区、单一设备映射场景。

2. 特殊卷真正需要的能力

对于动态卷、跨区卷,正确的问题不是“这个卷对应哪个磁盘号”,而是“这个卷分布在哪些物理磁盘 extent 上”。

因此正确接口应改为:

  • IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS

这个 IOCTL 返回:

  • VOLUME_DISK_EXTENTS
  • 内部包含多个 DISK_EXTENT

可以获取该卷分布在哪些磁盘上,以及每段 extent 的磁盘号、偏移和长度。


三、解决方案

这类问题有三种解决方向

方案一:不支持动态/扩展卷

普通卷走 IOCTL_STORAGE_GET_DEVICE_NUMBER查询即可,不兼容动态卷

方案二:兼容动态卷,返回扩展卷真实结构

当出现 ERROR_INVALID_FUNCTION(1) 时,自动改走 IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS

返回的是一卷多盘的结果

方案三:按返回结果做兼容

  1. 没有拿到 extent:跳过该卷

  2. 只映射到一个磁盘:继续按原模型处理
  3. 映射到多个磁盘:说明是跨盘卷,当前 LocalDisk / DiskVolumePath 仍是一卷一盘模型,不强行归属,直接跳过,避免语义错误

我们先看看Powershell是如何处理的:

image

Powershell,Volume列表返回了真实列表,但磁盘列表只返回了一个盘符C所在磁盘

再看看diskpart:

image

diskpart返回数据更合理

所以我也决定采用方案三的兼容方法,返兼容数据

  • 普通基础磁盘卷:继续正常识别
  • 动态卷但只落在单磁盘上的场景:可以通过 VOLUME_DISK_EXTENTS 正常识别
  • 跨区卷/多磁盘卷:不再导致 GetDisks() 整体失败
  • 卷枚举逻辑不会因为“跳过卷”而卡死

也就是说,原来是一个特殊卷拖垮全部磁盘查询,现在变成了特殊卷按能力降级处理,普通磁盘查询保持可用。

代码修改如下,补充VolumeExtents:

  1        private OperateResult<uint?> GetDiskNumberByVolumeName(string volumeName)
  2        {
  3            // 打开卷设备 volumeName: \\?\Volume{GUID}\
  4            string volumePathForDevice = volumeName.TrimEnd('\\'); // \\?\Volume{GUID}
  5            IntPtr hVolume = CreateFile(
  6                volumePathForDevice,
  7                0, // 只需要 IOCTL,不读写
  8                FILE_SHARE_READ | FILE_SHARE_WRITE,
  9                IntPtr.Zero,
 10                OPEN_EXISTING,
 11                0,
 12                IntPtr.Zero);
 13            IntPtr outBuf = IntPtr.Zero;
 14            try
 15            {
 16                // 不存在这个物理盘(或者无权限),忽略此异常
 17                if (hVolume == INVALID_HANDLE_VALUE)
 18                {
 19                    return OperateResult<uint?>.ToSuccess();
 20                }
 21                // 取 STORAGE_DEVICE_NUMBER
 22                uint size = (uint)Marshal.SizeOf<STORAGE_DEVICE_NUMBER>();
 23                outBuf = Marshal.AllocHGlobal((int)size);
 24                if (!DeviceIoControl(
 25                        hVolume,
 26                        IOCTL_STORAGE_GET_DEVICE_NUMBER,
 27                        IntPtr.Zero,
 28                        0,
 29                        outBuf,
 30                        size,
 31                        out _,
 32                        IntPtr.Zero))
 33                {
 34                    int err = Marshal.GetLastWin32Error();
 35                    if (err == ERROR_INVALID_FUNCTION)
 36                    {
 37                        var getDiskNumbersResult = GetDiskNumbersByVolumeExtents(volumeName);
 38                        if (!getDiskNumbersResult.Success)
 39                        {
 40                            return getDiskNumbersResult.ToResult<uint?>();
 41                        }
 42 
 43                        var diskNumbers = getDiskNumbersResult.Data ?? new List<uint>();
 44                        if (diskNumbers.Count == 0)
 45                        {
 46                            return OperateResult<uint?>.ToSuccess();
 47                        }
 48                        if (diskNumbers.Count == 1)
 49                        {
 50                            return OperateResult<uint?>.ToSuccess(diskNumbers[0]);
 51                        }
 52 
 53                        return OperateResult<uint?>.ToSuccess();
 54                    }
 55                    return OperateResult<uint?>.ToWin32Error("DeviceIoControl.IOCTL_STORAGE_GET_DEVICE_NUMBER failed", err);
 56                }
 57                STORAGE_DEVICE_NUMBER devNum = Marshal.PtrToStructure<STORAGE_DEVICE_NUMBER>(outBuf);
 58                // DeviceType 为 FILE_DEVICE_DISK(0x07) 一般表示物理磁盘
 59                var diskNumber = devNum.DeviceNumber;
 60                return OperateResult<uint?>.ToSuccess(diskNumber);
 61            }
 62            catch (Exception e)
 63            {
 64                return OperateResult<uint?>.ToError(e);
 65            }
 66            finally
 67            {
 68                Marshal.FreeHGlobal(outBuf);
 69                CloseInPtr(hVolume);
 70            }
 71        }
 72 
 73        private OperateResult<List<uint>> GetDiskNumbersByVolumeExtents(string volumeName)
 74        {
 75            string volumePathForDevice = volumeName.TrimEnd('\\');
 76            IntPtr hVolume = CreateFile(
 77                volumePathForDevice,
 78                0,
 79                FILE_SHARE_READ | FILE_SHARE_WRITE,
 80                IntPtr.Zero,
 81                OPEN_EXISTING,
 82                0,
 83                IntPtr.Zero);
 84            IntPtr outBuf = IntPtr.Zero;
 85            try
 86            {
 87                if (hVolume == INVALID_HANDLE_VALUE)
 88                {
 89                    return OperateResult<List<uint>>.ToSuccess(new List<uint>());
 90                }
 91 
 92                int extentSize = Marshal.SizeOf<DISK_EXTENT>();
 93                int firstExtentOffset = Marshal.OffsetOf<VOLUME_DISK_EXTENTS>(nameof(VOLUME_DISK_EXTENTS.Extents)).ToInt32();
 94                uint allocSize = (uint)(firstExtentOffset + extentSize * 4);
 95 
 96                while (true)
 97                {
 98                    outBuf = Marshal.AllocHGlobal((int)allocSize);
 99                    if (DeviceIoControl(
100                            hVolume,
101                            IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
102                            IntPtr.Zero,
103                            0,
104                            outBuf,
105                            allocSize,
106                            out uint bytesReturned,
107                            IntPtr.Zero))
108                    {
109                        int extentCount = Marshal.ReadInt32(outBuf);
110                        var diskNumbers = new List<uint>(extentCount);
111                        IntPtr pCurrent = IntPtr.Add(outBuf, firstExtentOffset);
112                        for (int i = 0; i < extentCount; i++)
113                        {
114                            var extent = Marshal.PtrToStructure<DISK_EXTENT>(pCurrent);
115                            uint diskNumber = unchecked((uint)extent.DiskNumber);
116                            if (!diskNumbers.Contains(diskNumber))
117                            {
118                                diskNumbers.Add(diskNumber);
119                            }
120                            pCurrent = IntPtr.Add(pCurrent, extentSize);
121                        }
122                        return OperateResult<List<uint>>.ToSuccess(diskNumbers);
123                    }
124 
125                    int err = Marshal.GetLastWin32Error();
126                    Marshal.FreeHGlobal(outBuf);
127                    outBuf = IntPtr.Zero;
128                    if (err != ERROR_MORE_DATA && err != ERROR_INSUFFICIENT_BUFFER && err != ERROR_BUFFER_OVERFLOW)
129                    {
130                        return OperateResult<List<uint>>.ToWin32Error("DeviceIoControl.IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS failed", err);
131                    }
132 
133                    uint nextSize = bytesReturned > allocSize ? bytesReturned : allocSize * 2;
134                    allocSize = nextSize;
135                }
136            }
137            catch (Exception e)
138            {
139                return OperateResult<List<uint>>.ToError(e);
140            }
141            finally
142            {
143                Marshal.FreeHGlobal(outBuf);
144                CloseInPtr(hVolume);
145            }
146        }

三块磁盘查询结果:

Number: 0
DeviceName:  WDC WD30EZRZ-00Z5HB0
SerialNumber: WD-WCC4N3TUDSUY
IsOnline: True
ReadOnly: False
BusType: Sata
IsInitialized: True
PartitionStyle: GPT
PartitionCount: 3
MountPaths: E:\
FileSystemType: NTFS
Tag: 杂烩
DiskSize: 2861588 M
DiskAllocateSize: 0 M
DiskUsedSize: 38354 M
------------------------------------------------------------
Number: 1
DeviceName:  Samsung SSD 870 EVO 1TB
SerialNumber: S627NF0R903848J
IsOnline: True
ReadOnly: False
BusType: Sata
IsInitialized: True
PartitionStyle: GPT
PartitionCount: 3
MountPaths: D:\
FileSystemType: NTFS
Tag: 代码
DiskSize: 953869 M
DiskAllocateSize: 0 M
DiskUsedSize: 248179 M
------------------------------------------------------------
Number: 2
DeviceName:  WDS500G3X0C-00SJG0
SerialNumber: E823_8FA6_BF53_0001_001B_448B_46D9_46A7.
IsOnline: True
ReadOnly: False
BusType: Nvme
IsInitialized: True
PartitionStyle: GPT
PartitionCount: 2
MountPaths: C:\
FileSystemType: NTFS
Tag: Win11_SYSTEM
DiskSize: 476940 M
DiskAllocateSize: 476739 M
DiskUsedSize: 334920 M
------------------------------------------------------------

为什么没有直接做成完整支持动态卷?

因为大部分场景都建立在“一卷对应一盘”的前提上。

但动态卷、跨区卷天然可能是一卷多盘。如果硬塞进当前模型,会引出卷标归属、挂载路径展示、容量统计重复、修改挂载点和扩容能力边界等一系列问题。上层业务处理会变的更复杂

四、结论

这次问题的本质,不是代码写错,而是对卷类型的抽象过于理想化

原来的逻辑默认一个卷一定能映射到一个磁盘号,但动态卷、跨区卷打破了这个前提。

最终结论是:

  • 普通卷:IOCTL_STORAGE_GET_DEVICE_NUMBER
  • 特殊卷:IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS

并且在现有单盘模型下,应对多磁盘卷做降级跳过,不要让特殊卷拖垮整体查询流程。

这次修复虽然不大,但本质上是把“错误的单一映射假设”改成了“按卷类型分流处理”,稳定性会好很多

posted @ 2026-03-10 01:40  唐宋元明清2188  阅读(95)  评论(2)    收藏  举报