在家庭以及企業場景下的網絡磁盤產品,使用Iscsi均需要對磁盤進行管理。不同Windows版本、安裝第三方軟件,導致每個C端用户的運行環境不同,對磁盤的管理帶來一定的使用干擾
本文介紹下磁盤管理的幾種方案以及存在的一些問題
對磁盤管理主要有以下操作入口/方式:
- Powershell
- Diskpart
- WMI
- WIN32(IOCTL)
下面介紹下四者之間的關係以及所依賴的windows系統服務
Windows磁盤管理服務依賴層級
從操作系統角度看,這幾種方式編程/操作入口是圍繞同一套內核與服務堆棧的不同“殼”,完成套娃封裝
從高到低,依次列下windows主要的磁盤相關入口和服務
1. GUI/工具層
MMC - Windows系統磁盤管理工具,如果需要快速查看和操作磁盤分區的話,可以用這個
以及Storage Spaces GUI - Windows系統設置存儲管理
這倆個工具主要是使用WMI相關操作來實現
2. 腳本/命令層
Powershell磁盤管理命令
diskpart磁盤管理命令
CIM磁盤管理命令
3. API/管理接口層
WMI服務:Winmgmt(Windows Management Instrumentation),使用Win32_DiskDrive 等
磁盤管理服務:Virtual Disk,VDS進程名稱vds.exe
磁盤存儲服務:Microsoft Storage Spaces SMP
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獲取結果為空:
下面對各個模塊展開介紹下
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協議磁盤
操作磁盤命令,比如初始化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 }
遍歷磁盤列表,4塊盤耗時接近1s:
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
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 }
同樣的遍歷磁盤列表(4塊),首次耗時20ms,二次查詢僅7ms:
封裝WIN32,異常碼只有基礎的Win32Exception異常碼,不像Powershell Storage有相對上層更多的業務異常碼和異常描述那麼好理解。
比如句柄CreateFile失敗,GetLastError異常碼是 0x00000002,轉換Win32Exception描述:“系統找不到指定的文件”。鬼知道是啥問題。。。結合上下文,才知道原來磁盤IsOffline狀態是無法查找卷、也無法創建分區訪問句柄
回到.NET磁盤管理方案選型,
沒有複雜的C端環境的話、僅運維等固定場景,磁盤管理操作可以使用Powersshell
對磁盤操作要求穩定、但又想快速實現功能,較少的磁盤功能調用,推薦WMI
對磁盤操作要求穩定、性能要求高,做產品級的存儲軟件,推薦WIN32
磁盤相關的其它文章:
Windows 本地虛擬磁盤 - 唐宋元明清2188 - 博客園
Windows 網絡存儲ISCSI介紹 - 唐宋元明清2188 - 博客園網絡虛擬存儲 Iscsi實現方案 - 唐宋元明清2188 - 博客園