基礎●●●○○

仮想メモリ

Virtual Memoryかそうメモリ

各プロセスに連続した専用メモリ空間があるように見せかける仕組み。隔離と物理メモリ以上の割り当てを可能にする。

概要

仮想メモリは、各プロセスに「連続した専用のメモリ空間を独り占めしている」ように見せかける仕組みです。実際には物理メモリ(RAM)は1つしかなく、多数のプロセスがそれを分け合っています。しかし各プロセスから見えるのは、0番地から始まる自分だけの広大なアドレス空間です。プログラムが触るアドレス(仮想アドレス)は、実行時にカーネルとCPUの連携によって実際の物理アドレスへ翻訳されます。この「見せかけの空間」と「本当の場所」を切り離したことが、仮想メモリの核心です。

この一枚の翻訳層があるおかげで、現代のOSは3つの難題を同時に解いています。プロセス同士が互いのメモリを壊せない隔離、物理メモリより大きなプログラムも動かせる容量の拡張、そしてどのプロセスも同じアドレス配置でロードできる単純さです。ふだん意識することはありませんが、malloc でメモリを確保するのも、大きなファイルを開くのも、プログラムがクラッシュしても他が無事なのも、すべてこの仕組みの上に成り立っています。

仮想メモリの働きが表に出るのは、たいてい問題が起きたときです。メモリ不足でプロセスが強制終了される、スワップが多発してシステムが極端に遅くなる、コンテナがメモリ制限に達して落ちる——こうした場面の背後には必ず、仮想メモリと物理メモリのやりくりがあります。

なぜ生まれたか

物理メモリを各プログラムに直接使わせていた時代には、3つの深刻な困りごとがありました。第一に安全性です。全プログラムが同じ物理アドレス空間を共有すると、あるプログラムが誤った番地に書き込めば、隣で動く別プログラムやOS本体のデータを平気で破壊できてしまいます。第二に容量です。物理メモリが足りなければ、それより大きなプログラムは端的に動きませんでした。第三に配置の再現性です。プログラムがロードされる物理アドレスは実行のたびに変わるため、コードは「どこに置かれても動く」ように書かねばならず、大きな負担でした。

これらをまとめて解決したのが、「各プロセスに独立した仮想アドレス空間を与え、ページという固定サイズの塊単位で物理メモリに対応付ける」という発明です。プロセスは常に自分の仮想アドレスだけを見ればよく(配置の悩みが消える)、他プロセスのページには物理的に手が届かず(安全性が生まれる)、いま使っていないページはディスクへ退避して必要なときに戻せる(物理メモリ以上を扱える)。翻訳という間接層を一枚挟むだけで、この3つが同時に成立したのです。

詳細

ページ・ページテーブル・MMU・TLB

仮想メモリは、アドレス空間を「ページ」と呼ぶ固定サイズの塊(多くは4KB)に区切って管理します。物理メモリ側も同じサイズの「フレーム」に区切られ、どの仮想ページがどの物理フレームに対応するかの対応表がページテーブルです。プロセスごとに独立したページテーブルを持つからこそ、同じ仮想アドレスがプロセスごとに別の物理フレームを指し、隔離が実現します。

この翻訳を毎回ソフトウェアでやっていては遅すぎるため、CPUにはMMU(Memory Management Unit)という専用ハードウェアが載っています。プログラムが仮想アドレスにアクセスするたび、MMUがページテーブルを引いて物理アドレスへ変換します。さらに、直近に使った翻訳結果を憶えておく小さな高速キャッシュがTLB(Translation Lookaside Buffer)です。TLB はCPUキャッシュと同じ発想で、「よく使うものは手元に置いて何度も遠くまで取りに行かない」ことで翻訳のコストを隠します。TLB に翻訳が載っていない(TLBミス)と、ページテーブルを辿り直す余分なコストがかかります。

デマンドページングとページフォールト

プログラムを起動するとき、その全体を最初から物理メモリに読み込む必要はありません。仮想メモリはデマンドページング、すなわち「実際にアクセスされたページだけを、そのとき初めて物理メモリに載せる」方式をとります。まだ物理メモリに載っていないページにアクセスすると、MMU がそれを検知してカーネルにページフォールトという一種の割り込みを上げます。カーネルは必要なページをディスクから読み込んで物理フレームに割り当て、ページテーブルを更新してから、何事もなかったかのようにプログラムの実行を再開させます。

この仕組みのおかげで、起動が速く、実際に使う部分だけがメモリを占めます。同じ実行ファイルから起動した複数プロセスがコード領域のページを共有できるのも、この翻訳層があるからです。

スワップと超過コミット

物理メモリが逼迫すると、カーネルは「しばらく使われていないページ」を選んでディスク上のスワップ領域へ書き出し、空いたフレームを別の用途に回します。追い出されたページに再びアクセスがあれば、ページフォールトが起きてディスクから読み戻されます。これにより物理メモリより大きな作業が可能になりますが、ディスクはメモリよりも桁違いに遅いため、スワップの出し入れが頻発する「スラッシング」に陥ると、システムは激しく遅くなります。

各プロセスに広大な仮想空間を約束する以上、OSは全プロセスの要求合計が物理メモリを超えても構わず割り当てを許します。これを超過コミット(オーバーコミット)と呼びます。多くのプロセスは約束された空間を全部は使わないので、平常時はこれで回ります。しかし実際の使用が本当に物理メモリ+スワップを超えると破綻し、Linux では OOM Killer(Out Of Memory Killer)がプロセスを選んで強制終了します。

プロセスAの仮想空間ページ A0ページ A1ページ A2プロセスBの仮想空間ページ B0ページ B1ページテーブルで翻訳物理フレーム1共有フレームAとBで共用物理フレーム2スワップ(ディスク)退避されたページ
2プロセスの仮想アドレス空間が別々の物理ページに対応付けられる。一部は共有ページ、一部はスワップへ退避されている

アドレス空間のレイアウトと隔離

1つのプロセスの仮想アドレス空間は、無秩序に使われるのではなく、決まった配置を持ちます。下位アドレスから順に、機械語コード、初期化済みデータ、下から上へ伸びるヒープ、そして上位アドレスから下へ伸びるスタックが並びます。ヒープとスタックが空間の両端から向かい合って伸びるのは、互いにぶつかるまで自由に成長できるようにするためです。プログラムが扱うこれらの番地はすべて仮想アドレスであり、物理メモリのどこに置かれているかをプログラマが気にする必要はありません。

そして、この空間はプロセスごとに完全に独立しています。プロセスAの仮想アドレス 0x1000 とプロセスBの 0x1000 は、ページテーブルが別なので別の物理フレームを指します。あるプロセスがどれだけ暴走しても、翻訳表に載っていない他プロセスの物理フレームには手が届きません。プロセスの「隔離」という安全性は、この仮想メモリの翻訳層が物理的に保証しているのです。

実務での顔

仮想メモリは、運用の現場でさまざまな姿で現れます。メモリリークは、プログラムが確保したまま解放しないヒープが積み上がる現象で、監視ではプロセスの使用メモリ(RSS: 実際に物理メモリを占める量)が単調増加する形で見えます。放置すればやがて OOM Killer に狙われます。コンテナのメモリ制限は、cgroup を使って「このコンテナが使える物理メモリの上限」を課すもので、超えるとそのコンテナ内のプロセスが OOM で殺されます。ホストにメモリの余裕があってもコンテナが落ちるのはこのためです。

性能面では、スワップの多発(vmstat の si/so 列や高いページフォールト率)が「メモリが足りていない」サインになります。また、大きなファイルを mmap で仮想アドレス空間に対応付ければ、readwrite を繰り返さずファイルをメモリのように扱えます——これもデマンドページングの応用です。仮想メモリを理解していると、「なぜ落ちたか」「なぜ遅いか」の多くが一本の筋で説明できるようになります。