博客 / 詳情

返回

.NET 磁盤管理-技術方案選型

在家庭以及企業場景下的網絡磁盤產品,使用Iscsi均需要對磁盤進行管理。不同Windows版本、安裝第三方軟件,導致每個C端用户的運行環境不同,對磁盤的管理帶來一定的使用干擾

本文介紹下磁盤管理的幾種方案以及存在的一些問題

對磁盤管理主要有以下操作入口/方式:

  1. Powershell
  2. Diskpart
  3. WMI
  4. WIN32(IOCTL)

下面介紹下四者之間的關係以及所依賴的windows系統服務

Windows磁盤管理服務依賴層級

從操作系統角度看,這幾種方式編程/操作入口是圍繞同一套內核與服務堆棧的不同“殼”,完成套娃封裝

從高到低,依次列下windows主要的磁盤相關入口和服務

1. GUI/工具層

MMC - Windows系統磁盤管理工具,如果需要快速查看和操作磁盤分區的話,可以用這個

image

以及Storage Spaces GUI - Windows系統設置存儲管理

imageimage

這倆個工具主要是使用WMI相關操作來實現

2. 腳本/命令層

Powershell磁盤管理命令

diskpart磁盤管理命令

CIM磁盤管理命令

3. API/管理接口層

WMI服務:Winmgmt(Windows Management Instrumentation),使用Win32_DiskDrive 等

image

磁盤管理服務:Virtual Disk,VDS進程名稱vds.exe

image

磁盤存儲服務:Microsoft Storage Spaces SMP

image

4. 內核/驅動/IOCTL層

Storage Management Provider:系統組件,不是單獨服務可見
IOCTL: Win32API、DevicerIoControl

磁盤類驅動(disk.sys)、卷管理器(volmgr/vdsci)、文件系統驅動(NTFS/ReFS)

而上面説的四種方案,依賴的底層服務:

PowerShell 基於 WMI / Storage Management API封裝,依賴的組件最多:Winmgmt、Microsoft Storage Spaces SMP、Storage Service、VDS等
WMI/CIM 有部分是走 VDS / Storage API,有部分直接調用底層驅動,依賴:VDS服務、Winmgmt服務

diskpart 內部是調用 VDS / Storage API / IOCTL,依賴相對較少:VDS服務等

Win32 IOCTL 是最底層(用户態可達)的接口,不依賴上層框架

比如下方的WMI服務不存在,會導致powershell磁盤查詢不到,WMI磁盤查詢不到,但diskpart訪問正常:

 1 PS C:\Users\yudong04> Get-Disk
 2 PS C:\Users\yudong04> Get-CimInstance -Namespace root/Microsoft/Windows/Storage -ClassName MSFT_Disk
 3 PS C:\Users\yudong04> diskpart
 4 
 5 Microsoft DiskPart 版本 10.0.26100.1150
 6 
 7 Copyright (C) Microsoft Corporation.
 8 在計算機上: GIH-D-24762
 9 
10 DISKPART> list disk
11 
12   磁盤 ###  狀態           大小     可用     Dyn  Gpt
13   --------  -------------  -------  -------  ---  ---
14   磁盤 0    聯機             3726 GB  1024 KB        *
15   磁盤 1    聯機             3726 GB  1024 KB        *
16   磁盤 2    聯機             2794 GB      0 B        *
17   磁盤 3    聯機              931 GB      0 B        *
18   磁盤 4    聯機              465 GB  1024 KB        *
19   磁盤 5    聯機             1863 GB      0 B
20   磁盤 6    聯機             7452 GB      0 B        *

還有Microsoft Storage Spaces SMP服務被第三方軟件禁用,導致Powershell Get-Disk獲取結果為空:

image

下面對各個模塊展開介紹下

Powershell磁盤管理

上面説了,PowerShell使用 Storage Management API + 新的 WMI/CIM 類,磁盤命令本質是對這些 WMI 類的包裝。層級如下:

PowerShell cmdlet
-> MSFT_* WMI 類 (CIM)、WMI服務Winmgmt
-> Storage Management Provider
-> 內核驅動 (disk.sys, partmgr.sys, volmgr.sys)
-> 設備硬件

powershell有以下查找主要命令,

Get-Disk - 查找磁盤

Get-Partition - 查找分區

Get-Volume - 查找卷

Get-Disk | Where-Object -FilterScript {  $_.BusType -Eq "iSCSI" -and $_.SerialNumber -Eq "8fa461f8-9436-4260-8191-789b23859757"} - 查找指定Iscsi協議磁盤

image

操作磁盤命令,比如初始化GPT磁盤:Initialize-Disk -PartitionStyle GPT -PassThru | New-Partition -UseMaximumSize | Format-Volume -FileSystem:NTFS -NewFileSystemLabel:測試盤 -Confirm:$false -Force

Powershell命令因易用性,非常適合腳本自動化、用户級的使用。但非常與用户環境有關,換個用户或換台機器就經常表現不同,比如:卡很久、超時、直接報錯、磁盤盤就是查詢不到

幾個原因:

WMI / CIM 調用超時

  • WMI 服務卡住、存儲驅動響應慢
  • 網絡/防火牆導致遠程調用超時

硬件IO超時

  • 壞盤 / 壞 U 盤 / USB 擴展塢質量問題
  • 大量重新嘗試 I/O 導致操作整體拖得很長

具體場景,發現公司內部某個部門發生powershell命令超時概率很多,因為這些設備都在跑軟件壓力測試。。。導致磁盤獲取命令,很容易超時

還有些特殊情況,服務異常出現的情況比較多。如WMI服務,以下是修復成功案例:

 1 PS C:\Users\yudong04> Get-WmiObject Win32_OperatingSystem
 2 Get-WmiObject : 無效類 “Win32_OperatingSystem”
 3 所在位置 行:1 字符: 1
 4 + Get-WmiObject Win32_OperatingSystem
 5 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 6     + CategoryInfo          : InvalidType: (:) [Get-WmiObject], ManagementException
 7     + FullyQualifiedErrorId : GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCommand
 8 
 9 PS C:\Users\yudong04> net stop winmgmt /y
10 Windows Management Instrumentation 服務正在停止.
11 Windows Management Instrumentation 服務已成功停止。
12 
13 PS C:\Users\yudong04> winmgmt /resetrepository
14 WMI 存儲庫已重置
15 
16 PS C:\Users\yudong04> Get-WmiObject Win32_OperatingSystem
17 
18 
19 SystemDirectory : C:\WINDOWS\system32
20 Organization    : Online Game Dept
21 BuildNumber     : 26100
22 RegisteredUser  : Windows 用户
23 SerialNumber    : 00329-00000-00003-AA238
24 Version         : 10.0.26100

還有Microsoft Storage Spaces SMP服務,如果Get-Disk拿不到磁盤,定位客户問題發現很大可能是這個服務異常了。重啓一下即可

WMI/CIM磁盤管理

WMI相關命令,需要拆分為倆部分:WIN32_*經典類,以及MSFT_*新的StorageWMI類

經典類:

  • Win32_DiskDrive
  • Win32_DiskPartition
  • Win32_LogicalDisk
  • Win32_Volume

早期設計,很多是通過內核 API + IOCTL 和 VDS 實現。主要用於查詢,修改操作有限

依賴服務:Winmgmt、RPCSS(RPC服務)、以及少量依賴VDS

StorageWmi類

  • MSFT_Disk
  • MSFT_Partition
  • MSFT_Volume
  • MSFT_StoragePool
  • MSFT_VirtualDisk

這是Windows8之後的新存儲管理WMI接口,詳見官網文檔:Storage Management API Classes - Windows drivers | Microsoft Learn, 依賴層級:

WMI (MSFT_* 類)
-> Storage Management Provider
-> IOCTL -> disk.sys / partmgr.sys / ...

具體依賴的服務:Winmgmt(WMI 服務)

WMI 是“管理數據模型 + 接口”,本身不是一個磁盤管理“方案”,而是很多方案的基礎接口。相對Powershell Storage管理,算是比較穩定和依賴較少的了

直接使用.NET通過WMI獲取詳細的磁盤列表數據,代碼如下:

 1     public OperateResult<List<LocalDisk>> GetDisks()
 2     {
 3         var disks = new List<LocalDisk>();
 4         try
 5         {
 6             // Win32_DiskDrive: 物理磁盤
 7             using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive"))
 8             using (var driveCollection = searcher.Get())
 9             {
10                 foreach (ManagementObject drive in driveCollection)
11                 {
12                     var diskInfo = new LocalDisk();
13 
14                     // 1. 磁盤編號 PhysicalDriveN
15                     // Win32_DiskDrive.DeviceID 一般為 "\\.\PHYSICALDRIVE0"
16                     var deviceId = (drive["DeviceID"] as string) ?? string.Empty;
17                     var diskNumber = ParsePhysicalDriveNumber(deviceId);
18                     diskInfo.Number = diskNumber;
19 
20                     // 2. 序列號 (不同廠商格式不統一;有時需要 Win32_PhysicalMedia)
21                     diskInfo.SerialNumber = (drive["SerialNumber"] as string)?.Trim() ?? string.Empty;
22 
23                     // 3. DeviceName
24                     diskInfo.DeviceName = (drive["Model"] as string)?.Trim() ?? string.Empty;
25 
26                     // 4. 只讀/在線狀態(WMI 並沒有非常標準的字段,這裏用粗略映射)
27                     //   Win32_DiskDrive.Status: "OK" / "Error" / "Degraded" ...
28                     diskInfo.IsOffline = GetOffline(diskNumber);
29 
30                     // 沒有直接 readonly 標記,先默認為 false,
31                     // 如需更精確可以通過 Win32_Volume 或 DeviceIoControl 獲取。
32                     diskInfo.IsReadOnly = GetReadonly(diskNumber);
33 
34                     // 5. 總線類型(沒有 STORAGE_BUS_TYPE 枚舉,使用 InterfaceType 粗略映射)
35                     var interfaceType = (drive["InterfaceType"] as string)?.Trim();
36                     diskInfo.BusType = MapBusType(interfaceType, diskInfo.DeviceName);
37 
38                     // 6. 磁盤容量 (字節 -> GB)
39                     // Win32_DiskDrive.Size 為字節數(string)
40                     if (drive["Size"] != null && long.TryParse(drive["Size"].ToString(), out long sizeBytes))
41                     {
42                         diskInfo.DiskSize = sizeBytes;
43                     }
44 
45                     // 7. 獲取掛載點及已用容量,通過 3 張 WMI 關聯表:
46                     // Win32_DiskDrive -> Win32_DiskDriveToDiskPartition -> Win32_DiskPartition ->
47                     // Win32_LogicalDiskToPartition -> Win32_LogicalDisk
48                     FillMountPathsAndUsedSize(diskInfo, drive);
49                     disks.Add(diskInfo);
50 
51                     diskInfo.Tag = GetVolumeLabel(diskInfo.MountPaths.FirstOrDefault());
52                 }
53             }
54 
55             return OperateResult<List<LocalDisk>>.ToSuccess(disks.OrderBy(i => i.Number).ToList());
56         }
57         catch (Exception ex)
58         {
59             return OperateResult<List<LocalDisk>>.ToError(ex.Message);
60         }
61     }

附帶的一些屬性獲取函數:

  1     private int ParsePhysicalDriveNumber(string deviceId)
  2     {
  3         // "\\.\PHYSICALDRIVE0" -> 0
  4         if (string.IsNullOrWhiteSpace(deviceId))
  5             return -1;
  6 
  7         var upper = deviceId.ToUpperInvariant();
  8         var idx = upper.LastIndexOf("PHYSICALDRIVE", StringComparison.Ordinal);
  9         if (idx < 0) return -1;
 10 
 11         var numPart = upper.Substring(idx + "PHYSICALDRIVE".Length);
 12         if (int.TryParse(numPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
 13             return num;
 14 
 15         return -1;
 16     }
 17 
 18     private StorageBusType MapBusType(string interfaceType, string deviceName)
 19     {
 20         if (string.IsNullOrEmpty(interfaceType))
 21             return StorageBusType.Unknown;
 22 
 23         switch (interfaceType.ToUpperInvariant())
 24         {
 25             case "SCSI":
 26                 if (deviceName.Contains("SCSI"))
 27                 {
 28                     return StorageBusType.Iscsi;
 29                 }
 30                 return StorageBusType.Scsi;
 31             case "IDE":
 32             case "ATA":
 33                 return StorageBusType.Ata;
 34             case "USB":
 35                 return StorageBusType.Usb;
 36             // 可根據需要擴展映射
 37             default:
 38                 return StorageBusType.Unknown;
 39         }
 40     }
 41 
 42     /// <summary>
 43     /// 填充 MountPaths(盤符)和 DiskUsedSize(GB)
 44     /// </summary>
 45     private OperateResult FillMountPathsAndUsedSize(LocalDisk diskInfo, ManagementObject diskDrive)
 46     {
 47         long totalUsedBytes = 0;
 48 
 49         // 通過 Win32_DiskDriveToDiskPartition 關聯到分區
 50         using (var partitionRel = new ManagementObjectSearcher(
 51                    "ASSOCIATORS OF {Win32_DiskDrive.DeviceID='" + diskDrive["DeviceID"] +
 52                    "'} WHERE AssocClass = Win32_DiskDriveToDiskPartition"))
 53         using (var partitions = partitionRel.Get())
 54         {
 55             foreach (ManagementObject partition in partitions)
 56             {
 57                 // 通過 Win32_LogicalDiskToPartition 關聯到邏輯磁盤(盤符)
 58                 using (var logicalRel = new ManagementObjectSearcher(
 59                            "ASSOCIATORS OF {Win32_DiskPartition.DeviceID='" + partition["DeviceID"] +
 60                            "'} WHERE AssocClass = Win32_LogicalDiskToPartition"))
 61                 using (var logicalDisks = logicalRel.Get())
 62                 {
 63                     foreach (ManagementObject logicalDisk in logicalDisks)
 64                     {
 65                         // 計算已用空間
 66                         if (logicalDisk["Size"] != null &&
 67                             logicalDisk["FreeSpace"] != null &&
 68                             long.TryParse(logicalDisk["Size"].ToString(), out long volSize) &&
 69                             long.TryParse(logicalDisk["FreeSpace"].ToString(), out long free))
 70                         {
 71                             totalUsedBytes += (volSize - free);
 72                         }
 73                     }
 74                 }
 75             }
 76         }
 77 
 78         diskInfo.DiskUsedSize =totalUsedBytes;
 79 
 80         try
 81         {
 82             var paths = GetAccessPaths(diskInfo.Number);
 83             var filtedPaths = paths.Where(i => !i.StartsWith(@"\\?\Volume")).ToList();
 84             diskInfo.MountPaths = filtedPaths;
 85             return OperateResult.ToSuccess();
 86         }
 87         catch (Exception e)
 88         {
 89            return OperateResult.ToError(e);
 90         }
 91     }
 92     /// <summary>
 93     /// 獲取磁盤的所有訪問路徑
 94     /// </summary>
 95     private List<string> GetAccessPaths(int diskNumber)
 96     {
 97         ManagementScope scope = new ManagementScope(@"\\.\root\Microsoft\Windows\Storage");
 98         scope.Connect();
 99         string query = $"SELECT * FROM MSFT_Partition WHERE DiskNumber = {diskNumber}";
100         using ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, new ObjectQuery(query));
101         var pathList = new List<string>();
102         foreach (var partition in searcher.Get().Cast<ManagementObject>())
103         {
104             // 獲取 AccessPaths 屬性(數組)
105             var accessPaths = partition["AccessPaths"] as string[];
106             if (accessPaths == null)
107             {
108                 continue;
109             }
110             pathList.AddRange(accessPaths);
111         }
112         return pathList;
113     }
View Code

遍歷磁盤列表,4塊盤耗時接近1s:

image

DiskPart磁盤管理

diskpart 是 原生 Win32 命令行工具,內部大致通過:

  • VDS / Storage Management API(老系統)
  • 新系統上,一部分功能由新的 Storage API 接管
  • 再往下還是 IOCTL 調用內核驅動

調用層級如下,diskpart.exe
-> VDS / Storage Management API
-> 內核驅動 (disk.sys, partmgr.sys, volmgr.sys)
-> 設備硬件

diskpart常用命令列表:

  • list disk
  • select disk 1
  • detail disk
  • list partition
  • list volume

image

DiskPart對WMI並不強依賴,基本上依賴服務就一個Virtual Disk了,操作也比較簡單。但缺點也比較明顯,訪問性能比較差、磁盤操作使用Powersehell調用diskpart命令基本也在s級以上

Win32 IOCTL磁盤管理

IOCTL是指通過直接調用 Windows API DeviceIoControl 對磁盤、卷、文件句柄發送控制碼:

  • IOCTL_DISK_*
  • IOCTL_STORAGE_*
  • FSCTL_*(針對文件系統)

IOCTL文檔:deviceIoControl 函數 (ioapiset.h) - Win32 apps | Microsoft Learn、Winioctl.h 標頭 - Win32 apps | Microsoft Learn

磁盤管理詳細文檔:磁盤管理 - Win32 apps | Microsoft Learn

WIN32方案,不依賴 VDS / WMI 等上層框架

僅依賴:

  • Win32 子系統 + 內核 I/O 棧
  • 對應的設備驅動(disk.sys, storport.sys, nvme.sys 等)

需要基於WIN32API一層層處理細節,比如獲取磁盤列表:

 1     /// <summary>
 2     /// 通過磁盤編號獲取序列號SerialNumber
 3     /// </summary>
 4     /// <param name="diskNumber">磁盤編號</param>
 5     /// <param name="volumeMaps"></param>
 6     /// <returns></returns>
 7     private OperateResult<LocalDisk> GetDiskInfoByDiskNumber(int diskNumber, Dictionary<int, List<string>> volumeMaps)
 8     {
 9         //逐個嘗試 PhysicalDrive0..N
10         string physicalDrive = @"\\.\PhysicalDrive" + diskNumber;
11         IntPtr hDisk = CreateFile(
12             physicalDrive,
13             GENERIC_READ,
14             FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
15             IntPtr.Zero,
16             OPEN_EXISTING,
17             0,
18             IntPtr.Zero);
19         try
20         {
21             // 不存在這個物理盤(或者無權限),忽略此異常
22             if (hDisk == INVALID_HANDLE_VALUE)
23             {
24                 return OperateResult<LocalDisk>.ToSuccess();
25             }
26             var diskInfo = new LocalDisk();
27             diskInfo.Number = diskNumber;
28 
29             //獲取磁盤基礎信息
30             var getDiskPropertiesResult = GetDiskProperties(hDisk);
31             if (!getDiskPropertiesResult.Success)
32             {
33                 return OperateResult<LocalDisk>.ToError($"Get disk {physicalDrive} properties failed, {getDiskPropertiesResult.Message}", getDiskPropertiesResult.Exception, getDiskPropertiesResult.Code);
34             }
35             var diskProperties = getDiskPropertiesResult.Data;
36             diskInfo.SerialNumber = diskProperties.SerialNumber;
37             diskInfo.DeviceName = diskProperties.DeviceName;
38             diskInfo.BusType = diskProperties.BusType;
39 
40             //是否只讀/聯機
41             var diskAttributesResult = GetDiskAttributes(hDisk);
42             if (!diskAttributesResult.Success)
43             {
44                 return OperateResult<LocalDisk>.ToError($"Get disk {diskProperties.DeviceName} attributes failed, {diskAttributesResult.Message}", diskAttributesResult.Exception, diskAttributesResult.Code);
45             }
46             var diskStorageAttributes = diskAttributesResult.Data;
47             diskInfo.IsReadOnly = diskStorageAttributes.IsReadOnly;
48             diskInfo.IsOffline = diskStorageAttributes.IsOffline;
49 
50             //磁盤容量
51             var getDiskSizeResult = GetDiskSize(hDisk);
52             diskInfo.DiskSize = getDiskSizeResult.Data;
53 
54             //獲取分區信息
55             var partitionInfoResult = GetPartitionInfo(hDisk);
56             if (!partitionInfoResult.Success)
57             {
58                 return OperateResult<LocalDisk>.ToError($"Get disk {diskProperties.DeviceName} partition failed, {partitionInfoResult.Message}", partitionInfoResult.Exception, partitionInfoResult.Code);
59             }
60             var diskPartitionInfo = partitionInfoResult.Data;
61             diskInfo.PartitionStyle = (DiskPartitionStyle)diskPartitionInfo.PartitionStyle;
62             diskInfo.PartitionCount = diskPartitionInfo.PartitionCount;
63             //基礎數據區分大小
64             diskInfo.DiskAllocateSize = diskPartitionInfo.Partitions.FirstOrDefault(i => i.PartitionType.ToUpper() == "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7")?.PartitionLength ?? 0;
65 
66             //掛載路徑
67             if (volumeMaps.TryGetValue(diskNumber, out var mounts) && mounts != null)
68             {
69                 diskInfo.MountPaths = mounts;
70             }
71             //獲取卷標名稱
72             if (diskInfo.MountPaths.Any())
73             {
74                 //通過任意一個mountPath獲取
75                 var mountPath = diskInfo.MountPaths.First();
76                 var getVolumeInfoResult = GetVolumeInfo(mountPath);
77                 diskInfo.Tag = getVolumeInfoResult.Data?.VolumeLabel ?? string.Empty;
78                 diskInfo.FileSystemType = getVolumeInfoResult.Data?.FileSystemType ?? string.Empty;
79             }
80             //磁盤已使用大小
81             if (diskInfo.MountPaths.Any())
82             {
83                 long diskUsedSize = 0L;
84                 //通過所有mountPath相加,獲取磁盤已使用大小
85                 foreach (var mountPath in diskInfo.MountPaths)
86                 {
87                     var usageByMountPathResult = GetDiskSizeUsageByMountPath(mountPath);
88                     diskUsedSize += usageByMountPathResult.Data?.UsedBytes ?? 0;
89                 }
90                 diskInfo.DiskUsedSize = diskUsedSize;
91             }
92             return OperateResult<LocalDisk>.ToSuccess(diskInfo);
93         }
94         finally
95         {
96             CloseHandle(hDisk);
97         }
98     }

其中磁盤屬性獲取細節,就不展示了:

  1         /// <summary>
  2         /// 獲取所有磁盤
  3         /// </summary>
  4         /// <returns></returns>
  5         public OperateResult<List<LocalDisk>> GetDisks()
  6         {
  7             // 1. 先拿卷 -> 卷所屬的物理磁盤號 + 盤符/掛載點
  8             var getVolumesResult = GetAllVolumeMountPaths();
  9             if (!getVolumesResult.Success)
 10             {
 11                 return OperateResult<List<LocalDisk>>.ToError(getVolumesResult.Message, getVolumesResult.Exception, getVolumesResult.Code);
 12             }
 13             var volumeMaps = getVolumesResult.Data;
 14 
 15             // 2. 獲取磁盤列表
 16             var diskList = new List<LocalDisk>();
 17             // 根據卷信息推一個最大磁盤號,同時至少查詢16 個
 18             int maxDiskNumberCount = Math.Max(volumeMaps.Max(i => i.Key), 16);
 19             for (int diskNumber = 0; diskNumber <= maxDiskNumberCount; diskNumber++)
 20             {
 21                 var getDiskResult = GetDiskInfoByDiskNumber(diskNumber, volumeMaps);
 22                 if (!getDiskResult.Success)
 23                 {
 24                     //結束查詢
 25                     if (diskNumber == maxDiskNumberCount - 1)
 26                     {
 27                         return getDiskResult.ToResult<List<LocalDisk>>();
 28                     }
 29                     //繼續查詢其它
 30                     continue;
 31                 }
 32                 //可能為空
 33                 if (getDiskResult.Data == null)
 34                 {
 35                     continue;
 36                 }
 37                 diskList.Add(getDiskResult.Data);
 38             }
 39 
 40             return OperateResult<List<LocalDisk>>.ToSuccess(diskList);
 41         }
 42 
 43         /// <summary>
 44         /// 獲取所有磁盤卷的掛載路徑信息
 45         /// <remarks>通過枚舉卷,並使用 IOCTL_STORAGE_GET_DEVICE_NUMBER 映射到設備號。</remarks>
 46         /// </summary>
 47         /// <returns>PhysicalDiskNumber -> 對應的所有掛載路徑(盤符、掛載點)</returns>
 48         private OperateResult<Dictionary<int, List<string>>> GetAllVolumeMountPaths()
 49         {
 50             var diskDict = new Dictionary<int, List<string>>();
 51 
 52             int maxPath = 1024;
 53             var volNameSb = new StringBuilder(maxPath);
 54             IntPtr findVolumeHandle = FindFirstVolumeW(volNameSb, (uint)volNameSb.Capacity);
 55             try
 56             {
 57                 if (findVolumeHandle == (IntPtr)(-1))
 58                 {
 59                     return OperateResult<Dictionary<int, List<string>>>.ToSuccess(diskDict);
 60                 }
 61                 while (true)
 62                 {
 63                     string volumeName = volNameSb.ToString();
 64                     // volumeName: \\?\Volume{GUID}\
 65 
 66                     // 打開卷設備
 67                     string volumePathForDevice = volumeName.TrimEnd('\\'); // \\?\Volume{GUID}
 68                     IntPtr hVolume = CreateFile(
 69                         volumePathForDevice,
 70                         0, // 只需要 IOCTL,不讀寫
 71                         FILE_SHARE_READ | FILE_SHARE_WRITE,
 72                         IntPtr.Zero,
 73                         OPEN_EXISTING,
 74                         0,
 75                         IntPtr.Zero);
 76 
 77                     uint? diskNumber = null;
 78 
 79                     if (hVolume != (IntPtr)(-1))
 80                     {
 81                         // 取 STORAGE_DEVICE_NUMBER
 82                         uint size = (uint)Marshal.SizeOf<STORAGE_DEVICE_NUMBER>();
 83                         IntPtr outBuf = Marshal.AllocHGlobal((int)size);
 84                         try
 85                         {
 86                             if (DeviceIoControl(
 87                                     hVolume,
 88                                     IOCTL_STORAGE_GET_DEVICE_NUMBER,
 89                                     IntPtr.Zero,
 90                                     0,
 91                                     outBuf,
 92                                     size,
 93                                     out uint bytesReturned,
 94                                     IntPtr.Zero))
 95                             {
 96                                 STORAGE_DEVICE_NUMBER devNum = Marshal.PtrToStructure<STORAGE_DEVICE_NUMBER>(outBuf);
 97                                 // DeviceType 為 FILE_DEVICE_DISK(0x07) 一般表示物理磁盤
 98                                 diskNumber = devNum.DeviceNumber;
 99                             }
100                         }
101                         finally
102                         {
103                             Marshal.FreeHGlobal(outBuf);
104                             CloseHandle(hVolume);
105                         }
106                     }
107 
108                     if (diskNumber.HasValue)
109                     {
110                         if (!diskDict.TryGetValue((int)diskNumber.Value, out var list))
111                         {
112                             list = new List<string>();
113                             diskDict[(int)diskNumber.Value] = list;
114                         }
115                         // 獲取卷的掛載路徑列表(可能有多個)
116                         var getMountPathsResult = GetMountPathsForVolume(volumeName);
117                         if (!getMountPathsResult.Success)
118                         {
119                             return OperateResult<Dictionary<int, List<string>>>.ToError($"磁盤{diskNumber}卷掛載路徑獲取失敗, {getMountPathsResult.Message}", getMountPathsResult.Exception, getMountPathsResult.Code);
120                         }
121                         foreach (var mp in getMountPathsResult.Data)
122                         {
123                             if (!list.Contains(mp))
124                                 list.Add(mp);
125                         }
126                     }
127 
128                     // 下一卷
129                     volNameSb.Clear();
130                     volNameSb.EnsureCapacity(maxPath);
131 
132                     if (!FindNextVolumeW(findVolumeHandle, volNameSb, (uint)volNameSb.Capacity))
133                     {
134                         int err = Marshal.GetLastWin32Error();
135                         // ERROR_NO_MORE_FILES
136                         if (err == 18)
137                             break;
138 
139                         return OperateResult<Dictionary<int, List<string>>>.ToWin32Error("query disk volumes failed", err);
140                     }
141                 }
142             }
143             catch (Exception ex)
144             {
145                 return OperateResult<Dictionary<int, List<string>>>.ToError("query disk volumes error", ex);
146             }
147             finally
148             {
149                 FindVolumeClose(findVolumeHandle);
150             }
151             return OperateResult<Dictionary<int, List<string>>>.ToSuccess(diskDict);
152         }
153 
154         /// <summary>
155         /// 獲取分區信息
156         /// </summary>
157         /// <param name="hDisk"></param>
158         /// <returns></returns>
159         private OperateResult<DiskPartitionInfo> GetPartitionInfo(IntPtr hDisk)
160         {
161             int outSize = Marshal.SizeOf<DRIVE_LAYOUT_INFORMATION_EX>() + 128 * 64; // 給多一點空間
162             IntPtr outBuffer = Marshal.AllocHGlobal(outSize);
163 
164             try
165             {
166                 if (!DeviceIoControl(
167                         hDisk,
168                         IOCTL_DISK_GET_DRIVE_LAYOUT_EX,
169                         IntPtr.Zero,
170                         0,
171                         outBuffer,
172                         (uint)outSize,
173                         out _,
174                         IntPtr.Zero))
175                 {
176                     return OperateResult<DiskPartitionInfo>.ToWin32Error("DeviceIoControl.IOCTL_DISK_GET_DRIVE_LAYOUT_EX failed", Marshal.GetLastWin32Error());
177                 }
178 
179                 // 只取結構開頭
180                 var layout = Marshal.PtrToStructure<DRIVE_LAYOUT_INFORMATION_EX_HEADER>(outBuffer);
181                 var partitionInfo = new DiskPartitionInfo()
182                 {
183                     PartitionCount = (int)layout.PartitionCount,
184                     PartitionStyle = layout.PartitionStyle,
185                     DiskId = layout.Gpt.DiskId,
186                     StartingUsableOffset = layout.Gpt.StartingUsableOffset,
187                     UsableLength = layout.Gpt.UsableLength,
188                     MaxPartitionCount = layout.Gpt.MaxPartitionCount
189                 };
190                 // 指向第一個 PARTITION_INFORMATION_EX 的指針:
191 
192                 IntPtr pCurrent = IntPtr.Add(outBuffer, Marshal.SizeOf<DRIVE_LAYOUT_INFORMATION_EX>());
193                 int partSize = Marshal.SizeOf<PARTITION_INFORMATION_EX>();
194                 for (int i = 0; i < layout.PartitionCount; i++)
195                 {
196                     var part = Marshal.PtrToStructure<PARTITION_INFORMATION_EX>(pCurrent);
197                     var item = new PartitionEntryInfo
198                     {
199                         PartitionNumber = (int)part.PartitionNumber,
200                         StartingOffset = part.StartingOffset,
201                         PartitionLength = part.PartitionLength,
202                         PartitionType = part.Gpt.PartitionType.ToString(),
203                         PartitionName = part.Gpt.Name
204                     };
205 
206                     partitionInfo.Partitions.Add(item);
207                     pCurrent = IntPtr.Add(pCurrent, partSize);
208                 }
209 
210                 return OperateResult<DiskPartitionInfo>.ToSuccess(partitionInfo);
211             }
212             finally
213             {
214                 Marshal.FreeHGlobal(outBuffer);
215             }
216         }
217 
218         /// <summary>
219         /// 獲取磁盤靜態屬性
220         /// </summary>
221         /// <param name="hDisk"></param>
222         /// <returns></returns>
223         private OperateResult<DiskStorageProperty> GetDiskProperties(IntPtr hDisk)
224         {
225             var storageProperties = new DiskStorageProperty();
226             var query = new STORAGE_PROPERTY_QUERY
227             {
228                 PropertyId = STORAGE_PROPERTY_ID.StorageDeviceProperty,
229                 QueryType = STORAGE_QUERY_TYPE.PropertyStandardQuery,
230                 AdditionalParameters = new byte[1]
231             };
232             uint allocSize = 1024;
233             IntPtr buffer = Marshal.AllocHGlobal((int)allocSize);
234             try
235             {
236                 if (!DeviceIoControl(
237                         hDisk,
238                         IOCTL_STORAGE_QUERY_PROPERTY,
239                         ref query,
240                         (uint)Marshal.SizeOf<STORAGE_PROPERTY_QUERY>(),
241                         buffer,
242                         allocSize,
243                         out var bytesReturned,
244                         IntPtr.Zero))
245                 {
246                     //讀取失敗
247                     int err = Marshal.GetLastWin32Error();
248                     if (err == ERROR_INSUFFICIENT_BUFFER && bytesReturned > allocSize)
249                     {
250                         // 重新分配更大緩衝區
251                         Marshal.FreeHGlobal(buffer);
252                         allocSize = bytesReturned;
253                         buffer = Marshal.AllocHGlobal((int)allocSize);
254                         if (!DeviceIoControl(
255                                 hDisk,
256                                 IOCTL_STORAGE_QUERY_PROPERTY,
257                                 ref query,
258                                 (uint)Marshal.SizeOf<STORAGE_PROPERTY_QUERY>(),
259                                 buffer,
260                                 allocSize,
261                                 out bytesReturned,
262                                 IntPtr.Zero))
263                         {
264                             //重新分配緩衝區,讀取失敗
265                             return OperateResult<DiskStorageProperty>.ToWin32Error("DeviceIoControl.IOCTL_STORAGE_QUERY_PROPERTY execute failed after adjust buffer size", Marshal.GetLastWin32Error());
266                         }
267                     }
268                     else
269                     {
270                         return OperateResult<DiskStorageProperty>.ToWin32Error("DeviceIoControl.IOCTL_STORAGE_QUERY_PROPERTY execute failed", err);
271                     }
272                 }
273 
274                 // 至少要包含 Version/Size/幾個 offset
275                 if (bytesReturned < 24)
276                     return OperateResult<DiskStorageProperty>.ToError($"DeviceIoControl.IOCTL_STORAGE_QUERY_PROPERTY execute success but bytesReturned {bytesReturned} is lower than 24");
277 
278                 // --- 讀取頭部固定字段(按官方 C 結構手工偏移)---
279                 // Size    (ULONG) at offset 0x04
280                 uint size = (uint)Marshal.ReadInt32(buffer, 4);
281                 if (size > bytesReturned) size = bytesReturned;
282 
283                 // 磁盤序列號,同 Get-Disk 的 SerialNumber
284                 uint serialOffset = (uint)Marshal.ReadInt32(buffer, 0x18);
285                 string serialRaw = ReadAnsiStringSafe(buffer, size, serialOffset);
286                 string serialClean = CleanSerialString(serialRaw);
287                 storageProperties.SerialNumber = serialClean;
288 
289                 // 磁盤廠商/名稱相關
290                 uint vendorOffset = (uint)Marshal.ReadInt32(buffer, 0x0C);
291                 uint productOffset = (uint)Marshal.ReadInt32(buffer, 0x10);
292                 uint revisionOffset = (uint)Marshal.ReadInt32(buffer, 0x14);
293                 storageProperties.Vendor = ReadAnsiStringSafe(buffer, size, vendorOffset);
294                 storageProperties.Product = ReadAnsiStringSafe(buffer, size, productOffset);
295                 storageProperties.Version = ReadAnsiStringSafe(buffer, size, revisionOffset);
296                 storageProperties.DeviceName = $"{storageProperties.Vendor} {storageProperties.Product}";
297                 // BusType
298                 uint busTypeOffset = (uint)Marshal.ReadInt32(buffer, 0x1C);
299                 storageProperties.BusType = Enum.IsDefined(typeof(StorageBusType), (int)busTypeOffset)
300                     ? (StorageBusType)busTypeOffset
301                     : StorageBusType.Unknown;
302                 return OperateResult<DiskStorageProperty>.ToSuccess(storageProperties);
303             }
304             catch (Exception ex)
305             {
306                 return OperateResult<DiskStorageProperty>.ToError(ex);
307             }
308             finally
309             {
310                 Marshal.FreeHGlobal(buffer);
311             }
312         }
313 
314         /// <summary>
315         /// 獲取磁盤大小(Bytes)
316         /// </summary>
317         /// <param name="hDisk"></param>
318         /// <returns></returns>
319         public OperateResult<long> GetDiskSize(IntPtr hDisk)
320         {
321             // 用一個足夠大的緩衝區,一般 1024 字節足夠
322             const int bufferSize = 1024;
323             IntPtr buffer = Marshal.AllocHGlobal(bufferSize);
324             try
325             {
326                 bool ok = DeviceIoControl(
327                     hDisk,
328                     IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
329                     IntPtr.Zero,
330                     0,
331                     buffer,
332                     (uint)bufferSize,
333                     out var bytesReturned,
334                     IntPtr.Zero);
335                 if (!ok)
336                     return OperateResult<long>.ToError("DeviceIoControl.IOCTL_DISK_GET_DRIVE_GEOMETRY_EX failed", Marshal.GetLastWin32Error());
337                 if (bytesReturned < Marshal.SizeOf<DISK_GEOMETRY_EX>())
338                     return OperateResult<long>.ToSuccess(0);
339 
340                 var geomEx = Marshal.PtrToStructure<DISK_GEOMETRY_EX>(buffer);
341                 return OperateResult<long>.ToSuccess(geomEx.DiskSize);
342             }
343             catch (Exception e)
344             {
345                 return OperateResult<long>.ToError(e);
346             }
347             finally
348             {
349                 Marshal.FreeHGlobal(buffer);
350             }
351         }
352 
353         /// <summary>
354         /// 獲取磁盤擴展屬性
355         /// </summary>
356         /// <param name="hDisk"></param>
357         /// <returns></returns>
358         private OperateResult<DiskStorageAttribues> GetDiskAttributes(IntPtr hDisk)
359         {
360             try
361             {
362                 int getSize = Marshal.SizeOf<GET_DISK_ATTRIBUTES>();
363                 var getAttr = new GET_DISK_ATTRIBUTES
364                 {
365                     Version = (uint)getSize, // 關鍵:Version = sizeof(GET_DISK_ATTRIBUTES)
366                     Reserved1 = 0,
367                     Attributes = 0
368                 };
369 
370                 if (!DeviceIoControl_DiskAttributes(
371                         hDisk,
372                         IOCTL_DISK_GET_DISK_ATTRIBUTES,
373                         ref getAttr,
374                         (uint)getSize,
375                         ref getAttr,
376                         (uint)getSize,
377                         out _,
378                         IntPtr.Zero))
379                 {
380                     return OperateResult<DiskStorageAttribues>.ToWin32Error("IOCTL_DISK_GET_DISK_ATTRIBUTES 失敗", Marshal.GetLastWin32Error());
381                 }
382                 //磁盤擴展屬性
383                 var diskStorageAttributes = new DiskStorageAttribues();
384                 diskStorageAttributes.IsOffline = (getAttr.Attributes & DISK_ATTRIBUTE_OFFLINE) != 0;
385                 diskStorageAttributes.IsReadOnly = (getAttr.Attributes & DISK_ATTRIBUTE_READ_ONLY) != 0;
386                 return OperateResult<DiskStorageAttribues>.ToSuccess(diskStorageAttributes);
387             }
388             catch (Exception ex)
389             {
390                 return OperateResult<DiskStorageAttribues>.ToError(ex);
391             }
392         }
393 
394         /// <summary>
395         /// 通過任意掛載路徑(盤符、目錄掛載點、Volume GUID)獲取卷大小與使用量
396         /// </summary>
397         private OperateResult<DiskSizeUsage> GetDiskSizeUsageByMountPath(string mountPath)
398         {
399             if (string.IsNullOrWhiteSpace(mountPath))
400             {
401                 return OperateResult<DiskSizeUsage>.ToError($"parameter {nameof(mountPath)} is empty");
402             }
403 
404             // 確保路徑末尾有反斜槓對某些場景更穩妥
405             if (!mountPath.EndsWith("\\"))
406                 mountPath += "\\";
407 
408             if (!GetDiskFreeSpaceExW(mountPath,
409                     out var freeAvailable,
410                     out var totalBytes,
411                     out var totalFreeBytes))
412             {
413                 return OperateResult<DiskSizeUsage>.ToError("GetDiskFreeSpaceExW failed", Marshal.GetLastWin32Error());
414             }
415 
416             return OperateResult<DiskSizeUsage>.ToSuccess(new DiskSizeUsage((long)totalBytes, (long)totalFreeBytes));
417         }
418 
419         /// <summary>
420         /// 通過掛載路徑獲取卷信息
421         /// </summary>
422         /// <param name="mountPath">盤符, e.g. "E:\\"</param>
423         /// <returns></returns>
424         private OperateResult<VolumeInfo> GetVolumeInfo(string mountPath)
425         {
426             var volumeName = new StringBuilder(256);
427             var fileSystemType = new StringBuilder(256);
428 
429             if (!mountPath.EndsWith("\\"))
430                 mountPath += "\\";
431             var success = GetVolumeInformationW(
432                 mountPath,
433                 volumeName, volumeName.Capacity,
434                 out _, out _, out _,
435                 fileSystemType, fileSystemType.Capacity);
436             if (!success)
437             {
438                 int err = Marshal.GetLastWin32Error();
439                 return OperateResult<VolumeInfo>.ToWin32Error($"GetVolumeInformationW get {mountPath} volume info failed", err);
440             }
441 
442             var volumeInfo = new VolumeInfo()
443             {
444                 VolumeLabel = volumeName.ToString(),
445                 FileSystemType = fileSystemType.ToString()
446             };
447             return OperateResult<VolumeInfo>.ToSuccess(volumeInfo);
448         }
449 
450         /// <summary>
451         /// 通過掛載路徑獲取磁盤信息
452         /// <para>先獲取磁盤列表,再篩選</para>
453         /// </summary>
454         /// <param name="mountPath"></param>
455         /// <returns></returns>
456         public OperateResult<LocalDisk> GetDiskByMountPath(string mountPath)
457         {
458             var getDisksResult = GetDisks();
459             if (!getDisksResult.Success)
460             {
461                 return getDisksResult.ToResult<LocalDisk>();
462             }
463 
464             var iscsiDisks = getDisksResult.Data.FirstOrDefault(i => i.MountPaths.Contains(mountPath));
465             return OperateResult<LocalDisk>.ToSuccess(iscsiDisks);
466         }
View Code

同樣的遍歷磁盤列表(4塊),首次耗時20ms,二次查詢僅7ms:

image

封裝WIN32,異常碼只有基礎的Win32Exception異常碼,不像Powershell Storage有相對上層更多的業務異常碼和異常描述那麼好理解。

比如句柄CreateFile失敗,GetLastError異常碼是 0x00000002,轉換Win32Exception描述:“系統找不到指定的文件”。鬼知道是啥問題。。。結合上下文,才知道原來磁盤IsOffline狀態是無法查找卷、也無法創建分區訪問句柄

 

回到.NET磁盤管理方案選型,

沒有複雜的C端環境的話、僅運維等固定場景,磁盤管理操作可以使用Powersshell

對磁盤操作要求穩定、但又想快速實現功能,較少的磁盤功能調用,推薦WMI

對磁盤操作要求穩定、性能要求高,做產品級的存儲軟件,推薦WIN32

 

磁盤相關的其它文章:

Windows 本地虛擬磁盤 - 唐宋元明清2188 - 博客園

Windows 網絡存儲ISCSI介紹 - 唐宋元明清2188 - 博客園

網絡虛擬存儲 Iscsi實現方案 - 唐宋元明清2188 - 博客園

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

發佈 評論

Some HTML is okay.