博客 / 詳情

返回

.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() 整體失敗
  • 卷枚舉邏輯不會因為“跳過卷”而卡死

也就是説,原來是一個特殊卷拖垮全部磁盤查詢,現在變成了特殊卷按能力降級處理,普通磁盤查詢保持可用。

三塊磁盤查詢結果:

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

並且在現有單盤模型下,應對多磁盤卷做降級跳過,不要讓特殊卷拖垮整體查詢流程。

這次修復雖然不大,但本質上是把“錯誤的單一映射假設”改成了“按卷類型分流處理”,穩定性會好很多

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.