.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

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

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

就可能在 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是如何处理的:

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

再看看diskpart:

diskpart返回数据更合理

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

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

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

三块磁盘查询结果:

复制代码
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

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

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

相关推荐
hez20102 小时前
Satori GC:同时做到高吞吐、低延时和低内存占用
c#·.net·.net core·gc·clr
唐青枫20 小时前
C#.NET Channel 深入解析:高性能异步生产者消费者模型实战
c#·.net
小峥降临1 天前
Rokid UXR 的手势追踪虚拟中更真实的手实战开发【含 工程源码 和 最终完成APK】
c#
晨星shine5 天前
GC、Dispose、Unmanaged Resource 和 Managed Resource
后端·c#
用户298698530146 天前
.NET 文档自动化:Spire.Doc 设置奇偶页页眉/页脚的最佳实践
后端·c#·.net
用户3667462526746 天前
接口文档汇总 - 2.设备状态管理
c#
用户3667462526746 天前
接口文档汇总 - 3.PLC通信管理
c#
Ray Liang7 天前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
阿白的白日梦7 天前
winget基础管理---更新/修改源为国内源
windows