最近在處理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,核心是:
DeviceTypeDeviceNumberPartitionNumber
它適合基礎磁盤、普通分區、單一設備映射場景。
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
並且在現有單盤模型下,應對多磁盤卷做降級跳過,不要讓特殊卷拖垮整體查詢流程。
這次修復雖然不大,但本質上是把“錯誤的單一映射假設”改成了“按卷類型分流處理”,穩定性會好很多