当前位置: 首页 > news >正文

.NET 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()整体失败
  • 卷枚举逻辑不会因为“跳过卷”而卡死

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

代码修改如下,补充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
http://www.jsqmd.com/news/562311/

相关文章:

  • MySQL篇 索引失效
  • 常见电机分类
  • C# Serilog, Serilog.Sinks.Console, Serilog.Sinks.File
  • 防火墙穿透实操:openEuler22.03的vsftp被动模式配置详解
  • 别再只盯着功耗了!理解Wi-Fi STA的TIM/DTIM,才是优化设备续航的关键
  • 【数据结构】栈与队列全方位对比 + C 语言完整实现
  • 5步颠覆性方案:BilibiliDown让视频下载效率飙升300%
  • 2026版AI论文工具测评:精选8款免费利器,省时降重,高效成稿 - 沁言学术
  • 别再让PCB走线偷走你的电压!手把手教你用开尔文四线法搞定FPGA核心供电
  • FPGA驱动14K超高清屏:MIPI DSI接口的实战解析与点屏全流程
  • 如何用ScanTailor Advanced将扫描文档变身为专业级电子文档?完全开源解决方案
  • 基于STM32freeRTOS的Modbus从机设备数据传输方案
  • 自动化办公三件套:OpenClaw+百川2-13B处理邮件、日历与文档
  • 清华大学重磅发现:AI模型读不懂“符号“,原来它们在“靠蒙“!
  • HoRain云--Vue3条件渲染完全指南
  • Linux 内核中的内存管理优化:从理论到实践
  • 如何用React打造经典Windows XP桌面体验:完整实现指南
  • 原创:黄大年茶思屋难题揭榜第11期|5道核心题精简公开·被退稿求技术指正
  • eFuse电子保险丝:现代电路保护的智能选择
  • 【数据结构】字符串模式匹配:暴力算法与 KMP 算法实现与解析
  • Origin绘图进阶:如何在现有图形上叠加散点图与等高线(附完整操作步骤)
  • PingFangSC字体实战:3个关键决策提升中文界面性能与体验
  • 4步终极指南:用OpenCore Legacy Patcher让老Mac重获新生
  • 解决MicroBlaze程序启动难题:Vivado中bit与elf文件合并的完整流程
  • HoRain云--Vue.js循环渲染完全指南:v-for实战技巧
  • 手把手教你用TI官方方案搭建V-I转换器恒流源(含MOSFET选型指南)
  • WinDiskWriter:突破Mac系统限制的Windows启动盘制作革新工具
  • ISL29125 RGB环境光传感器驱动与嵌入式应用实战
  • 终极指南:Windows APK安装工具完整使用教程
  • 2026年媒体发稿平台首选:传声港新媒体平台三大核心平台赋能企业全域传播 - 博客湾