システムドライブだけでなく、EFIや回復パーティションを含む「物理ディスク全体」の空き容量/使用容量を正しく把握したい――その要求に対し、ボリューム単位のAPIでは届かないことが多々あります。本記事では、WMI(CIM)クラス MSFT_Disk→MSFT_Partition→MSFT_Volume をたどり、VB/.NETから確実に集計する実装と運用の要点を解説します。
課題の整理:なぜ GetDiskFreeSpace(Ex) では不十分なのか
GetDiskFreeSpace/GetDiskFreeSpaceEx は「ボリューム(ドライブレターやマウントポイント)」に対する空き容量を返します。したがって、ドライブレターを持たないEFIや回復パーティションは集計対象から漏れ、さらに複数ボリュームが同一物理ディスクに跨る場合、物理ディスク単位の全体像を正しく示せません。
| 観点 | ボリュームAPI | 物理ディスク集計 |
|---|---|---|
| EFI/回復パーティション | 取得不可(多くはドライブレターなし) | 取得可(ディスク→パーティション→ボリューム) |
| 総容量 | ボリューム単位の容量のみ | ディスク総容量と照合しながら集計可 |
| 空き容量 | ボリュームの空き容量 | 全ボリュームの空き容量合算で把握 |
| 未割り当て領域 | 不明 | ディスク総容量 − 全パーティション容量の差で推定 |
解決方針の全体像
「デバイス(ディスク)」を起点にCIMの関連をたどり、対象ディスク上に存在するあらゆるボリュームの使用状況を合算します。具体的には次の順路です。
- 起点の特定:
root\microsoft\windows\storageでMSFT_Diskを検索し、OSがブートしている物理ディスク(IsBoot = TRUE)を特定。 - ディスク → パーティション:
ASSOCIATORS OF {MSFT_Disk.ObjectId="..."}でMSFT_Partitionを列挙。 - パーティション → ボリューム: 各パーティションから
MSFT_Volumeを関連取得。ドライブレター無しのボリュームも列挙されます。 - 集計: すべてのボリュームの
SizeとSizeRemainingを合算し、TotalSize/TotalFree/TotalUsedを算出。
この流れにより、物理ディスク全体の使用状況(EFIや回復を含む)が再現性高く得られます。
使用クラスと主なプロパティ(一覧)
| クラス | 主なプロパティ | 意味 | 補足 |
|---|---|---|---|
MSFT_Disk | ObjectId, Number, Size, IsBoot, PartitionStyle | 物理ディスクの識別・総容量・ブート判定など | 起点となるオブジェクト。IsBoot=TRUE がシステムディスク。 |
MSFT_Partition | ObjectId, PartitionNumber, Size, GptType, Type | パーティション情報 | EFI/MSR/回復などの判定に活用可能。 |
MSFT_Volume | ObjectId, Size, SizeRemaining, DriveLetter, FileSystemLabel | ボリューム容量と空き容量 | ドライブレター無しも列挙される点が重要。 |
WQLクエリ(確認用)
-- システムが起動している物理ディスク
SELECT * FROM MSFT_Disk WHERE IsBoot = TRUE;
-- ディスク → パーティション
ASSOCIATORS OF {MSFT_Disk.ObjectId=""} WHERE ResultClass = MSFT_Partition;
-- パーティション → ボリューム
ASSOCIATORS OF {MSFT_Partition.ObjectId=""} WHERE ResultClass = MSFT_Volume;
フルコード(VB/.NET Framework 4.8 コンソール)
以下は、管理者権限で実行することを前提に、システムディスク配下のすべてのボリュームを合算して「物理ディスク全体の空き容量/使用容量」を計算する、動作に必要な最小限+αを含む実装です。重複ボリュームの計上を避けるため、ObjectId での重複排除(HashSet)も行っています。
Option Strict On
Imports System
Imports System.Collections.Generic
Imports System.Management
Module Module1
Sub Main()
Console.OutputEncoding = System.Text.Encoding.UTF8
' 管理スコープ(管理者権限での実行が必要)
Dim scope As New ManagementScope("\\.\root\microsoft\windows\storage")
scope.Connect()
' ◆ システムディスク(IsBoot=TRUE)を特定
Dim bootDisk As ManagementObject = Nothing
Using searcher As New ManagementObjectSearcher(scope, New ObjectQuery("SELECT * FROM MSFT_Disk WHERE IsBoot = TRUE"))
Dim col = searcher.Get()
For Each d As ManagementObject In col
bootDisk = d
Exit For ' 複数返ることは通常無いが最初の1件でOK
Next
End Using
If bootDisk Is Nothing Then
Console.WriteLine("システムディスクが見つかりません。管理者権限で実行しているか確認してください。")
Return
End If
Dim diskNumber As UInteger = Convert.ToUInt32(bootDisk("Number"))
Dim diskSize As ULong = Convert.ToUInt64(bootDisk("Size"))
Dim diskObjId As String = CStr(bootDisk("ObjectId"))
Console.WriteLine($"対象ディスク: #{diskNumber}")
Console.WriteLine($"ディスク総容量: {FormatBytes(diskSize)} ({diskSize} bytes)")
Console.WriteLine()
' ◆ ディスク → パーティション
Dim totalVolumeSize As ULong = 0UL
Dim totalVolumeFree As ULong = 0UL
Dim seenVolumeIds As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)
Dim dAssoc As String = $"ASSOCIATORS OF {{MSFT_Disk.ObjectId=""{EscapeWql(diskObjId)}""}} WHERE ResultClass = MSFT_Partition"
Using partSearch As New ManagementObjectSearcher(scope, New ObjectQuery(dAssoc))
Dim parts = partSearch.Get()
For Each part As ManagementObject In parts
Dim partNum As UInteger = 0UI
Dim partSize As ULong = 0UL
Try
partNum = Convert.ToUInt32(part("PartitionNumber"))
Catch
End Try
Try
partSize = Convert.ToUInt64(part("Size"))
Catch
End Try
' ◆ パーティション → ボリューム
Dim partObjId As String = CStr(part("ObjectId"))
Dim vAssoc As String = $"ASSOCIATORS OF {{MSFT_Partition.ObjectId=""{EscapeWql(partObjId)}""}} WHERE ResultClass = MSFT_Volume"
Using volSearch As New ManagementObjectSearcher(scope, New ObjectQuery(vAssoc))
Dim vols = volSearch.Get()
For Each vol As ManagementObject In vols
Dim volId As String = CStr(vol("ObjectId"))
If Not seenVolumeIds.Add(volId) Then
' 同じボリュームを複数パーティションから辿るなどの重複に対処
Continue For
End If
Dim vSize As ULong = 0UL
Dim vFree As ULong = 0UL
Try
vSize = Convert.ToUInt64(vol("Size"))
Catch
End Try
Try
vFree = Convert.ToUInt64(vol("SizeRemaining"))
Catch
End Try
totalVolumeSize += vSize
totalVolumeFree += vFree
Dim driveLetter As String = Convert.ToString(vol("DriveLetter"))
Dim label As String = Convert.ToString(vol("FileSystemLabel"))
If String.IsNullOrEmpty(driveLetter) Then driveLetter = "(no letter)"
Console.WriteLine($"[Volume] {driveLetter} Label={label} Size={FormatBytes(vSize)} Free={FormatBytes(vFree)}")
Next
End Using
Next
End Using
Dim totalUsed As ULong = totalVolumeSize - totalVolumeFree
Console.WriteLine()
Console.WriteLine("=== 集計結果(ボリューム合算) ===")
Console.WriteLine($"合計容量: {FormatBytes(totalVolumeSize)} ({totalVolumeSize} bytes)")
Console.WriteLine($"空き容量: {FormatBytes(totalVolumeFree)} ({totalVolumeFree} bytes)")
Console.WriteLine($"使用容量: {FormatBytes(totalUsed)} ({totalUsed} bytes)")
' ◆ 参考:未割り当て領域を推定(ディスク総容量 − 全パーティション容量)
Dim partitionsTotal As ULong = 0UL
Using partSearch2 As New ManagementObjectSearcher(scope, New ObjectQuery(dAssoc))
Dim parts2 = partSearch2.Get()
For Each part As ManagementObject In parts2
Try
partitionsTotal += Convert.ToUInt64(part("Size"))
Catch
End Try
Next
End Using
Dim unallocated As ULong = 0UL
If diskSize >= partitionsTotal Then
unallocated = diskSize - partitionsTotal
End If
Console.WriteLine($"未割り当て領域(推定): {FormatBytes(unallocated)} ({unallocated} bytes)")
Console.WriteLine()
Console.WriteLine("完了。任意のキーで終了します。")
Console.ReadKey()
End Sub
' WQLの文字列リテラルで二重引用符をエスケープ
Private Function EscapeWql(value As String) As String
If value Is Nothing Then Return ""
Return value.Replace("""", """""")
End Function
' 直感的な単位で表示
Private Function FormatBytes(bytes As ULong) As String
Dim units = New String() {"B", "KiB", "MiB", "GiB", "TiB", "PiB"}
Dim size As Double = bytes
Dim unitIdx As Integer = 0
While size >= 1024 AndAlso unitIdx < units.Length - 1
size /= 1024.0
unitIdx += 1
End While
Return $"{size:0.##} {units(unitIdx)}"
End Function
End Module
ポイント
- 管理者権限:
root\microsoft\windows\storageへのアクセスは管理者権限が必要です(Visual Studio/コンソールを「管理者として実行」)。 - 重複排除: ある種の構成では同一ボリュームが複数のパーティションに関連付く可能性があるため、
ObjectIdによる重複排除が安全です。 - 未割り当て領域: WMIで未割り当て領域は直接は得られないため、ディスク総容量から全パーティション合計を差し引いて推定します。
実行例(想定出力)
[Volume] C Label=OS Size=238.27 GiB Free=121.54 GiB
[Volume] (no letter) Label=SYSTEM Size=100 MiB Free=70 MiB
[Volume] (no letter) Label=WINRE Size=1.03 GiB Free=250 MiB
=== 集計結果(ボリューム合算) ===
合計容量: 239.4 GiB (257000000000 bytes)
空き容量: 121.78 GiB (130800000000 bytes)
使用容量: 117.62 GiB (126200000000 bytes)
未割り当て領域(推定): 0 B (0 bytes)
値は環境により異なります。EFIや回復パーティションを含めて合算される点が、ボリュームAPIとの差です。
任意のディスク番号を指定して取得する(応用)
サーバーではシステムディスク以外の集計ニーズもあります。MSFT_Disk.Number = n で起点を切り替え、同じ手順を適用できます。
SELECT * FROM MSFT_Disk WHERE Number = 1;
実装面では IsBoot = TRUE のクエリ文字列だけ差し替えれば同様に動作します。
全物理ディスクを走査して一覧化する(応用)
全ディスクの「合計/空き/使用」をダッシュボード化したい場合、MSFT_Disk を全件取得し、各ディスクで前述の集計ルーチンを呼び出します。ボリュームの重複排除はディスクごとに独立させてください(ボリュームが複数ディスクに跨る場合は分散構成に注意)。
PowerShellでの等価取得(検証・運用スクリプト向け)
$disk = Get-CimInstance -Namespace root/microsoft/windows/storage -ClassName MSFT_Disk -Filter "IsBoot = True"
$parts = Get-CimAssociatedInstance -InputObject $disk -ResultClassName MSFT_Partition
$vols = foreach ($p in $parts) { Get-CimAssociatedInstance -InputObject $p -ResultClassName MSFT_Volume }
# 重複排除しつつ集計
$unique = $vols | Group-Object ObjectId | ForEach-Object { $_.Group[0] }
$total = ($unique | Measure-Object -Property Size -Sum).Sum
$free = ($unique | Measure-Object -Property SizeRemaining -Sum).Sum
$used = $total - $free
"{0:N0},{1:N0},{2:N0}" -f $total, $free, $used
ジョブスケジューラに登録すれば、定期監視やしきい値超過時の通知の基礎になります。
落とし穴と対策(必読)
- 権限不足: 管理者権限がないと
MSFT_*クラスの取得に失敗します。必ず管理者で実行してください。 - ドライブレター無しボリューム:
MSFT_Volumeの列挙に含まれるため問題ありませんが、UI表示では名称が空になりがちです。FileSystemLabelなどと併記すると識別しやすくなります。 - 重複ボリュームの計上: ストレージスペース/ダイナミックディスク等の構成で、同一ボリュームに複数の関連が貼られる場合があります。
ObjectIdをキーに重複排除しましょう。 - 未割り当て領域の扱い: 直接のWMIプロパティがないため、ディスク総容量 − 全パーティション容量で推定します。計算誤差を避けるため、型は
UInt64(VBのULong)で統一します。 - 仮想化/RAID環境: 物理ディスクの見え方が抽象化される場合があります。監視設計ではハイパーバイザーやRAIDコントローラ側のメトリクスとも整合を取ってください。
- BitLocker: 暗号化有無は容量計算に影響しませんが、回復パーティションの存在が前提の構成では、回復領域も集計されることを理解しておくと良いです。
- OSバージョン:
MSFT_*ストレージクラスは比較的新しいWindowsで提供されます。古いOSでは同等のクラスが不足する場合があるため、対象環境を事前に確認してください。
パフォーマンス最適化のヒント
- スコープの再利用:
ManagementScopeは1回接続して再利用。 - ASSOCIATORSの粒度:
ResultClassを指定し、不要な関連取得を抑制。 - 一括処理: 取得した
ManagementObjectCollectionを都度Usingブロック内で処理し、COMリソースを確実に解放。 - 例外の局所化: プロパティ取得の
Try ... Catchはループ外へ波及させない(ホットパスのオーバーヘッド回避)。
検証・テスト観点(チェックリスト)
| シナリオ | 確認ポイント | 期待結果 |
|---|---|---|
| EFI+回復+OSの標準構成 | EFI/回復が計上されるか | 全ボリューム合算に含まれる |
| ドライブレター無しボリューム | 列挙・集計の可否 | DriveLetter空でも計上される |
| ストレージスペース/ダイナミック | 重複ボリュームの除外 | ObjectIdで重複排除できる |
| 未割り当て領域が存在 | 推定計算の妥当性 | 総容量 − パーティション合計 ≒ 未割り当て |
| 権限不足で実行 | 例外・エラーメッセージ | アクセス権限に関する明確なメッセージ |
よくある拡張要件への対応
- しきい値監視:
TotalFreeが閾値未満でアラート(Windows タスクスケジューラ+イベントログ/メール連携)。 - ログ出力: 時系列監視のため、
csv/jsonにDiskNumber/TotalSize/TotalFree/Timestampを追記。 - GUIツール化: WPFやWinFormsで一覧表を表示。
MSFT_Disk.Numberごとにグリッド行を作成。
トラブルシューティング
| 症状 | 原因候補 | 対処 |
|---|---|---|
| 「システムディスクが見つからない」 | 管理者権限不足/OSサポート外 | 管理者で再実行、対象OSを確認 |
| 容量が合わない | 重複計上/未割り当て領域の存在 | ObjectId重複排除と未割り当て推定の見直し |
| 一部ボリュームが表示されない | マウント解除/オフライン | MSFT_Volumeの状態を確認、オンライン化 |
まとめ
物理ディスク全体の空き容量/使用容量を正確に取得するためには、ボリュームAPIだけでは不十分です。MSFT_Disk を起点に MSFT_Partition、MSFT_Volume へ関連をたどり、すべてのボリューム(ドライブレター無しを含む)を合算する――この手順こそが、EFIや回復パーティションを含めた「ディスク全体」を定量化する唯一の堅牢なアプローチです。本記事のVBコードをベースに、しきい値監視、ログ化、GUI化など、運用要件へ柔軟に発展させてください。

コメント