最近在处理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
返回的是一卷多盘的结果
方案三:按返回结果做兼容
- 没有拿到 extent:跳过该卷
- 只映射到一个磁盘:继续按原模型处理
- 映射到多个磁盘:说明是跨盘卷,当前 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
并且在现有单盘模型下,应对多磁盘卷做降级跳过,不要让特殊卷拖垮整体查询流程。
这次修复虽然不大,但本质上是把“错误的单一映射假设”改成了“按卷类型分流处理”,稳定性会好很多
出处:http://www.cnblogs.com/kybs0/让学习成为习惯,假设明天就有重大机遇等着你,你准备好了么本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |