プログラミング
FreeBSDが私のRAMを食った
FreeBSD ate my RAM (crocidb.com)
要約
この記事は、FreeBSDにおけるメモリ使用量の表示がなぜ分かりにくいのかを掘り下げています。オペレーティングシステムがパフォーマンス向上のためにディスクキャッシュをRAMに積極的に利用する仕組みと、fastfetchやbtopといったツールがそれぞれ異なるヒューリスティックに基づいてメモリ使用量を報告するため、表示にばらつきが生じる理由を解説しています。特に、btopがFreeBSDでのキャッシュメモリの扱いにおいて、他のツールと異なる計算方法をしている点が指摘されています。
全文翻訳
先月、サイトサーバーを古いUbuntuサーバーからFreeBSDに移行した旅について投稿しました。Hacker Newsの何人かのユーザーが、fastfetchの結果を示した際に、btopと比較してメモリ使用量に混乱していると述べ、fastfetchの方がおそらく正確だろうとコメントしたことに気づきました。
私はその深淵に飛び込み、現代のオペレーティングシステムで空きメモリや使用済みメモリを報告することが、見かけよりも複雑である理由を理解しようと決めました。
別のユーザーはLinux ate my RAMを共有し、Linuxでも同様の効果について簡単な説明を提供しています。そして、FreeBSDについても簡単な答えを知りたいなら、OSは全体的なパフォーマンスを向上させるためにディスクから可能な限りのものをRAMにキャッシュするため、使用量は時に奇妙に見えることがありますが、そのキャッシュは揮発性であり、より多くのメモリが必要になった場合には解放されるからです。もう少し長い説明が欲しい場合は、読み進めてください。
しかし、まず簡単な免責事項を述べます。私はオペレーティングシステムの内部、特にFreeBSDの専門家ではありません。これは、私が自由時間にこの分野で数週間にわたって調査した内容の書き起こしです。特に間違っている点を見つけたら、コメントしてください。知識の共有は思いやりです!
メモリ使用量の定義は難しい
Linux ate my RAMの全体的なポイントは、未使用のRAMは無駄なRAMであると説明することです。
CPUがRAMの内容をキャッシュするように、CPUはそのRAMの内容に素早くアクセスできるため、RAMはディスクデータをキャッシュしてシステムのユーザーエクスペリエンスを向上させます。
そのキャッシュがどのように機能するかは少し複雑ですが、その前に、カーネルがRAMをどのように管理しているかを理解することが重要です。
ほとんどの現代のオペレーティングシステムは仮想メモリ(VM)システムを持っています。
これは基本的に物理メモリを(通常は)4KiBのページに分割するものです。
各ページは異なるキューに追加されるため、カーネルはそれらを操作して、すべてのプロセスが必要なときにメモリを確実に確保し、システム全体が不足時にも機能し続けるようにします。
例えば、スワップメモリです。
スワップメモリがどのように使用されるかについては、それがディスク上の別個の領域であり、必要に応じてRAMの一部を一時的に格納するということ以外、私は正確には考えたことがありませんでした。
しかし、要約すると、OSは割り当てられたRAMがあまり使用されていないと認識すると、より多くのメモリが要求された場合にディスクに格納できるように設定します。
そのページが所有するプログラムによって再び要求されると、RAMに移動されます。
すべてのOSには、ページを管理するための異なるセットのルールとルールがあります。
FreeBSDでは、ページキューのタイプは次のとおりです。
#define PQ_NONE 255
#define PQ_INACTIVE 0
#define PQ_ACTIVE 1
#define PQ_LAUNDRY 2
#define PQ_UNSWAPPABLE 3
#define PQ_COUNT 4
これはsys/vm/vm_page.hで見つけることができます。
Linux、OpenBSD、NetBSD、DragonFlyBSDなど、他のすべてのUnixベースのシステムも同様のものを持っています。
topを見ると、メモリ使用量を示すだけでなく、いくつかのカテゴリに分割されていることがわかります。
topは、メモリ、スワップ、ディスクキャッシュの各セクションを詳細に報告します。
active: アクティブなページは、(主に)ユーザーランドプロセスによって積極的に使用されているページです。
inactive: 一定期間プロセスによってアクセスされていないページは、inactiveキューに移動されます。
laundry: これはスワップに書き込まれるページのキューです。システムがフリーキューにないスペースを割り当てる必要がある場合、inactiveページをこのキューに移動します。
wired: これはPQ_NONE、PQ_UNSWAPPABLEのメモリであり、カーネル自体が使用しておりVMによって管理されていないメモリです。
free: 純粋に使用されていないメモリです。
inactiveだったメモリがlaundryに入り、ディスク(スワップ)に書き込まれ、再びその所有プログラムによって要求された場合、ディスクからinactiveに、そして最終的に再びactiveに取得されます。
そして、正確にどれだけのメモリが使用されており、どれだけが空いているかを判断するのがそれほど簡単ではない理由が見えてきました。
freeキューのメモリは確実に空ですが、inactiveキューのメモリも空だと主張できます。なぜなら、カーネルがより多くのメモリを要求するときはいつでも解放されるため、回収可能だからです。
Wiredメモリはほとんどロックされていますが、そこにディスクキャッシュが入るため、wiredにある部分的なものも回収可能であり、「空」と見なすことができます!
ディスクキャッシュ
現在、FreeBSDのデフォルトファイルシステムであるZFSには、最近使用されたデータをメモリにキャッシュしてディスクからの繰り返し読み取りを改善する専用システムであるARC(Adaptive Replacement Cache)があります。
システムがより多くのメモリを要求すると、そのキャッシュは縮小します。
カーネル自体にもこのキャッシュを行うメカニズムがありますが、ARCはそれをバイパスします。
そのすべての統計情報は、カーネルパラメータkstat.zfs.misc.arcstats.*を介してアクセスできます。
sysctlを使用して、すべてを取得できます。
sysctl kstat.zfs.misc.arcstats
これは文字通り利用可能なすべてのパラメータを表示しますが、今ではこれだけが重要です。
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にも、さらに詳細に表示されます。
これはZFS用ですが、FreeBSDで他のファイルシステムを実行することもできます。
なぜfastfetchとbtopは異なるレポートをするのか?
さて、興味深い部分に来ました。
これらのツールや、htopなどの他の多くのツールは、ユーザー(またはシステム管理者)がシステムの状況を把握できるように、メモリ使用量を報告しようとします。
そのため、すべてがヒューリスティックを選択する必要があります。つまり、何を使用済みメモリと呼ぶかを決定する必要があります。
そして、その違いのすべては、それらが異なるヒューリスティックを持っているという事実から来ています。
私は各ツールのソースコードを掘り下げて、それらがどのように決定するかを調べました。
fastfetchは次のように行います。
free memory = free + inactive + cache*
used memory = total - free memory
そのキャッシュについては後ほど!
私の古いThinkPad X230で、FreeBSD 15.0-RELEASEを実行している場合、次のようになります。
82% の使用済みメモリ!
一方、btopは次のように行います。
available memory = total memory - active - wired
free memory = free
used memory = active + wired
fastfetchと同時に実行すると、次のようになりました。
わずか7%の使用済み?!
さらに興味深いことに、htopもチェックしましたが、メモリカテゴリをそれらのバーで個別に報告する一方で、バーの最後に使用済みメモリを表示します。
4.49G/5.69G
このヒューリスティックを使用します。
used memory = wired + active + laundry
次に、すべてのヒューリスティックを一度に表示するPythonスクリプトを作成しました。
ここで見つけることができます。
Pastedimage20260701215700.png
btopを除いて、それは非常に間違っていますが、正しく見えます。
しかし、注意深く見ているなら、以前共有したスクリーンショットのキャッシュ値も空であることに気づいたでしょう。
それに気づくのに本当に数週間かかりました。
そこで、さらにコードを掘り下げ始めました。
btopのメモリレポートはFreeBSDではかなり間違っています。
ソースコードで、特にsrc/freebsd/btop_collect.cppでメモリ情報を取得している部分を見ると、次のようになります。
int mib[4];
u_int memActive, memWire, cachedMem, freeMem;
size_t len;
len = 4;
sysctlnametomib("vm.stats.vm.v_active_count", mib, &len);
len = sizeof(memActive);
sysctl(mib, 4, &(memActive), &len, nullptr, 0);
memActive *= Shared::pageSize;
len = 4;
sysctlnametomib("vm.stats.vm.v_wire_count", mib, &len);
len = sizeof(memWire);
sysctl(mib, 4, &(memWire), &len, nullptr, 0);
memWire *= Shared::pageSize;
mem.stats.at("used") = memWire + memActive;
mem.stats.at("available") = Shared::totalMem - memActive - memWire;
len = sizeof(cachedMem);
len = 4;
sysctlnametomib("vm.stats.vm.v_cache_count", mib, &len);
sysctl(mib, 4, &(cachedMem), &len, nullptr, 0);
cachedMem *= Shared::pageSize;
mem.stats.at("cached") = cachedMem;
len = sizeof(freeMem);
len = 4;
sysctlnametomib("vm.stats.vm.v_free_count", mib, &len);
sysctl(mib, 4, &(freeMem), &len, nullptr, 0);
freeMem *= Shared::pageSize;
mem.stats.at("free") = freeMem;
これはsysctl(FreeBSDのlibcに含まれている)を使用し、vm.stats.vm.*から直接各キューのページ数を取得します。
アクティブにはvm.stats.vm.v_active_countを使用し、wiredはvm.stats.vm.v_wire_countを使用します。クールです。
次に、ページ数をShared::pageSizeで乗算してバイト量を処理します。