- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
前回、アドレスがDRAM上の座標ではないと予告しました。今回はその変換の仕組みと、そこから生まれる実務上の落とし穴を扱います。
まず結論
どのプログラムも「自分だけが全メモリを使っている」という前提で動けます。実際には、プログラムが見ているアドレスは偽物で、CPUが実物のアドレスへ変換しています。
この仕組みの帰結として、mallocが成功しても物理メモリはまだ確保されていません。実際に触った瞬間に初めて割り当てられます。これを知らないと、メモリ使用量の数字をすべて読み違えます。
1GBのメモリを malloc で確保して成功しました。この直後、実際に使われている物理メモリはどれだけ増えているでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
全プロセスが「自分が主役」だと思い込んでいる
いま動いているプログラムは何十個もあります。ブラウザ、エディタ、音楽プレイヤー。それぞれが勝手にメモリを使っているのに、なぜ互いのデータを壊さないのでしょうか。
素朴に考えれば危険です。ブラウザが「アドレス 0x1000 に書き込む」といい、エディタも「0x1000 に書き込む」といえば、衝突するはずです。
ところが衝突しません。理由は、プログラムが見ているアドレスが偽物だからです。
どのプロセスも「自分は 0 番地から広大なメモリを独占している」という前提で動いています。その前提は嘘なのですが、嘘を嘘のまま成立させる仕組みがあるので、プログラムは気づかずに動けます。それが仮想メモリです。
同じ番号が、別の場所を指している
- ・同じ 0x1000 でも、変換表が違うので行き先が違う
- ・変換表に載っていないアドレスには、そもそも到達できない = 他プロセスのメモリを触れない
ポイントは2つあります。1つは、変換表がプロセスごとに別々なので、同じ番号でも行き先が違うこと。もう1つは、変換表に載っていないアドレスには物理的に到達できないことです。
つまり他のプロセスのメモリを覗こうとしても、変換表にその対応が無いので、行きたくても行けません。これが保護の正体です。善意でも悪意でもなく、単純に道が無いのです。
あるプロセスが他のプロセスのメモリを読めないのは、なぜですか。
変換は4段の表を辿って行われる
では変換はどう行われるのでしょうか。x86-64では、4階層の表を順に辿ります。
48ビットの仮想アドレスを、9ビットずつ4つに切り分け、残り12ビットをページ内の位置とします。
ビット位置 役割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` が非NULLを返したら、そのメモリは必ず使えると保証されますか。
VSZ と RSS
VSZは予約したアドレス空間、RSSは実際に物理メモリ上にある量です。ヒープの事前予約や共有ライブラリの計上で、VSZは膨らみます。
USER PID %CPU %MEM VSZ RSS COMMANDapp 1432 0.6 2.1 1284740 349872 java -Xmx2g -jar app.jarVSZ 1.2GB = 予約したアドレス空間RSS 350MB = 実際の物理メモリ
free と available
freeは完全に手つかずの量、availableは「今すぐ新しいアプリに使える見込み」です。buff/cacheの多くは必要になれば回収できるので、実質的な空きはavailableを見ます。
total used free buff/cache availableMem: 15Gi 4.6Gi 2.7Gi 8.0Gi 10Gifree 2.7GB = 完全に手つかずavailable 10GB = 実際に使える見込み
プロセス単位のVSZ/RSSと、システム全体のfree/available。見ている対象は違いますが、同じ読み違えが起きます。それは何でしょうか。
どちらも、大きい方の数字を見て焦る人がいます。
この章のまとめ
- プログラムが見ているアドレスは仮想アドレスで、プロセスごとの変換表を通じて物理アドレスに変換される。
- 変換表が別々なので、同じ番号でも行き先が違う。表に載っていない場所へは到達手段が無い——これが保護の正体。
- x86-64は4KiBページ・4階層(各512エントリ)。毎回表を辿ると遅いのでTLBが変換結果をキャッシュする。
- mallocが成功しても物理メモリはまだ確保されていない。 実際に触った時点で割り当てられる(デマンドページング)。
- VSZ(予約)とRSS(実物)、free(手つかず)とavailable(使える見込み)は別物。混同するとメモリ不足を誤診する。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
2つのプロセスが両方ともアドレス 0x1000 に書き込んだとき、何が起きますか。
用語集(6語)
- 仮想アドレス / 物理アドレス
- 仮想アドレスはプログラムが見る偽物の番号。物理アドレスは実際のメモリ上の番号。MMUが前者を後者に変換する。
- ページ
- メモリを管理する単位。x86-64では標準4KiB。大きなページ(2MiB・1GiB)も選べる。
- ページテーブル
- 仮想アドレスと物理アドレスの対応表。プロセスごとに用意されるため、同じ仮想アドレスでもプロセスが違えば別の物理アドレスを指す。
- TLB
- アドレス変換結果を覚えておくキャッシュ。これが無いと変換のたびに4段の表を辿ることになる。
- デマンドページング
- 確保した時点では物理メモリを割り当てず、実際にアクセスされた瞬間に割り当てる方式。mallocの直後に物理メモリが増えない理由。
- VSZ / RSS
- VSZ=仮想アドレス空間として予約した量。RSS=実際に物理メモリ上にある量。VSZ ≫ RSS が普通。