Dev Study
← コンピュータの仕組み
レッスン 9 / 13·約 14 分·第1期 コンピュータ単体の仕組み

仮想メモリ — なぜプロセスは互いのメモリを壊さないのか

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

前回、アドレスがDRAM上の座標ではないと予告しました。今回はその変換の仕組みと、そこから生まれる実務上の落とし穴を扱います。

まず結論

どのプログラムも「自分だけが全メモリを使っている」という前提で動けます。実際には、プログラムが見ているアドレスは偽物で、CPUが実物のアドレスへ変換しています。

この仕組みの帰結として、mallocが成功しても物理メモリはまだ確保されていません。実際に触った瞬間に初めて割り当てられます。これを知らないと、メモリ使用量の数字をすべて読み違えます。

? 読む前に予測

1GBのメモリを malloc で確保して成功しました。この直後、実際に使われている物理メモリはどれだけ増えているでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

全プロセスが「自分が主役」だと思い込んでいる

いま動いているプログラムは何十個もあります。ブラウザ、エディタ、音楽プレイヤー。それぞれが勝手にメモリを使っているのに、なぜ互いのデータを壊さないのでしょうか。

素朴に考えれば危険です。ブラウザが「アドレス 0x1000 に書き込む」といい、エディタも「0x1000 に書き込む」といえば、衝突するはずです。

ところが衝突しません。理由は、プログラムが見ているアドレスが偽物だからです。

どのプロセスも「自分は 0 番地から広大なメモリを独占している」という前提で動いています。その前提は嘘なのですが、嘘を嘘のまま成立させる仕組みがあるので、プログラムは気づかずに動けます。それが仮想メモリです。

具体実物

同じ番号が、別の場所を指している

2つのプロセスが同じ仮想アドレスを使っても衝突しない
ブラウザ0x1000 に書き込む
エディタ0x1000 に書き込む
それぞれ専用の変換表(ページテーブル)を通る
変換表Aブラウザ用
変換表Bエディタ用
違う物理アドレスに着地する
物理 0x8A3000ブラウザの実データ
物理 0x2F1000エディタの実データ
  • ・同じ 0x1000 でも、変換表が違うので行き先が違う
  • ・変換表に載っていないアドレスには、そもそも到達できない = 他プロセスのメモリを触れない

ポイントは2つあります。1つは、変換表がプロセスごとに別々なので、同じ番号でも行き先が違うこと。もう1つは、変換表に載っていないアドレスには物理的に到達できないことです。

つまり他のプロセスのメモリを覗こうとしても、変換表にその対応が無いので、行きたくても行けません。これが保護の正体です。善意でも悪意でもなく、単純に道が無いのです。

✓ ここまでの確認

あるプロセスが他のプロセスのメモリを読めないのは、なぜですか。

抽象原理

変換は4段の表を辿って行われる

では変換はどう行われるのでしょうか。x86-64では、4階層の表を順に辿ります。

48ビットの仮想アドレスを、9ビットずつ4つに切り分け、残り12ビットをページ内の位置とします。

x86-64 の4階層ページング(Intel SDM の定義)表
ビット位置 役割
47:39 第1階層(PML4)の何番目のエントリか
38:30 第2階層(PDPT)の何番目か
29:21 第3階層(PD)の何番目か
20:12 第4階層(PT)の何番目か
11:0 ページ内の位置(4KiB = 12ビット)

9ビットということは 2の9乗 = 512エントリです。1エントリ8バイトなので、512 × 8 = 4096バイト = ちょうど1ページ。表そのものが1ページにきれいに収まるよう設計されています。

ただしここで問題があります。変換のたびに4回もメモリを読むことになり、前回学んだとおりメモリアクセスは200〜400サイクルかかります。これでは変換だけで遅すぎます。

そこで TLB という専用のキャッシュが、変換結果を覚えておきます。一度変換したアドレスは、次から表を辿らずに済みます。

具体実物

malloc は、実はまだ何も確保していない

ここからが実務に直結します。仮想メモリの仕組みから、非常に誤解されやすい挙動が生まれます。

malloc(1GB) が成功したとします。この時点で物理メモリは1GB確保されていません。

manページにはこう明記されています。

By default, Linux follows an optimistic memory allocation strategy. This means that when malloc() returns non-NULL there is no guarantee that the memory really is available.

malloc が返すのは仮想アドレスです。「この範囲を使う予定です」と記録されただけで、対応する物理メモリはまだありません。実際にその番地へ書き込んだ瞬間に、ページ単位で初めて割り当てられます。これをデマンドページングといいます。

malloc(1GB) してから実際に使うまで
malloc(1GB)仮想アドレス空間を1GB予約○ VSZ が +1GB、RSS はほぼ不変
先頭4KBに書き込むページフォルト発生 → 物理ページを1枚割り当て○ RSS が +4KB
全体に書き込む触ったページから順に割り当て○ RSS がようやく +1GB
↓
確保した量(VSZ)と、実際に使っている量(RSS)は別物
  • ・だから「確保に成功したのに後からメモリ不足で落ちる」ことが起こりうる
  • ・ページフォルトはエラーではなく、正常な割り当ての手順
✓ ここまでの確認

`malloc` が非NULLを返したら、そのメモリは必ず使えると保証されますか。

⇄ 2つを見比べる
具体例 A

VSZ と RSS

VSZは予約したアドレス空間、RSSは実際に物理メモリ上にある量です。ヒープの事前予約や共有ライブラリの計上で、VSZは膨らみます。

$ ps aux(抜粋・表示例)コマンド出力
USER PID %CPU %MEM VSZ RSS COMMAND
app 1432 0.6 2.1 1284740 349872 java -Xmx2g -jar app.jar
 
VSZ 1.2GB = 予約したアドレス空間
RSS 350MB = 実際の物理メモリ
具体例 B

free と available

freeは完全に手つかずの量、availableは「今すぐ新しいアプリに使える見込み」です。buff/cacheの多くは必要になれば回収できるので、実質的な空きはavailableを見ます。

$ free -h(表示例)コマンド出力
total used free buff/cache available
Mem: 15Gi 4.6Gi 2.7Gi 8.0Gi 10Gi
 
free 2.7GB = 完全に手つかず
available 10GB = 実際に使える見込み

プロセス単位のVSZ/RSSと、システム全体のfree/available。見ている対象は違いますが、同じ読み違えが起きます。それは何でしょうか。

どちらも、大きい方の数字を見て焦る人がいます。

この章のまとめ

  • プログラムが見ているアドレスは仮想アドレスで、プロセスごとの変換表を通じて物理アドレスに変換される。
  • 変換表が別々なので、同じ番号でも行き先が違う。表に載っていない場所へは到達手段が無い——これが保護の正体。
  • x86-64は4KiBページ・4階層(各512エントリ)。毎回表を辿ると遅いのでTLBが変換結果をキャッシュする。
  • mallocが成功しても物理メモリはまだ確保されていない。 実際に触った時点で割り当てられる(デマンドページング)。
  • VSZ(予約)とRSS(実物)、free(手つかず)とavailable(使える見込み)は別物。混同するとメモリ不足を誤診する。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 4正解: 0

2つのプロセスが両方ともアドレス 0x1000 に書き込んだとき、何が起きますか。

用語集(6語)
仮想アドレス / 物理アドレス
仮想アドレスはプログラムが見る偽物の番号。物理アドレスは実際のメモリ上の番号。MMUが前者を後者に変換する。
ページ
メモリを管理する単位。x86-64では標準4KiB。大きなページ(2MiB・1GiB)も選べる。
ページテーブル
仮想アドレスと物理アドレスの対応表。プロセスごとに用意されるため、同じ仮想アドレスでもプロセスが違えば別の物理アドレスを指す。
TLB
アドレス変換結果を覚えておくキャッシュ。これが無いと変換のたびに4段の表を辿ることになる。
デマンドページング
確保した時点では物理メモリを割り当てず、実際にアクセスされた瞬間に割り当てる方式。mallocの直後に物理メモリが増えない理由。
VSZ / RSS
VSZ=仮想アドレス空間として予約した量。RSS=実際に物理メモリ上にある量。VSZ ≫ RSS が普通。