
2026/07/04 4:08
FreeBSD が私の RAM を食い尽くした
RSS: https://news.ycombinator.com/rss
要約▶
日本語翻訳:
上記に不足している要素と推測された飛躍が特定されているため、改善版を提供します:
サマリー(改善版)
FreeBSD で以前使われていた
btop や htop といったシステム監視ツールは、廃止されたカーネル統計量に依存していたため、不正確なメモリ使用量の表示を行っていましたが、具体的には有効な ZFS ARC キャッシュを誤って除外したり、高メモリシステムでは 32 ビット整数の巻き戻りエラーが発生してしまい、5 GB 以上のメモリーが存在しながらも利用可能な RAM が低いと示すような誤った報告を引き起こしていました。この核心的な問題は、FreeBSD 12.0 以降のダミー sysctl 値を使用していたこと(実際のページキャッシュカウントを報告しなくなったこと)と、ZFS ARC の存在下でアクティブ、非アクティブ、回収可能なメモリバッターを区別することができないことに由来していました。近代オペレーティングシステムは物理メモリーをページ(通常 4 KiB)に分割し、PQ_ACTIVE, PQ_INACTIVE, PQ_LAUNDRY, PQ_UNSWAPPABLE, PQ_NONE などのページキューを使用してカテゴリー化します。FreeBSD では、これらのカテゴリーは PQ_NONE 255, PQ_INACTIVE 0, PQ_ACTIVE 1, PQ_LAUNDRY 2, PQ_UNSWAPPABLE 3 などの定数によって定義されています。top はメモリーをアクティブ(ユーザーランドプロセス)、非アクティブ(最近アクセスされていない)、洗濯(スワップに書込むために用意されるもの)、ワイアド(カーネル使用中かつスワップ不能、ディスクキャッシュを含む)、フリー(未使用)として報告しますが、ZFS では独自の ARC(Adaptive Replacement Cache)を使用して最近使用されたデータをキャッシュし、カーネルの一般的なキャッシングメカニズムを回避します。ZFS ARC の統計量は kstat.zfs.misc.arcstats.* を介してアクセス可能であり、gnumfmt を使用して人間が読みやすい単位で表示できます。異なるツールは異なるヒューリスティックを使用しています:fastfetch はフリーメモリーをフリー + 非アクティブ + キャッシュとして計算し、使用メモリーを合計 - フリーとして計算します。当初 btop は利用可能なメモリーを合計 - アクティブ - ワイアドとして計算し、ARC をワイアド報告から除外していました。一方 htop はワイアド、アクティブ、洗濯をその使用メモリー計算に含めます。著者の最初の混乱は、コミュニティプロジェクトと共有した後の fastfetch の結果で、ユーザーが fastfetch と btop の間に不一致があると指摘したことから生じました。著者は OS internals の専門家ではありませんでしたが、数週間の独立した研究により、複数のオープンソースイニシアチブに重要な修正が導入されました。テストには、FreeBSD 13.5-RELEASE(ZFS)と 15.1-RELEASE(UFS)を実行する仮想マシンを作成すること、および FreeBSD 15.0-RELEASE を実行する ThinkPad X230 でのテストが含まれ、ファイルシステム全体でキャッシュバケットの問題が確認されました。著者は dd を使用してランダムファイルを生成し、それを読み返すことや、4 GB を割り当てて解放する C プログラムを使用して、btop などのツールにおけるキャッシュの成長と動態を観察しました。これらの問題に対処するために、著者は btop で uint64_t 変数をスイッチすることで巻き戻りを防止し、かつ ZFS ARC ステータス (arcsize - arcmin) を使用して回収されたキャッシュを計算するように vfs.bufspace に加えて修正を行いました。htop に提出した PR では、ARC をワイアドから差し引きし、キャッシュ/バッタークラスを統合する変更を行い、初期の議論後に変更がマージされました。また、著者の入力に従って fastfetch は当初 v_cache_count を使用していましたが、現在は ARC をサポートする他のプラットフォームとの並列で ZFS ARC 検出も含めるようになりました。これらの改善により、メモリー成分の正確な追跡が可能となり、管理者が誤って重要なディスクキャッシュをクリアしたり、総メモリー制限を誤解したりすることを防ぐことが可能になり、開発環境と生产環境の両方でパフォーマンスボトルネックの診断のための信頼性の高い基盤を提供します。今後の作業では、DragonFly BSD の調査を含め、これらの修正された基準に合わせてその報告方法を一致させることを検討しています。著者の貢献は、btop, htop, fastfetch という 3 つの重大なプロジェクトに対するパッチを生み出し、コミュニティ全体で FreeBSD メモリー使用量の監視精度を向上させることに寄与しました。この旅路は、若者の頃に「Operating Systems: Design and Implementation」を読みながら最初につくりかけられ、初期版のオリジナルのコピーを購入することから始まりました。本文
FreeBSD サーバー移行と RAM 使用量報告の不一致:原因調査と修正記
先月、Ubuntu から FreeBSD へのサーバー移行記事について議論が進みました。特にメモリ(RAM)使用量の報告方法に対し、「fastfetch」と「btop」では大きく数字が異なるという指摘を受け、OS の内部構造を解明する調査を行いました。
本記事では、なぜ OS は「空きメモリ」を増やして見せかけるのか、ツール間の表示不一致の正体、そしてbtop に存在した重大なバグと修正プロセスについて詳述します。
1. RAM 使用量は定義が難しい
「Linux(または FreeBSD)が RAM を食った」と言うのは、システムが正常に機能している証拠です。CPU キャッシュのように、OS はディスクデータの読み込みを高速化するために RAM を活用しています。このキャッシュは揮発性を持ち、必要であればいつでも解放されます。
仮想メモリとページ管理
現代の OS は「仮想メモリ(VM)」を採用し、物理メモリのページ(4KiB)を以下のようなキューに分類・管理します。
| キュー名 | 定義 |
|---|---|
(0) | 一定時間アクセスされていないページ(解放候補) |
(1) | 現在利用中のページ |
(2) | スワップ対象の待機リスト |
(3) | スワップ不可能な領域(カーネル用など) |
この管理ルールにより、OS は「未使用」であるはずの RAM を見つけると、必要な時にディスクへ一時保存(スワップ)できるように設定します。その後再度アクセスされると、再び RAM へ戻されます。
メモリのカテゴリと実質的な空き
top コマンドなどで表示されるカテゴリには以下の意味があります。
| カテゴリ | 説明 |
|---|---|
| active | ユーザープロセスで現在アクティブに利用されているページ |
| inactive | アクセスされておらず、インアクティブキューに移されたページ |
| laundry | スワップ対象の待機リスト。空き不足時にここからデータを読み込む |
| wired | カーネル管理領域(, )。ディスクキャッシュを含む |
| free | 未割り当てかつ未使用の純粋な空きメモリ |
重要なポイント
- **「inactive」も実質的な「free」**です。アクセス直前であれば即座に再利用可能です。
- 「wired」の中にもディスクキャッシュが含まれるため、実質的に利用可能なメモリとして扱われます。
- この仕組みにより、「実際に使われている量」と「空きメモリ」の境界は曖昧になります。
2. ディスクキャッシュ(ARC)について
FreeBSD のデフォルトファイルシステムである ZFS は、ARC(Adaptive Replacement Cache) という高度なキャッシュ機構を採用しています。直近にアクセスされたデータを RAM に保持することで読み込み性能を飛躍的に向上させます。
- ARC はシステムが利用可能なメモリが増えるほど大きくなります。
- カーネル自体も同様の機構を持ちますが、ZFS の ARC は独自に動作します。
ARC ステータスの確認方法
カーネルパラメータ
kstat.zfs.misc.arcstats.* を確認可能です。
# 全てのパラメータを表示 sysctl kstat.zfs.misc.arcstats # 特に重要な 3 つのパラメータを抽出表示 sysctl -n kstat.zfs.misc.arcstats.size # 現在のキャッシュサイズ sysctl -n kstat.zfs.misc.arcstats.c_min # キャッシュ最小値 sysctl -n kstat.zfs.misc.arcstats.c_max # キャッシュ最大値
出力例(
gnumfmt で可読化):
$ sysctl -n kstat.zfs.misc.arcstats.size | gnumfmt --to=iec 3.1G
この数値は
top コマンドでも詳細に確認できます。
3. なぜ fastfetch と btop は異なる表示をするのか?
各ツールがメモリ使用量を表示する際、「どの値を『使用済み』とみなすか」という判定基準(ヒューリスティック)が異なります。
ツールの計算ロジック比較
| ツール | 計算式・論理 | 結果の傾向 |
|---|---|---|
| fastfetch | | キャッシュを含めるため、**「使用メモリ率が高い(例:82%)」**と表示。ZFS の ARC を「cache」として含める。 |
| btop (旧) | | キャッシュを見なさないため、「使用メモリ率が低く見える(例:7%)」。 |
| htop | | 中間的な表示を行う。バー末尾に合計使用量を表示。 |
問題の一端:キャッシュ値が 0 であること
以前のスクリプトでは、fastfetch が表示した「cache」の値が常に 0 でした。これは btop と同じく、古くなったレガシーな sysctl パラメータを参照していたためです。
4. btop の FreeBSD 向けメモリ報告は重大な誤り
調査の結果、btop(および関連ツール)の FreeBSD 側の実装には致命的な欠陥が複数存在することが判明しました。
欠陥①:32 ビット整数による桁捨て(Overflow)
btop は FreeBSD の libc を介して
sysctl でメモリ情報を取得しています。しかし、以下のコードで変数が u_int(符号なし 32 ビット整数)として宣言されています。
// [旧実装] 重大なバグ箇所 int mib[4]; u_int memActive, memWire, cachedMem, freeMem; // <--- u_int (最大約 4GB) size_t len; // ... sysctl 呼び出し省略 ... memActive *= Shared::pageSize; // 超過すると桁捨てが発生
- 32 ビット整数の限界: $2^{32} - 1 \approx 4.29$ GiB
- 問題発生: RAM が 4GB を超えると、値が負数や異常な小さな数値(例:4.42 GiB → 422 MiB)として表示されてしまいます。
- 結論: btop のメモリ計算ロジックは FreeBSD 環境では桁捨てにより致命的に間違っていることが確認されました。
欠陥②:キャッシュ情報の欠落 (vm.stats.vm.v_cache_count
)
vm.stats.vm.v_cache_count「Cached memory」の値が常に 0 である理由は、btop が参照している
v_cache_count というパラメータ自体の問題にあります。
$ sysctl -n vm.stats.vm.v_cache_count 0 $ sysctl -d vm.stats.vm.v_cache_count vm.stats.vm.v_cache_count: Dummy for compatibility
- FreeBSD 12.0 以降、このパラメータは**「互換性用のダミー値(Dummy for compatibility)」**として機能しており、実際のキャッシュ情報を返しません。
- btop は FreeBSD の最初の実装からこの非推奨なパラメータを参照し続けており、その理由さえ解明されていませんでした。
5. 修正策の開発と改善
調査に基づき、以下の 2 つの主要な修正を行いました。
修正①:データ型の昇格
メモリ量を保持する変数を
u_int(32 ビット)から **uint64_t(64 ビット)**に変更し、4GB 超え時の桁捨て問題を解消しました。
修正②:キャッシュの正しく取得
ZFS の ARC キャッシュと一般的なファイルシステムキャッシュを適切に区別するロジックを実装しました。
HTop のコメントを参考に、「ZFS の場合、ARC は
buffers にカウントされず wired に含まれる」ため、計算時に関連部分から引き下げる必要があります。
btop への適用された修正コード(一部):
// cached メモリの正しく取得処理(vfs.bufspace と ZFS ARC を組み合わせる) uint64_t cachedBytes = 0; // uint64_t で宣言(旧: u_int から変更) // vfs.bufspace (ファイルシステムキャッシュ) if (sysctlnametomib("vfs.bufspace", mib, &len) == 0) { uint64_t bufSpace = 0; // sysctl 呼び出し省略 ... cachedBytes += bufSpace; } // ZFS ARC の取得(ZFS 環境のみ有効) if (sysctlnametomib("kstat.zfs.misc.arcstats.size", mib, &len) == 0) { uint64_t arcSize = 0; // sysctl 呼び出し省略 ... // ARC の可変部分をキャッシュとして計上(最小サイズより大きい場合のみ差分を計算) cachedBytes += (arcSize > arcMin) ? (arcSize - arcMin) : 0; } // free メモリの取得 // sysctl(v_free_count)... // バイト単位の計算(uint64_t で桁捨て防止) uint64_t activeBytes = (uint64_t)memActive * Shared::pageSize; uint64_t wireBytes = (uint64_t)memWire * Shared::pageSize; uint64_t freeBytes = (uint64_t)freeMem * Shared::pageSize; // rawUsed: 実質的な使用メモリ(active + wired) uint64_t rawUsed = activeBytes + wireBytes; // usedBytes: キャッシュを含めた総合「使用」量を正しく計算 // (キャッシュが over-report するのを防ぐために、rawUsed との比較で上限を設ける) uint64_t usedBytes = (cachedBytes < rawUsed) ? rawUsed - cachedBytes : 0; mem.stats.at("used") = usedBytes; mem.stats.at("available") = Shared::totalMem - usedBytes; mem.stats.at("cached") = cachedBytes; mem.stats.at("free") = freeBytes;
テスト方法
キャッシュの挙動を確認するため、以下のようなテストを行いました。
の数千ファイルを開く(効率が悪いため非推奨)。/usr/src
を使用してランダムデータを大量に読み込む。dd- C プログラムで RAM を割り当て・解放するループを実行。
これにより、キャッシュが拡張される様子や、解放される際のメモリ変化をリアルタイムで確認できました。
6. fastfetch のケース
fastfetch も同様に
v_cache_count からデータを取ろうとしており、提出した PR は却下されました(コメントなし)。しかし、その後以下の事実が判明しました。
- 快挙: fastfetch は FreeBSD/NetBSD/Linux/ZFS などのマルチプラットフォーム対応を視野に入れ、ARC キャッシュを検出する機能を追加する形で改修が進んでいます。
- 影響: 私の提案の一部がこの改修に取り入れられ、最終的に「正確なメモリ表示」が実現しました。
DragonFly BSD の注記 DragonFly BSD へのパッチも提出しましたが、そちらはまだ独自の表示方針(キャッシュを含む)を維持しているようです。今後さらに調査予定です。
おわりに
今回の調査を通じて以下の成果を得ることができました。
- 深い理解: FreeBSD の仮想メモリ内部構造とキャッシュ機構について詳細に学びました。
- コミュニティ貢献:
- btop: 重大なバグの修正パッチを提出。
- fastfetch/htop: 類似問題への対応を促し、結果的に改善につながりました。
- Total: 主要なオープンソースプロジェクトに 3 つのパッチを提出する機会を得ました。
- 技術的回顧: 「OS を自作したい」という夢から始まるキャリアパスを再確認しました。「MINIX 本」(通称:OSDI)を読み込む中で感じた「50,000 行という規模」への衝撃は、現代の OS 開発においても色あせていません。
FreeBSD のメモリ管理は複雑ながら非常に優秀な設計です。今後も引き続き FreeBSD とオープンソースコミュニティに貢献してまいります。