.NET 磁盘管理-技术方案选型

在家庭以及企业场景下的网络磁盘产品,使用Iscsi均需要对磁盘进行管理。不同Windows版本、安装第三方软件,导致每个C端用户的运行环境不同,对磁盘的管理带来一定的使用干扰

本文介绍下磁盘管理的几种方案以及存在的一些问题

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

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

下面介绍下四者之间的关系以及所依赖的windows系统服务

Windows磁盘管理服务依赖层级

从操作系统角度看,这几种方式编程/操作入口是围绕同一套内核与服务堆栈的不同"壳",完成套娃封装

从高到低,依次列下windows主要的磁盘相关入口和服务

  1. GUI/工具层

MMC - Windows系统磁盘管理工具,如果需要快速查看和操作磁盘分区的话,可以用这个

以及Storage Spaces GUI - Windows系统设置存储管理

这俩个工具主要是使用WMI相关操作来实现

  1. 脚本/命令层

Powershell磁盘管理命令

diskpart磁盘管理命令

CIM磁盘管理命令

  1. API/管理接口层

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

磁盘管理服务:Virtual Disk,VDS进程名称vds.exe

磁盘存储服务:Microsoft Storage Spaces SMP

  1. 内核/驱动/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     }

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 LearnWinioctl.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 - 博客园

相关推荐
William_cl1 天前
C# ASP.NET路由系统全解析:传统路由 vs 属性路由,避坑 + 实战一网打尽
开发语言·c#·asp.net
感谢地心引力1 天前
安卓、苹果手机无线投屏到Windows
android·windows·ios·智能手机·安卓·苹果·投屏
风清扬_jd1 天前
libtorrent-rasterbar-2.0.11编译说明
c++·windows·p2p
虚心低调的tom1 天前
Moltbot 助手在 Windows 10 上安装并连接飞书教程
windows·飞书·moltbot
x***r1512 天前
Putty远程管理软件安装步骤详解(附首次连接教程)
windows
初九之潜龙勿用2 天前
C# 操作Word模拟解析HTML标记之背景色
开发语言·c#·word·.net·office
tod1132 天前
Makefile进阶(上)
linux·运维·服务器·windows·makefile·进程
时光追逐者2 天前
使用 MWGA 帮助 7 万行 Winforms 程序快速迁移到 WEB 前端
前端·c#·.net
老骥伏枥~2 天前
【C# 入门】程序结构与 Main 方法
开发语言·c#
全栈师2 天前
java和C#的基本语法区别
java·开发语言·c#