Dev Study
OSと低レイヤのしくみ コース

12. mallocが成功しても物理メモリはまだ無い — デマンドページング

仮想メモリを確保する瞬間と、物理メモリが割り当てられる瞬間がどれだけズレているかを図で追っていきましょう。

① mmapが返すのは「住所」だけで「部屋」ではない

mmap(2) が成功した直後に存在しているもの / していないもの
mmap(NULL, 1GB, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)プロセスが1GBの匿名メモリを要求
カーネルがやること
仮想アドレス範囲の 帳簿に1行足すvm_area_struct を1つ作る
戻り値として 先頭仮想アドレスを返す失敗時は MAP_FAILED = (void *) -1
カーネルがやらないこと
この領域用の物理ページの確保1ページも用意しない
ページテーブルへの記入PTEはまだ空のまま
  • ・MAP_ANONYMOUS は「ファイルに紐づかず、内容はゼロで初期化される」マッピングだと mmap(2) に規定されています
  • ・成功時の戻り値はマップされた領域へのポインタ、失敗時は MAP_FAILED((void *) -1)

mmap の成功が意味するのは「その仮想アドレス範囲を君が使ってよい」という予約だけです。中身の物理メモリはまだ1ページも用意されていません。

② 最初に触った瞬間にページフォルトが起きる

未割り当てページへの最初のアクセスで起きること
p[0] = 1;確保した領域へ初めて書き込む
MMUがPTEを引く
PTEのpresentビットが0対応する物理ページが無い
CPUが例外を上げる
ページフォルト例外ユーザ空間の実行が中断される
カーネルのフォルトハンドラへ
この仮想アドレスは 登録済みの範囲か?帳簿(VMA)を検索
登録済みだった
ここで初めて 物理ページを1枚割り当てるゼロ埋めして PTE に書き込む
  • ・割り当ての単位はページ単位です。1GB確保して1バイト書いても、増える物理メモリは1ページ分だけです
  • ・帳簿に無いアドレスへのアクセスだった場合は、割り当てではなく SIGSEGV になります

物理メモリが渡されるのは「要求した時」ではなく「実際に触った時」です。この遅延こそがデマンドページングと呼ばれる仕組みです。

③ だから確保直後はRSSが増えない

1GB確保 → 1ページ書き込み、の前後で /proc/[pid]/status がどう動くか
mmap で 1GB 確保した直後
2つの数字を読み比べる
VmSize+1GB 増える
仮想メモリサイズ
VmRSSほぼ増えない
実際に載っている物理ページ
p[0] に1バイト書き込む
VmSize変化なし(1GBのまま)
VmRSS / RssAnon1ページ分だけ増える
  • ・VmSize は proc_pid_status(5) で「Virtual memory size」、VmRSS は「Resident set size」と定義されています
  • ・同じ man ページは VmRSS について「RssAnon・RssFile・RssShmem の合計」と明記しています
  • ・匿名の malloc/mmap 領域に触れて増えるのは RssAnon の側です

VmSize は「予約した住所の広さ」、VmRSS は「実際に物理メモリを占有している量」。この2つが別物であることが、デマンドページングの一番わかりやすい観測結果です。

④ 予約の合計は物理メモリを超えられる

物理8GB・スワップ2GBのマシンで起きること
物理RAM 8GB実在する部屋
スワップ 2GB退避先
その上で各プロセスが確保する
プロセスA 6GB 予約実使用 0.5GB
プロセスB 6GB 予約実使用 0.5GB
プロセスC 6GB 予約実使用 0.5GB
合計すると
予約の合計 18GB物理+スワップの10GBを超えている
実際の使用 1.5GBまったく足りている
  • ・触られないページには物理メモリが渡らないため、予約の総量が実在量を超えても成立します
  • ・/proc/meminfo の Committed_AS は proc_meminfo(5) で「プロセスが確保したメモリの合計。まだ実際には使われていないものも含む」と説明されています

②で見た「触るまで渡さない」性質があるからこそ、予約の総和が実在量を超えることを許せます。これをオーバーコミットと呼びます。

⑤ 既定の設定は、このズレを許す側に振られている

/proc/sys/vm/overcommit_memory の既定値が意味すること
overcommit_memory の既定値は 0ヒューリスティックなチェック
このモードが確保時にすること
明らかに無茶な要求だけ拒否予約の総量では止めない
だから確保はほぼ通る
①〜③のズレが そのまま観測できるmmap は成功し、RSSは増えない
  • ・proc_sys_vm(5) はモード0を「heuristic overcommit (this is the default)」と明記しています
  • ・同じ man ページはモード0について「MAP_NORESERVE 付きの mmap はチェックされず、既定のチェックは非常に弱いため、プロセスがOOM-killされるリスクにつながる」と述べています
  • ・厳密に止める設定(モード2)と、その上限である CommitLimit の扱いは別レッスンで扱います

既定のモード0は「明らかに異常な要求だけ弾く」ゆるい検査です。この緩さがあるので、確保と割り当てのズレが実際に観測できます。

⑥ 約束を守れなくなった時のためにOOM killerがいる

確保時に断らなかったツケを払う瞬間
確保時:カーネルは「いいよ」と返したmmap は成功している
後から全プロセスが本気で使い始める
各プロセスが予約分に次々と触る触るたびにページフォルト → 物理ページ割り当て
物理ページが尽きる
回収もスワップアウトも間に合わないフォルトハンドラが渡すページを作れない
確保時に断っていないので今さら断れない
OOM killer が起動どれかのプロセスを選んで強制終了させる
  • ・失敗を返せる場所が確保時にしか無いため、そこで断らなかったぶんは実行中に精算するしかありません
  • ・mmap(2) は MAP_NORESERVE について「スワップが予約されていればマッピングを変更できることが保証される」、予約しない場合は「物理メモリが無いと書き込みで SIGSEGV を受け取る可能性がある」と記しています

確保の時点で払えない約束をした以上、破綻の後始末をする誰かが必要になります。OOM killer はデマンドページングを選んだことの帰結です。

⑦ なぜこの設計なのか

確保と割り当てを分離することで得られるもの
設計判断:確保時に物理ページを渡さない
得られる利点
確保が速い帳簿に1行書くだけ。サイズに比例しない
使わない分を無駄にしない触られないページは物理メモリを消費しない
実在量より多く走らせられる予約の合計が物理+スワップを超えてよい
支払う代償
初回アクセスが遅いフォルト処理の分だけコストがかかる
失敗が後ろにズレるmmap成功後に落ちうる
  • ・この代償を避けたい領域には、あらかじめ触っておく(プリフォルトする)という選択肢があります
  • ・mmap(2) の MAP_NORESERVE は、スワップの予約を明示的に行わない指定です

「確保の成功」を安く速くする代わりに、失敗の可能性を後ろへ先送りした設計です。②で見たページフォルトが、その先送りの精算窓口にあたります。

公式ドキュメントで詳しく ↗