在家庭以及企业场景下的网络磁盘产品,使用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\yudong> Get-Disk
- 2 PS C:\Users\yudong> Get-CimInstance -Namespace root/Microsoft/Windows/Storage -ClassName MSFT_Disk
- 3 PS C:\Users\yudong> 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\yudong> 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\yudong> net stop winmgmt /y
- 10 Windows Management Instrumentation 服务正在停止.
- 11 Windows Management Instrumentation 服务已成功停止。
- 12
- 13 PS C:\Users\yudong> winmgmt /resetrepository
- 14 WMI 存储库已重置
- 15
- 16 PS C:\Users\yudong> 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 }
复制代码 附带的一些属性获取函数:
View Code遍历磁盘列表,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 }
复制代码 View Code同样的遍历磁盘列表(4块),首次耗时20ms,二次查询仅7ms:
封装WIN32,异常码只有基础的Win32Exception异常码,不像Powershell Storage有相对上层更多的业务异常码和异常描述那么好理解。
比如句柄CreateFile失败,GetLastError异常码是 0x00000002,转换Win32Exception描述:“系统找不到指定的文件”。鬼知道是啥问题。。。结合上下文,才知道原来磁盘IsOffline状态是无法查找卷、也无法创建分区访问句柄
回到.NET方案选型,
没有复杂的C端环境的话,磁盘管理操作可以使用Powersshell
对磁盘操作要求稳定、但又想快速实现功能,推荐WMI
对磁盘操作要求稳定、性能要求高,推荐WIN32
磁盘相关的其它文章:
Windows 本地虚拟磁盘 - 唐宋元明清2188 - 博客园
Windows 网络存储ISCSI介绍 - 唐宋元明清2188 - 博客园
网络虚拟存储 Iscsi实现方案 - 唐宋元明清2188 - 博客园
出处:http://www.cnblogs.com/kybs0/让学习成为习惯,假设明天就有重大机遇等着你,你准备好了么本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |