デマンドページングとページフォルト処理
プログラムが必要としたページだけを実際に触れた瞬間に読み込む仕組みと、ページフォルトでカーネルが何を判定し何を埋めるのかを、マイナー・メジャーの分岐から内部動作で理解できます。
- 1.デマンドページングは、メモリ確保時には物理ページを割り当てず、最初にアクセスされた瞬間のページフォルトで初めて中身を埋める遅延戦略。起動とメモリ使用量を抑える。
- 2.フォルトは、I/Oを伴わず既存ページに繋ぐだけのマイナーフォルトと、ディスクからの読み込みが要るメジャーフォルトに分かれる。性能を支配するのは後者の回数。
- 3.ハンドラはVMA照合で正当性を確認し、匿名・ファイル・スワップ・CoWの別に分岐して物理ページを用意する。先読み(readahead)でメジャーフォルトをまとめて減らす。
デマンドページングとは:確保と居住の分離
mmap や malloc でメモリを確保しても、その瞬間に物理ページが割り当てられるわけではありません。カーネルがやるのは、プロセスの仮想アドレス空間に どの範囲が何の用途か を記述した区間(Linux でいう VMA, virtual memory area)を登録するだけです。物理メモリ(フレーム)は そのページに実際にアクセスした瞬間 に初めて与えられます。これが デマンドページング(demand paging) で、「要求があってから(on demand)載せる」遅延戦略です。
狙いは二つあります。第一に 起動が速い:実行ファイルが数十 MB あっても、最初に触れる数ページだけ読み込めば動き出せます。第二に メモリを節約できる:確保したが一度も触らない領域には物理ページが付かないため、malloc の予約と実使用の差がメモリを食いません。鍵となるのは、ページテーブルのエントリ(PTE)を 「無効(not present)」 のままにしておき、CPU のアクセスでわざと例外を起こさせる点です。
仮想空間に区間を確保することと、物理ページが住み着く(resident になる)ことは別レイヤーの操作です。RSS(resident set size)が確保量より小さいのはこのため。デマンドページングは「予約は安く、居住は必要になってから」を徹底する仕組みです。
ページフォルトの発生:ハードウェアからハンドラへ
CPU が仮想アドレスを物理アドレスへ変換しようとして、対応する PTE が無効、または許可されていないアクセス(読み取り専用ページへの書き込みなど)だった場合、MMU は ページフォルト例外 を上げます。これは同期例外(フォルト)で、命令の途中で発生し、原因を解決すれば 同じ命令をやり直せる ように設計されています。
ハードウェアはフォルト時に、フォルトを起こした アドレス(x86 なら CR2 レジスタ)と、エラーコード(読み/書き、ユーザ/カーネル、存在しない/権限違反 の別)をカーネルに渡します。カーネルのページフォルトハンドラはこれを入口に処理を始めます。アドレス変換の前提は 仮想記憶のアドレス変換とMMU/TLBの内部 を参照してください。
ハンドラの最初の仕事は 正当性の判定 です。フォルトアドレスがプロセスのどの VMA に属するかを探し、その VMA に記録された許可(読み/書き/実行)と照合します。ここで二つに分かれます。
- VMA が存在し、アクセスが許可と矛盾しない → 正当なフォルト。物理ページを用意して PTE を埋め、命令を再実行させる。
- VMA が無い、または許可違反(読み取り専用への書き込みで CoW でもない、実行不可領域の実行など)→ 不正アクセス。ユーザ空間なら
SIGSEGVを送る。
ページフォルトは正常動作の一部です。デマンドページングでは「まだ載せていないページに初めて触れた」だけでフォルトが起き、ハンドラが粛々と埋めます。異常なのは VMA に属さないアドレスや権限違反のフォルトだけで、これが SIGSEGV になります。両者を混同しないことが内部理解の第一歩です。
マイナーフォルトとメジャーフォルト
正当なフォルトは、ディスク I/O を伴うか否か でさらに二分されます。この区別が性能を支配します。
| 種類 | 条件 | コスト | 代表例 |
|---|---|---|---|
| マイナーフォルト | 必要なデータは既にメモリ上にある。PTEを繋ぐだけ | 数百ナノ秒〜(CPU処理のみ) | ゼロページの割当、共有済みファイルページへの新規マップ、CoW複製 |
| メジャーフォルト | データをストレージ(スワップ/ファイル)から読む必要がある | 数十マイクロ〜ミリ秒(I/O待ち) | 未ロードのファイルページ、スワップアウト済みページの復帰 |
マイナーフォルト(minor / soft fault) は、解決に必要なページがすでに物理メモリのどこかに居る場合です。たとえば他プロセスがロード済みの共有ライブラリページに自分が初めてマップするとき、中身は page cache にあるので 新しい I/O は要らず、自分の PTE をそのページへ向けるだけ で済みます。CoW による複製も、コピー元はメモリ上にあるためマイナーに分類されます。
メジャーフォルト(major / hard fault) は、必要なデータがメモリに無く ストレージから読み込む ケースです。プロセスは I/O 完了までブロックされるため、待ち時間がマイナーより桁違いに長くなります。つまり アプリの体感速度を決めるのはメジャーフォルトの発生回数 であり、チューニングの主目標になります。/proc/<pid>/stat の minflt/majflt、ps -o min_flt,maj_flt、vmstat などで両者を計測できます。
ハンドラの分岐:何を埋めるか
正当なフォルトと判定された後、ハンドラは VMA の種類とアクセス内容から 物理ページの中身をどう用意するか を分岐します。
page_fault(addr, error_code):
vma = find_vma(addr)
if vma が無い or 許可違反:
→ SIGSEGV(不正アクセス)
if 書き込みフォルト and PTE が read-only:
# 共有読み取り専用ページへの書き込み
→ CoWフォルト:ページを複製し、複製先を書込可で繋ぐ(マイナー)
elif PTE が「スワップ済み」を示す:
→ スワップインI/Oを発行(メジャー)、復帰後にPTEを繋ぐ
elif 匿名マッピング(ヒープ/スタック/MAP_ANONYMOUS):
→ ゼロ初期化ページを割り当てPTEを繋ぐ(多くはマイナー)
else: # ファイルバックドマッピング
if 該当ページが page cache に在る:
→ そのページに繋ぐ(マイナー)
else:
→ ファイルから読み込み(メジャー)+ 先読み
要点を順に押さえます。
- 匿名ページ(anonymous):ヒープやスタック、
MAP_ANONYMOUSの初回アクセス。中身の出所が無いので ゼロで初期化 したページを与えます。多くの OS は読み取りの初回フォルトを 共有のゼロページ に読み取り専用で繋ぐ最適化を持ち、書き込みが来て初めて専用ページを割り当てます(これも CoW の一形態でマイナー)。 - ファイルバックドページ(file-backed):実行ファイルや
mmapしたファイル。該当オフセットのページが page cache にあればマイナー、無ければファイルからの読み込みでメジャーになります。仕組みは メモリマップトファイル(mmap) を参照。 - スワップ済みページ:以前 ページ置換アルゴリズム によって追い出されたページ。PTE にスワップの位置が記録されており、スワップインの I/O(メジャー)で復帰させます。
CoW フォルトとの関係
CoW(コピーオンライト)は、ページフォルト機構の上に成り立つ典型例です。fork 直後、親子は同じ物理ページを 読み取り専用 で共有します。どちらかが書き込もうとすると、読み取り専用ページへの書き込みとして ページフォルトが発生 し、ハンドラが「これは権限違反ではなく CoW だ」と判定して そのページだけを複製、書き込み側の PTE を新しいページに書込可で繋ぎ直します。
ここで重要なのは分類です。CoW フォルトはマイナーフォルト です。コピー元のページは必ずメモリ上にあるため、ディスク I/O が発生しないからです。コストはページ複製(memcpy)とページ割り当てであり、メジャーのような I/O 待ちはありません。詳細な動作は コピーオンライト(CoW) を参照してください。
読み取り専用ページへの書き込みフォルトは、文脈で意味が変わります。CoW 用に意図的に read-only にした共有ページなら正当な CoW フォルトとして複製します。一方、本当に書き込み禁止の領域(定数領域や .text など)への書き込みなら権限違反として SIGSEGV です。ハンドラは VMA の本来の許可と PTE の現在状態を突き合わせてこれを見分けます。
先読み(readahead):メジャーフォルトをまとめる
メジャーフォルトは I/O 待ちが重いので、1 回の I/O でまとめて多くのページを取り込む ことが効きます。これが 先読み(readahead) です。フォルトしたページの周辺(後続のオフセット群)を、まだ要求されていなくても投機的に読み込んでおきます。
効く理由は 空間的局所性 と I/O の固定コスト にあります。ディスク(特に回転体)では、シークなどの固定コストが 1 回の I/O に乗るため、1 ページずつ N 回読むより連続した N ページを 1 回で読むほうが圧倒的に速い。先読みが当たれば、後続ページは到達時点で page cache に居るので メジャーが起きずマイナーで済む、あるいはフォルトすら起きません。
逐次アクセス時の効果:
先読みなし: fault→read(1) … fault→read(1) … fault→read(1) ← 毎回I/O待ち
先読みあり: fault→read(N) ……………………… ← 1回のI/Oで後続Nページを充填
以降は cache ヒット(メジャーが消える)
カーネルはアクセスパターンを観測して 先読みの量を適応的に調整 します。逐次アクセスを検知すると先読み窓(read-ahead window)を広げ、ヒットが続く限り拡大します。逆にランダムアクセスを検知すると先読みを縮小・停止します。無駄な先読みは page cache を汚し、本当に要るページを追い出して逆効果になるためです。madvise の MADV_SEQUENTIAL/MADV_RANDOM/MADV_WILLNEED でアプリ側からヒントを与え、この挙動を制御できます。
(1)マイナー=I/Oなし、メジャー=I/Oあり、という定義の軸を即答できること。(2)CoW フォルトがマイナーである理由(コピー元がメモリ上にある)。(3)デマンドページングが RSS を確保量より小さく保つ理屈。(4)readahead が効くのは逐次アクセス+I/O固定コストが理由で、ランダムでは逆効果になりうること。この 4 点が頻出です。
まとめ
- デマンドページング は、確保時には PTE を無効のままにし、初回アクセスのページフォルトで初めて物理ページを埋める 遅延戦略。起動を速くし、確保量に対して RSS を小さく保つ。
- ハンドラはまず VMA 照合で正当性を判定 し、不正なら
SIGSEGV。正当なフォルトは 匿名・ファイル・スワップ・CoW に分岐して中身を用意する。 - フォルトは マイナー(I/Oなし) と メジャー(I/Oあり) に分かれ、体感性能を支配するのはメジャーの回数。CoW フォルトはコピー元がメモリ上にあるためマイナー。
- 先読み(readahead) は周辺ページを投機的に読み、メジャーをまとめて減らす。逐次では有効、ランダムでは逆効果になりうるため適応制御される。
前提は 仮想記憶のアドレス変換とMMU/TLBの内部 と メモリマップトファイル(mmap)、追い出し側の論理は ページ置換アルゴリズム、書き込み時複製は コピーオンライト(CoW) も合わせて読むと全体像が繋がります。
OS Article
デマンドページングとページフォルト処理を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
デマンドページング
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
フォルトは、I/Oを伴わず既存ページに繋ぐだけのマイナーフォルトと、ディスクからの読み込みが要るメジャーフォルトに分かれる。性能を支配するのは後者の回数。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「デマンドページング / ページフォルト」に近いか確認する。
- 強みである「デマンドページングは、メモリ確保時には物理ページを割り当てず、最初にアクセスされた瞬間のページフォルトで初めて中身を埋める遅延戦略。起動とメモリ使用量を抑える。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。