.NET 磁盘管理-技术方案选型
【声明】CSDN只做转发不做时时更新,最新博客请关注博客园 .NET 阻止Windows关机以及阻止失败的一些原因 - 唐宋元明清2188 - 博客园
在家庭以及企业场景下的网络磁盘产品,使用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 }
附带的一些属性获取函数:

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 }
其中磁盘属性获取细节,就不展示了:

View Code
同样的遍历磁盘列表(4块),首次耗时20ms,二次查询仅7ms:

封装WIN32,异常码只有基础的Win32Exception异常码,不像Powershell Storage有相对上层更多的业务异常码和异常描述那么好理解。
比如句柄CreateFile失败,GetLastError异常码是 0x00000002,转换Win32Exception描述:“系统找不到指定的文件”。鬼知道是啥问题。。。结合上下文,才知道原来磁盘IsOffline状态是无法查找卷、也无法创建分区访问句柄
回到.NET磁盘管理方案选型,
没有复杂的C端环境的话、仅运维等固定场景,磁盘管理操作可以使用Powersshell
对磁盘操作要求稳定、但又想快速实现功能,较少的磁盘功能调用,推荐WMI
对磁盘操作要求稳定、性能要求高,做产品级的存储软件,推荐WIN32
磁盘相关的其它文章:
Windows 本地虚拟磁盘 - 唐宋元明清2188 - 博客园
更多推荐
所有评论(0)