仮想メモリ:1つのメモリアクセス命令からページテーブル、ページフォルト、TLBを理解する

このページの目次

プログラムが大量のメモリを正常にマップしても、RSS(実効メモリ使用量)は同期して増加しません。最初の書き込み時にページフォルトが発生します。value = *p という1つのロード命令を追跡することで、⁠有効なアドレス、ページテーブルによる変換、実際のコンテンツという3つの層を分離して理解できます。

以下では、x86-64アーキテクチャと4 KiBのベースページを用いて手計算でアドレスを求め、1ページのデータが「初回使用」「fork」「アンマップ」の各段階をどう経るかを追います。他のアーキテクチャではページサイズや権限エンコーディングが異なる場合があります。

説明すべき現象該当箇所
TLBミスは必ずカーネルに入るのか?第1節:メモリアクセスパス
多段ページテーブルはどのようにメモリを節約するのか?第2~3節:アドレス計算と権限
fork後、なぜ同じ内容が読めるが、別々に変更できるのか?第4節:COWと実験
アンマップ時に、なぜ他のCPUに通知するのか?第5~8節:TLB、大ページ、観測

1. ロード命令がカーネルに入らない場合とページフォルトする理由

プログラムが value = *p を実行すると仮定します。CPUは仮想アドレスと読み取り操作を受け取り、MMUはマッピングとアクセス権限を確認する必要があります。通常、TLBにキャッシュされた変換をまず利用します。ヒットし、権限が満たされれば、このアクセスのためにページツリー全体を最初から読み取る必要はありません。TLBに適切なエントリがない場合のみ、ハードウェアはページウォークを試みます。ページテーブルキャッシュと通常のCPUキャッシュは、このプロセスをさらに高速化します。

1つのアドレスアクセス:TLBヒットとページテーブルウォークは2つのパス 仮想アドレス + アクセス種別 読 / 書 / 命令フェッチ TLB照会 アドレス空間タグと権限 アクセス対象の物理アドレス ヒットかつ権限許可 ハードウェアページテーブルウォーク 変換欠落時に実行 有効なリーフエントリ 変換を埋め込み、アクセスを継続 ページフォルト マッピング欠落または権限不一致 TLBミスはページフォルトを意味しません。ページフォルトが必ずしもディスクからの読み出しを意味するわけでもありません。

ウォークの結果有効なマッピングが得られれば、TLBミスはハードウェアのみで完全に解決できます。マッピングが欠落しているか、権限が一致しない場合、CPUは例外を発生させ、カーネルがソフトウェア側のアドレス範囲とアクセスセマンティクスを確認します。これは有効な初回割り当て、スワップイン、コピー・オン・ライト(COW)、あるいはプログラムが所有しないアドレスへのアクセスなどが考えられます。処理が成功すれば、通常は元の命令を再実行します。ユーザーアクセスが満たせない場合は、状況に応じて SIGSEGV、SIGBUS などが発生します。

「ページフォルト」を具体的なシナリオに置き換えてみましょう: ファイルの内容が他のプロセスによってページキャッシュに読み込まれている場合でも、本プロセスが初めてアクセスする際にはページフォルトが発生する可能性があります。対応するPTEがまだインストールされていないためです。この場合、既存の内容を再利用できるため、ディスクからの再読み出しは不要です。

確認すべきこと参照すべき層
このアドレスは読み取り可能か?VMAの範囲とアクセスセマンティクス
CPUはアドレス変換を直接完了できるか?現在のページテーブルとTLBの状態
バックエンドからコンテンツを再取得する必要があるか?ページキャッシュ、スワップバックアップなどの駐留状態

したがって、VMAが存在し、PTEが存在し、データが駐留していることは、互いに代替可能なものではありません。

2. なぜページテーブルは多段構造なのか:スペースの計算

48ビットアドレス、4 KiBページということは、2^48 / 2^12 = 2^36 個の仮想ページが存在します。各ページに固定で8バイトのエントリを1つ確保すると、単一の線形ページテーブルには 2^39 バイト、つまり512 GiBが必要です。ほとんどのプロセスはそのごく一部しか使用しないため、すべての空白領域に対して事前にページテーブルを割り当てるのは非常に非効率です。

多段ページテーブルにより、上位の空エントリが未マッピング範囲全体を表すことができます。実際に必要な分岐のみが下位テーブルページを割り当てられます。x86-64の4 KiBテーブルページの場合、512個の8バイトエントリがちょうど1ページを満たすため、各レベルのインデックスは9ビットを使用します。4レベルのインデックスで合計36ビット、それに12ビットのページ内オフセットを加えて48ビットとなります。

構成Linuxの階層とインデックス最終的なページ内オフセット
4レベル x86-64PGD [47:39]、P4D折りたたみ、PUD [38:30]、PMD [29:21]、PTE [20:12][11:0]
5レベル x86-64PGD [56:48]、P4D [47:39]、その後4レベルと同じ[11:0]

「折りたたみ」とは、Linuxの共通コードが依然として5レベルのインターフェースで記述できることを意味し、ターゲット構成では不要な層が独立したハードウェアテーブルに対応しないことを意味します。4レベルモードのPGDはハードウェアのPML4に対応し、5レベルではPGDはPML5に対応します。名前を機械的に特定のハードウェア層に固定してはいけません。詳細は ページテーブル階層 を参照してください。

アドレスの手計算:物理ページへの位置特定

インデックスが4レベルで順に1、2、3、4となり、ページ内オフセットが 0x567 となるアドレスを構成します。CR3は最上位テーブルの物理ベースアドレスと関連する制御情報を提供します。ハードウェアは最上位の第1項を読み取り、次のテーブルのベースアドレスを取得します。続いて第2、第3、第4項を取得します。各ステップのエントリのアドレスは「そのレベルのテーブルベース + インデックス × 8」であり、4つのインデックスを足してからページサイズを掛けるわけではありません。

最後のリーフエントリが物理ページベースアドレス 0x12345000 を指していると仮定すると、目標の物理アドレスは 0x12345567 になります。ページ内オフセットは仮想アドレスの下位12ビットから取得されます。PTEの低位はフラグであり、このオフセットは保持されません。

indices = [1, 2, 3, 4]
shifts = [39, 30, 21, 12]
va = sum(index << shift for index, shift in zip(indices, shifts)) | 0x567
assert [(va >> shift) & 0x1ff for shift in shifts] == indices
physical_base = 0x12345000  # 仮の結果。実際のページテーブルは読み込まない
physical = physical_base | (va & 0xfff)
assert physical == 0x12345567
assert (1 << 48) // 4096 * 8 == 512 * (1 << 30)
print(f"virtual={va:#x}, physical={physical:#x}")

以下では、アドレスをインデックスとオフセットに分解し、リーフサイズを直接変更して、ウォークがどのレベルで終了するかを観察します。図ではアラインメントされたデータベースアドレスとして 0x40000000 を選択し、計算ルールは上記の手計算と同じです。

bはリーフがカバーする範囲のオフセットビット数です:本図ではそれぞれ 12、21、30 を使用します。

図を準備しています
アドレスのどのビットが、このレベルで使用されているか?

多段インデックスが順にエントリを選択する。物理アドレスはリーフページのベースアドレスにページ内オフセットを加えて得られる。大ページでは上位レベルでウォークが終了し、より下位のビットが直接オフセットとなる。

アドレス幅:能力、有効化状態、実際の割り当てを分けて見る

x86-64アドレスは、現在のモードにおけるcanonical規則も満たす必要があります。未使用の上位ビットは、有効な最高アドレスビットで拡張されます。5レベルにより仮想空間は拡大しますが、プロセスが即座により多くの物理メモリを割り当てる必要はありません。CPUがLA57を公開するのは能力を示すだけであり、カーネルの構成とランタイムモードが必要です。Linuxはアプリケーションの互換性を考慮し、通常、通常のmmapを47ビットのウィンドウの外に配置することはありません。詳細は 5レベルページングの説明 を参照してください。

3. 権限はマッピングパス中に存在し、最後のエントリの単なるスイッチではない

まず、一般的なx86のリーフエントリでいくつかの情報を区別しましょう。

情報役割読み取りビット定義時の境界
Presentマッピングの有効性判断に関与non-presentはswapだけでなく、複数のソフトウェア状態を表現できる
R/W、U/S書き込み権限とユーザーアクセス制御有効な権限は上位エントリとアーキテクチャ状態にも制約される
NX実行不可セマンティクス一般的なx86エンコーディングではビット63、ビット11はNXではない
Accessed、Dirty使用と書き込みのヒントを提供マッピングレベルの情報であり、コンテンツの回復可能性を単独で証明するものではない

大ページリーフと通常のPTEでは、いくつかのビットの意味解釈が異なり、物理アドレスのビット幅もCPUの能力に応じて処理する必要があります。

例えば、上位がユーザーアクセスを拒否している場合、リーフエントリのU/Sが許可していても、読み取りは成功しません。また、あるPTEのDirty=0だからといって、匿名コンテンツに有効なバックアップコピーがあることを証明するものではありません。ハードウェアのアクセスヒントとデータの回復元は異なる問題です。

カーネルが管理するVMAの権限は「プログラムがこの範囲をどのように使用できるか」を記述し、ハードウェアのページテーブル権限は「現在のアクセスを直接完了できるか」を記述します。これらは常に一致する意図はなく、これが次の節のCOWが動作する基礎です。具体的なビット定義を確認するには、対象のカーネル arch/x86/include/asm/pgtable_types.h を参照し、教育的な簡易図を直接移植可能なビットマスクとして扱わないでください。

4. 1ページのデータが demand paging と COW を通じて完了する

プログラムがプライベートな匿名の読み書き可能な空間をマップすると、カーネルはまずアドレス範囲とセマンティクスを登録します。通常、すべてのアドレスに対してすぐにデータページを割り当てる必要はありません。最初の読み取りでは共有ゼロページなどの最適化を使用する可能性があります。最初の実際の書き込みでは、本プロセスのデータを保存できるページが必要であり、新しく割り当てられたユーザーデータのコンテンツが以前の使用者の情報を漏洩しないように保証する必要があります。

次に fork を呼び出します。親と子は fork 時の同じプライベートメモリコンテンツを見る必要がありますが、子プロセスがすぐに exec する場合、すべてのデータページを即時コピーすると大量の作業が無駄になります。典型的なCOWの手法では、両側が一時的に物理ページを共有し、関連するハードウェアマッピングを書き込み禁止に設定すると同時に、両側のVMAの書き込みセマンティクスを保持します。実際の書き込みが発生して初めて、カーネルが分離が必要かどうかを判断します。

プライベート匿名ページ:承諾アドレスから共有へ、そして独立した書き込みへ mmap で読み書き可能な範囲を確立 VMA は書き込みを許可するが、まだ対応する駐留データページがない場合もある 1 初回書き込みでページフォルト発生 物理ページ P を割り当ててゼロクリアし、書き込み可能なマッピングを確立 2 fork でアドレス空間のビューをコピー 典型的なCOW:親と子が P を共有し、ハードウェアの書き込み権限を厳格化 3 子プロセスが書き込みを試みる VMA は書き込みを許可するが、PTE は書き込み禁止のため、カーネルが COW を処理 4 子プロセスの独立した書き込み可能ページ P′ を確立 マッピングと変換キャッシュを更新し、その書き込み命令を再実行 5 古いページがすでに排他的に使用可能であれば、カーネルは再利用する可能性もある。可視性セマンティクスは、毎回物理コピーが必要であることを意味しない。

図の読み取り:VMA は書き込みを許可しているのに、なぜ書き込み禁止例外が発生するのか

子プロセスが書き込みを行う際、カーネルは「VMA は書き込みを許可しているが、現在の共有マッピングは直接書き込みを禁止している」と発見し、プライベートセマンティクスを満たす書き込み可能なページを確立します。実際にコピーが必要な場合、子プロセスの新しいページは元のコンテンツを受け取り、その後、元の修正を実行します。親プロセスは古いページへのマッピングを継続します。古いページがすでに排他的条件を満たしている場合、書き込み権限を回復するだけかもしれません。具体的なパスは、大ページ、固定参照などの要因に影響され、ユーザーがCOWセマンティクスを見るだけで、何回のmemcpyが発生したかを逆算することはできません。

ファイルの MAP_PRIVATE マッピングでもプライベートなCOWページを生成できます。修正された部分は元のファイルに書き戻されません。MAP_SHARED は共有修正セマンティクスを表現し、ファイルページは書き戻しパスに入る可能性があります。両方ともページフォルトが発生する可能性がありますが、⁠ページフォルトの数自体が、データが最終的に誰に属するかを教えてくれるわけではありません。

以下では、同じページの2つのマッピングを保持したまま、「相手がまだ存在する」場合と「相手がすでにアンマップされた」場合のそれぞれで書き込みを試みます。矢印の色が変わっただけでなく、完全なコンテンツとコピー回数を比較します。

図を準備しています
1バイトを変更するだけで、なぜ1ページ全体が分かれるのか?

fork後のプライベートページは、一時的に共有され書き込み禁止に設定される。書き込み時、まだ共有されている場合、相手のコンテンツを保持する必要がある。すでに排他的であれば、書き込み権限を回復するだけかもしれない。

実験:隔離結果を観測し、コピー回数を推測しない

以下では、初回書き込みと fork 後の可視性を同時に観測し、64個のベースページのみを使用します。

#define _GNU_SOURCE
#include <assert.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    long page = sysconf(_SC_PAGESIZE);
    assert(page > 0);
    size_t length = 64 * (size_t)page;
    unsigned char *memory = mmap(NULL, length, PROT_READ | PROT_WRITE,
                                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    assert(memory != MAP_FAILED);
    assert(madvise(memory, length, MADV_NOHUGEPAGE) == 0);
    struct rusage before, after;
    assert(getrusage(RUSAGE_SELF, &before) == 0);
    volatile unsigned char *data = memory;
    for (size_t i = 0; i < length; i += (size_t)page)
        data[i] = 7;
    assert(getrusage(RUSAGE_SELF, &after) == 0);
    printf("初回のページ単位の書き込み minor faults 差分: %ld\n",
           after.ru_minflt - before.ru_minflt);
    fflush(stdout);
    pid_t child = fork();
    assert(child >= 0);
    if (child == 0) {
        assert(data[0] == 7);
        data[0] = 9;
        assert(data[0] == 9);
        _exit(0);
    }
    int status;
    assert(waitpid(child, &status, 0) == child);
    assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
    assert(data[0] == 7);
    assert(munmap(memory, length) == 0);
    puts("子プロセスのプライベートな変更が親プロセスの内容を変えていない:通過");
}

cc -std=c11 -Wall -Wextra -Werror vm-demo.c -o vm-demo でコンパイルし、-DNDEBUG は付けない。minor fault の差分は観測のみを目的としており、必ずしも 64 に等しいとは限らない。測定には他のプロセス内のアクティビティが含まれる可能性がある。minor fault は通常、今回の処理のためにバックエンドからの読み出しを実行する必要はないが、オーバーヘッドが無視できることを意味するものではない。実験はPFNを読み取らないため、検証するのはユーザーが可視な隔離であり、物理ページ共有の直接的な証拠ではない。

5. なぜTLBの一貫性はメモリセキュリティの一部なのか

CPU 0 が munmap を実行してアドレス A の PTE をクリアし、CPU 1 が同じアドレス空間の別のスレッドを実行中であり、そのTLBがまだ A から物理ページ P への変換をキャッシュしていると仮定します。通常のデータキャッシュ一貫性はページテーブルメモリの変更を伝播できますが、各TLBエントリが自動的にページテーブルを再照会するわけではありません。この時点で P を別のオブジェクトに割り当てると、CPU 1 の古い変換が新しいオブジェクトのデータを引き続き読み書きする可能性があります。

なぜPTEをクリアした直後に古いページを他のオブジェクトに渡してはいけないのか CPU 0:アンマップ ページテーブルはもはや P を指していない CPU 1:古いTLB 依然としてアドレス A を P に変換する可能性がある 物理ページ P この時点では安全に再利用できない 関連CPUの無効化を調整 バッチ / IPI / アーキテクチャメカニズム 古い変換がもはや使用できないことを確認 並列アドレス空間切り替えの制約を含む 他の参照条件を満たす 初めて P の回収または再割り当てが可能 データキャッシュの一貫性はTLB無効化に取って代わるものではない。古い変換を使用する可能性のある関連する実行環境のみをカバーすればよい。

したがって、⁠古い物理ページを他人に再度渡す前に、危険な古い変換は無効化されなければならない。 マッピングの取り消し、権限の厳格化、ページマイグレーションでは、変換の管理とページの寿命を調整する必要があります。

従来のx86パスでは、関連するCPUをIPIで調整することが多いですが、無差別なブロードキャストは不要です。アドレス空間の使用記録、バッチ無効化、遅延処理、およびハードウェアの能力により、作業量を削減できます。具体的な関数チェーンは実装によって変化しますが、安全境界は変わりません。詳細は TLB無効化インターフェース を参照してください。

PCID/ASID は、キャッシュされた変換にアドレス空間タグを追加し、異なるアドレス空間の結果がTLBに同時に残る可能性を可能にします。不要なクリアを減らしますが、期限切れのマッピングを永続的に有効にするものではありません。タグの再利用とページテーブルの更新では、依然として無効化を処理する必要があります。mprotect/munmap の頻繁な呼び出しのオーバーヘッドは、アドレス空間がアクティブなCPUの数とバッチサイズによって変化するため、変更されたPTEの数だけで見積もってはなりません。

6. 大ページのベネフィットはどこから来て、コストは誰が支払うのか

1 GiBのデータを完全にマップするために必要なリーフエントリを計算してみましょう。

マッピング粒度リーフエントリ数最下位テーブルの変化
4 KiB ベースページ262144 個の PTEPTEの保存だけで約 2 MiB
2 MiB PMD 大ページ512 個の PMD リーフ対応する下位 PTE テーブルを省略

1回の大ページ変換はより広い仮想範囲をカバーし、特定のワークセットのTLB負荷を軽減できます。ベネフィットは変換カバー範囲とページテーブル作業量の変化から得られますが、実際の規模はCPUとアクセスパターンによります。

しかし、異なるページサイズのTLB容量と構成は必ずしも同じではなく、同じ固定エントリ数で掛け算して、あるCPUの実際の性能と見なすことはできません。大ページは対応する連続した物理範囲も要求します。割り当て、ゼロクリア、マイグレーション、および整理により、コストが1つのページフォルトに集中する可能性があります。少量のバイトしか使用しない範囲では内部の浪費が発生する可能性があり、部分的なアンマップや権限の変更には分割が必要になる場合があります。

メカニズム大ページの取得方法主なトレードオフ
HugeTLB明示的なプールとマッピングインターフェース、予約/実際の割り当てなどの管理が存在容量管理が比較的明確だが、予約が無限のリソースを意味しない。通常のスワップ不可
PMD THPページフォルト時の試行またはバックグラウンドマージ、ポリシーに基づいて決定変換とページフォルトのオーバーヘッドを削減するが、ベースページにフォールバックしたり、整理の遅延を支払ったりする可能性がある
mTHP複数のサイズの大きな folio、通常は PTE マッピングのまま複数のページをバッチ処理し、一部のページフォルトオーバーヘッドを削減する。必ずしも1レベルのページテーブルを省略するわけではない

THP は単に「すでに連続している 512 個のページにフラグを変更する」ことではありません。バックグラウンドマージは新しい大ページを割り当ててコンテンツを移動でき、ページフォルトパスでも直接大ページを試すことができます。mTHP の PTE マッピングが追加のハードウェア変換ベネフィットを得られるかはアーキテクチャによります。

実際のサイズと有効化ポリシーは、各 hugepages-*kB/enabled などのインターフェースを確認し、トップレベルのスイッチだけですべてのパスを判断してはいけません。明示的な MADV_COLLAPSE には独立した契約があります。詳細は THP管理ドキュメント と HugeTLB を参照してください。

7. なぜPTIはページテーブルを切り替えるのか

従来の共有レイアウトでは、ユーザーとカーネルが同じアドレス変換構造にマッピングされ、通常の権限チェックがユーザーによるカーネルページへのアクセスを阻止します。しかし、Meltdown などの問題は、特定のCPU上では、アーキテクチャの権限によって拒否されたアクセスでも、トランジエント実行中に観察可能なサイドチャネルの痕跡を残す可能性があることを示しています。PTI の考え方は、ユーザー空間のページテーブルに表示されるカーネルマッピングの集合を縮小し、ほとんどの機密アドレスがそのマッピングに存在しないようにすることです。

カーネルに入る際に完全なビューに切り替え、ユーザーモードに戻る際に簡素化されたビューに切り替えます。エントリに必要な最小限の領域は依然としてアクセス可能でなければなりません。2つのビューはユーザー部分を共有できるため、すべてのプロセスの全ユーザーデータと各レベルのページテーブルが完全に2回コピーされることを意味しません。オーバーヘッドにはページテーブルの切り替えと関連する変換管理が含まれ、PCID は一部のコストを緩和できます。「すべてのシステムコールでちょうど2回のTLBミス」と固定して記述してはいけません。詳細は x86 PTI 設計 を参照してください。

8. 観測結果を読む際に、4つの数量を分ける

モニタリングを読む際には、4つの数量を別々に記録しましょう。

数量典型的な観測何を示すか
仮想範囲VmSizeプログラムにどのくらいのアドレス空間を登録したか
駐留データRSS、smaps現在どのコンテンツがメモリを占有しているか
ページテーブルの占用VmPTE変換構造自体のメモリコスト
ページフォルトイベントfault 計数カーネルの介入が必要なアクセスの回数

VmSize が大きく RSS が小さい場合、未接触のマッピングに起因する可能性があります。大ページが実際に使用されているかどうかは、smaps などのフィールドと組み合わせて判断すべきであり、設定スイッチのみを見てはいけません。

/proc/<pid>/pagemap はバイナリインターフェースであり、PFN は権限制限により非表示にされる可能性があり、ビットの解釈もドキュメントのバージョンに従って読み取るべきであり、そのままテキストとして cat してはいけません。次の記事 VMA と Maple Tree では、カーネルが特定のページフォルトアドレスが有効であることをどのように知っており、権限の一部を変更するだけでなぜより多くの VMA が分割されるのかをさらに説明します。