x86 KVM シャドウ MMU

MMU(arch/x86/kvm 配下、ファイルは mmu.[ch] と paging_tmpl.h)は、ゲストに 標準的な x86 MMU を提供しつつ、ゲスト物理アドレスをホスト物理アドレスに 変換する役割を担う。

MMU のコードは、次の要件を満たそうとする:

  • correctness:

    タイミングを除いて、ゲストが自分はエミュレートされた MMU 上で 動いていると判断できないようにする(特定の実装の特性、例えば tlb のサイズをエミュレートするのではなく、仕様に従おうとする)

  • security:

    ゲストは、自分に割り当てられていないホストメモリに触れては ならない

  • performance:

    MMU によって生じる性能低下を最小限にする

  • scaling:

    大きなメモリと多数の vcpu を持つゲストに対応できるようにする

  • hardware:

    x86 の仮想化ハードウェアの全範囲をサポートする

  • integration:

    Linux のメモリ管理コードがゲストメモリを制御下に置く。それにより、 スワップ、ページマイグレーション、ページマージ、透過的 ヒュージページといった機能が変更なしに動く

  • dirty tracking:

    ゲストメモリへの書き込みを報告し、ライブマイグレーションと フレームバッファベースのディスプレイを可能にする

  • footprint:

    ピン留めされたカーネルメモリの量を低く抑える(大半のメモリは 縮小可能であるべきだ)

  • reliability:

    複数ページの確保や GFP_ATOMIC の確保を避ける

略語

pfn

ホストページフレーム番号

hpa

ホスト物理アドレス

hva

ホスト仮想アドレス

gfn

ゲストフレーム番号

gpa

ゲスト物理アドレス

gva

ゲスト仮想アドレス

ngpa

ネストゲスト物理アドレス

ngva

ネストゲスト仮想アドレス

pte

ページテーブルエントリ(ページング構造エントリを総称するのにも使う)

gpte

ゲスト pte(gfn を指す)

spte

シャドウ pte(pfn を指す)

tdp

2 次元ページング(NPT と EPT を指すベンダ非依存の用語)

サポートする仮想ハードウェアと実ハードウェア

MMU は第 1 世代の MMU ハードウェアをサポートする。このハードウェアでは、ゲスト のエントリ中に現在のページングモードと cr3 をアトミックに切り替えられ、さらに 2 次元ページング(AMD の NPT と Intel の EPT)も扱える。MMU がゲストに公開する エミュレート済みハードウェアは、従来の 2/3/4 レベル x86 MMU であり、グローバル ページ、pae、pse、pse36、cr0.wp、1GB ページをサポートする。NPT 対応のホスト上 では、NPT 対応ハードウェアを公開することもできる。

アドレス変換

MMU の主な役割は、ゲストのアドレスを変換するようプロセッサの MMU を設定する ことである。必要な変換は時と場合によって異なる:

  • ゲストのページングが無効のときは、ゲスト物理アドレスをホスト物理アドレスに 変換する(gpa->hpa)

  • ゲストのページングが有効のときは、ゲスト仮想アドレスをゲスト物理アドレスに、 さらにホスト物理アドレスに変換する(gva->gpa->hpa)

  • ゲストが自身のゲストを起動したときは、ネストゲスト仮想アドレスをネストゲスト 物理アドレスに、ゲスト物理アドレスに、さらにホスト物理アドレスに変換する (ngva->ngpa->gpa->hpa)

最大の課題は、1 回の変換しかできないハードウェア(従来型)と 2 回の変換ができる ハードウェア(tdp)に、1〜3 回の変換を詰め込むことにある。必要な変換の回数が ハードウェアと一致するとき、MMU はダイレクトモードで動作する。一致しないときは シャドウモードで動作する(後述)。

メモリ

ゲストメモリ(gpa)は、kvm を使っているプロセスのユーザアドレス空間の一部である。 ゲストアドレスとユーザアドレスの間の変換(gpa->hva)はユーザ空間が定義する。 2 つの gpa が同じ hva を指すことはありうるが、その逆はない。

これらの hva には、ホストで使える任意の方法でバッキングできる。無名メモリ、 ファイルバッキングのメモリ、デバイスメモリなどである。メモリはホストによっていつ でもページングされうる。

イベント

MMU はイベントによって駆動される。イベントにはゲスト由来のものとホスト由来の ものがある。

ゲストが生成するイベント:

  • 制御レジスタへの書き込み(特に cr3)

  • invlpg/invlpga 命令の実行

  • 存在しない変換または保護された変換へのアクセス

ホストが生成するイベント:

  • gpa->hpa 変換の変化(gpa->hva の変化によるものか、hva->hpa の変化によるものか を問わない)

  • メモリの逼迫(シュリンカ)

シャドウページ

中心となるデータ構造はシャドウページ、'struct kvm_mmu_page' である。シャドウ ページは 512 個の spte を持ち、各 spte はリーフ spte か非リーフ spte のどちらか である。1 つのシャドウページにリーフ spte と非リーフ spte が混在してもよい。

非リーフ spte は、ハードウェア MMU がリーフページに到達できるようにするもので、 変換そのものとは直接関係しない。他のシャドウページを指す。

リーフ spte は、1 つのページング構造エントリに符号化された 1 回または 2 回の変換 に対応する。リーフ spte は常に変換スタックの最下位レベルであり、上位レベルの変換 があれば NPT/EPT に任せる。リーフ pte はゲストページを指す。

次の表は、リーフ pte が符号化する変換を示す。上位レベルの変換は括弧内に示す:

ネストしないゲスト:

nonpaging:     gpa->hpa
paging:        gva->gpa->hpa
paging, tdp:   (gva->)gpa->hpa

ネストゲスト:

non-tdp:       ngva->gpa->hpa  (*)
tdp:           (ngva->)ngpa->gpa->hpa

(*) the guest hypervisor will encode the ngva->gpa translation into its page
    tables if npt is not present
シャドウページは次の情報を持つ:
role.level:

このシャドウページが属するシャドウページング階層のレベル。 1=4k spte、2=2M spte、3=1G spte など。

role.direct:

セットされている場合、このページから到達できるリーフ spte は線形範囲に対応 する。例としては、リアルモードの変換、小さなホストページでバッキングされた 大きなゲストページ、NPT か EPT が有効なときの gpa->hpa 変換がある。 線形範囲は (gfn << PAGE_SHIFT) から始まり、その大きさは role.level で決まる (第 1 レベルは 2MB、第 2 レベルは 1GB、第 3 レベルは 0.5TB、第 4 レベルは 256TB)。 クリアされている場合、このページは gfn フィールドが示すゲストページテーブル に対応する。

role.quadrant:

role.has_4_byte_gpte=1 のとき、ゲストは 32 ビットの gpte を使い、ホストは 64 ビットの spte を使う。つまりゲストページテーブルはホストより多くの pte を 含むため、1 つのゲストページをシャドウするのに複数のシャドウページが必要に なる。第 1 レベルのシャドウページでは、role.quadrant は 0 か 1 で、ゲスト ページテーブル内の 1 番目または 2 番目の 512-gpte ブロックを表す。第 2 レベル のページテーブルでは、各 32 ビット gpte が 2 つの 64 ビット spte に変換される (各第 1 レベルのゲストページは 2 つの第 1 レベルのシャドウページでシャドウ されるため)。そのため role.quadrant は 0..3 の値をとる。各象限は 1GB の仮想 アドレス空間をマップする。

role.access:

親 pte から継承したゲストのアクセス権限を uwx の形式で表す。実行権限は否定形 ではなく肯定形である点に注意。

role.invalid:

このページは無効であり、使うべきではない。現在ピン留めされている(cpu ハードウェアレジスタがこのページを指している)ルートページであり、ピン留めが 外れると破棄される。

role.has_4_byte_gpte:

このページが有効となるゲスト PTE のサイズを表す。ダイレクトマップか 64 ビット gpte を使っている場合は '0'、32 ビット gpte を使っている場合は '1' である。

role.efer_nx:

このページが有効となる efer.nx の値を持つ。

role.cr0_wp:

このページが有効となる cr0.wp の値を持つ。

role.cr4_smep:

このページが有効となる cr4.smep の値を持つ。

role.smap_andnot_wp:

このページが有効となる cr4.smap && !cr0.wp の値を持つ(これが真となるページは 他のページとは異なる。後述の cr0.wp=0 の扱いを参照)。

role.smm:

システム管理モードでこのページが有効なら 1 である。このフィールドは、この シャドウページの構築に kvm_memslots 配列のどれを使ったかを決める。また、 kvm_memslots_for_spte_role マクロと __gfn_to_memslot を通じて、struct kvm_mmu_page からメモリスロットへ戻るのにも使う。

role.ad_disabled:

その MMU インスタンスが A/D ビットを使えないなら 1 である。EPT は Haswell より前には A/D ビットを持たなかった。L1 ハイパーバイザが A/D ビットを有効に しなければ、シャドウ EPT ページテーブルも A/D ビットを使えない。

role.guest_mode:

このシャドウページがネストゲスト用に作成されたことを示す。

role.passthrough:

このページはゲストページテーブルでバッキングされていないが、先頭のエントリが ゲストページテーブルを指す。NPT が 5 レベルのページテーブルを使い(ホストの CR4.LA57=1)、L1 の 4 レベル NPT(L1 CR4.LA57=0)をシャドウしているときに 設定される。

mmu_valid_gen:

このページの MMU 世代。VM 内の全 MMU ページを、vCPU を長くブロックせずに 高速に破棄するために使う。具体的には、KVM が VM ごとの有効な MMU 世代を更新 すると、各 mmu ページの mmu_valid_gen が一致しなくなる。これにより既存の MMU ページはすべて無効になる。無効なページは使えない。したがって、vCPU は ゲストに再エントリする前に、新しい有効なルートをロードしなければならない。 MMU 世代は '0' か '1' のいずれかしかとらない。なお、TDP MMU はこのフィールド を使わない。ルート以外の TDP MMU ページは、それを所有するルートからしか 到達できないためである。よって TDP MMU は、ルートページで role.invalid を 使うだけで全 MMU ページを無効化できる。

gfn:

このページがシャドウする変換を含むゲストページテーブルか、線形変換の基準と なるページフレームのいずれか。role.direct を参照。

spt:

このページの変換を含む 64 ビット spte が 1 ページ分並んだもの。kvm と ハードウェアの両方からアクセスされる。 spt が指すページは、その page->private がシャドウページ構造体を指し返す。 spt 内の spte はゲストページか、下位レベルのシャドウページを指す。具体的に は、sp1 と sp2 がシャドウページなら、sp1->spt[n] が __pa(sp2->spt) を指すこと がある。sp2 は parent_pte を通じて sp1 を指し返す。spt 配列は、シャドウページ をノード、ゲストページを葉とする DAG 構造をなす。

shadowed_translation:

512 個のシャドウ変換エントリの配列で、存在する pte ごとに 1 つ対応する。 pte から gfn とそのアクセス権限への逆引きを行うのに使う。role.direct が セットされているときは、shadow_translation 配列は確保されない。この配列の 各要素に含まれる gfn は、使用時に gfn フィールドから計算できるためである。 さらに role.direct がセットされているときは、KVM は gfn ごとのアクセス権限を 追跡しない。role.direct と gfn を参照。

root_count / tdp_mmu_root_count:

root_count はシャドウ MMU におけるルートシャドウページの参照カウンタである。 vCPU は、ルートページとして使われるシャドウページ、つまりハードウェアに 直接ロードされるページ(CR3、PDPTR、nCR3 EPTP)を取得するときに参照カウント を増やす。ルートページは参照カウントが 0 でない間は破棄できない。 role.invalid を参照。tdp_mmu_root_count も同様だが、TDP MMU 専用で、 アトミックな参照カウンタとして使う。

parent_ptes:

このページの spt を指す pte の逆引き。parent_ptes のビット 0 が 0 なら、 このページを指す spte は 1 つだけで、parent_ptes はその 1 つの spte を指す。 そうでなければ、このページを指す spte が複数あり、(parent_ptes & ~0x1) が 親 spte のリストを持つデータ構造を指す。

ptep:

このシャドウページを指す SPTE のカーネル仮想アドレス。TDP MMU 専用で、この フィールドは parent_ptes との共用体である。

unsync:

真なら、このページの変換はゲストの変換と一致しない可能性がある。これは、 pte が変更されてから tlb エントリがフラッシュされる前の tlb の状態に相当 する。したがって unsync な pte は、ゲストが invlpg を実行するか、他の方法で tlb をフラッシュしたときに同期される。リーフページで有効。

unsync_children:

このページ内の spte のうち、unsync なページ(または未同期の子を持つページ) を指すものの数。

unsync_child_bitmap:

spt 内のどの spte が、未同期かもしれないページを(直接または間接に)指すかを 示すビットマップ。あるページから到達できる未同期ページをすべて素早く見つける のに使う。

clear_spte_count:

32 ビットホストにのみ存在し、そこでは 64 ビット spte をアトミックに書き込め ない。読み手は、MMU ロックを取らずに動いている間にこれを使い、更新が進行中で あることを検出し、書き手が書き込みを終えるまで再試行する。

write_flooding_count:

ゲストはページテーブルに何度も書き込むことがあり、そのページを書き込み保護 する必要があるとエミュレーションが多発する(後述の「同期ページと未同期 ページ」を参照)。リーフページは未同期にでき、頻繁なエミュレーションを 起こさずに済むが、非リーフページではそれができない。このフィールドは、ページ テーブルが最後に実際に使われてからのエミュレーション回数を数える。このページ でエミュレーションが頻繁に起きすぎる場合、KVM は将来のエミュレーションを避け るためにページをアンマップする。

tdp_mmu_page:

シャドウページが TDP MMU のページなら 1 である。この変数は、TDP MMU と シャドウ MMU の両方のページを含みうるデータ構造を KVM が辿るときに、制御 フローを分岐させるのに使う。

逆引き

MMU は逆引きを維持しており、gfn が与えられればそのページをマップするすべての pte に到達できる。これは例えば、ページをスワップアウトするときに使う。

同期ページと未同期ページ

ゲストは tlb とページテーブルを同期するために 2 つのイベントを使う。tlb フラッシュ とページ無効化(invlpg)である。

tlb フラッシュとは、ゲストの cr3 から到達できるすべての spte を同期する必要が あることを意味する。これはコストが高いので、すべてのゲストページテーブルを 書き込み保護したままにし、gpte が書き込まれたときに spte を gpte に同期する。

特別な場合は、ゲストページテーブルが現在のゲスト cr3 から到達できるときである。 この場合、ゲストはその変換を使う前に invlpg 命令を発行する義務がある。これを 利用して、そのゲストページから書き込み保護を外し、ゲストが自由に変更できるように する。変更された gpte は、ゲストが invlpg を呼び出したときに同期する。これにより、 ゲストが複数の gpte を変更したり、ゲストページがページテーブルとして使われなく なり任意のゲストデータに使われたりするときに、行わなければならないエミュレー ションの量が減る。

副作用として、tlb フラッシュ時に到達可能な未同期シャドウページをすべて再同期する 必要がある。

イベントへの対応

  • ゲストのページフォールト(または npt のページフォールト、ept 違反)

これは最も複雑なイベントである。ページフォールトの原因は次のいずれかである:

  • 真のゲストフォールト(ゲストの変換がそのアクセスを許さない) (*)

  • 存在しない変換へのアクセス

  • 保護された変換へのアクセス - ダーティページを記録するとき、メモリは書き込み保護される - 同期されたシャドウページは書き込み保護される (*)

  • 変換できないメモリへのアクセス(mmio)

(*) ダイレクトモードでは該当しない

ページフォールトの処理は次のように行う:

  • エラーコードの RSV ビットがセットされている場合、ページフォールトはゲストが MMIO にアクセスしたことが原因で、キャッシュされた MMIO 情報が利用できる。

    • シャドウページテーブルを辿る

    • spte 内の世代番号が有効か確認する(後述の「MMIO spte の高速無効化」を参照)

    • 情報を vcpu->arch.mmio_gva、vcpu->arch.mmio_access、vcpu->arch.mmio_gfn に キャッシュし、エミュレータを呼び出す

  • エラーコードの P ビットと R/W ビットが両方セットされている場合、これは 「高速ページフォールト」として処理できる可能性がある(MMU ロックを取らずに 修正する)。Documentation/virt/kvm/locking.rst の説明を参照。

  • 必要なら、ゲストの変換(gva->gpa または ngpa->gpa)を求めるためにゲストの ページテーブルを辿る

    • 権限が足りない場合は、フォールトをゲストに返す

  • ホストページを求める

    • これが mmio 要求ならホストページは存在しない。情報を vcpu->arch.mmio_gva、 vcpu->arch.mmio_access、vcpu->arch.mmio_gfn にキャッシュする

  • 変換に対応する spte を見つけるためにシャドウページテーブルを辿り、必要に応じて 欠けている中間ページテーブルを実体化する

    • これが mmio 要求なら、mmio 情報を spte にキャッシュし、spte にいくつかの 予約ビットをセットする(kvm_mmu_set_mmio_spte_mask の呼び出し元を参照)

  • ページを未同期にしてみる

    • 成功すれば、ゲストを続行させ、gpte を変更させることができる

  • 命令をエミュレートする

    • 失敗したら、ページのシャドウを外し、ゲストを続行させる

  • 命令によって変更された変換を更新する

invlpg の処理:

  • シャドウページ階層を辿り、影響を受ける変換を捨てる

  • ゲストが近いうちに使うことを期待して、指示された変換を実体化し直してみる

ゲストの制御レジスタの更新:

  • cr3 への mov

    • 新しいシャドウルートを探す

    • 新たに到達可能になったシャドウページを同期する

  • cr0/cr4/efer への mov

    • 新しいページングモード用の mmu コンテキストを設定する

    • 新しいシャドウルートを探す

    • 新たに到達可能になったシャドウページを同期する

ホスト側の変換の更新:

  • 更新された hva を伴って mmu notifier が呼ばれる

  • 逆引きを通じて影響を受ける spte を探す

  • 変換を捨てる(または更新する)

cr0.wp のエミュレーション

tdp が有効でない場合、ホストは cr0.wp=1 を保つ必要がある。そうすることでページの 書き込み保護が、ゲストのユーザ空間ではなくゲストカーネルに対して働く。ゲストの cr0.wp=1 のときは問題にならない。しかしゲストの cr0.wp=0 のときは、gpte.u=1, gpte.w=0 の権限をどの spte にもマップできない(この意味論では、任意のゲスト カーネルのアクセスと、ユーザの読み取りアクセスを許す必要がある)。

これは、フォールトの種類に応じて権限を 2 通りの spte にマップすることで対処する:

  • カーネルの書き込みフォールト: spte.u=0, spte.w=1(カーネルの完全なアクセスを 許し、ユーザのアクセスを許さない)

  • 読み取りフォールト: spte.u=1, spte.w=0(完全な読み取りアクセスを許し、カーネルの 書き込みアクセスを許さない)

(ユーザの書き込みフォールトは #PF を発生させる)

1 番目の場合には、さらに 2 つの厄介な点がある:

  • CR4.SMEP が有効な場合: そのページをカーネルページに変えたので、カーネルが それを実行できるようになる。これは spte.nx もセットすることで対処する。ユーザ のフェッチか読み取りのフォールトが起きたら、spte.u=1 と spte.nx=gpte.nx に 戻す。これが機能するよう、KVM はシャドウページングを使っている間は EFER.NX を 1 に強制する。

  • CR4.SMAP が無効な場合: そのページはカーネルページに変えられているので、 CR4.SMAP が有効になったときに再利用できない。この事態を避けるため、CR4.SMAP && !CR0.WP をシャドウページの role にセットする。なお、CR4.SMAP が有効な場合は 考慮していない。権限チェックに失敗して KVM がゲストに直接 #PF を注入するため である。

cr0.wp=0 のときにカーネルページへ変換された spte が、cr0.wp が 1 に変わった後に カーネルから書き込まれるのを防ぐため、cr0.wp の値をページ role の一部にする。 これは、ある cr0.wp の値で作成された spte が、cr0.wp が別の値のときには使えない ことを意味する。シャドウページ探索のコードで見つからなくなるだけである。同様の 問題は、cr0.wp=0 かつ cr4.smap=0 で作成された spte を、cr4.smap を 1 に変えた後に 使うときにも起きる。これを避けるため、!cr0.wp && cr4.smap の値もページ role の 一部にする。

ラージページ

MMU は、ゲストとホストの大きいページと小さいページのすべての組み合わせを サポートする。サポートするページサイズは 4k、2M、4M、1G である。MMU は常に PAE ページングを使うため、4M ページはゲスト側でもホスト側でも、2 つの独立した 2M ページとして扱う。

大きな spte を実体化するには、4 つの制約を満たす必要がある:

  • spte が大きなホストページを指していること

  • ゲスト pte が、少なくとも同等サイズの大きな pte であること(tdp が有効なら ゲスト pte は存在せず、この条件は満たされる)

  • spte が書き込み可能になる場合、その大きなページフレームが書き込み保護された ページと重ならないこと

  • ゲストページが単一のメモリスロットに完全に収まっていること

最後の 2 つの条件を確認するため、MMU はメモリスロットと大きなページサイズごとに ->disallow_lpage の配列の組を維持する。書き込み保護されたページがあるたびに、 対応する disallow_lpage がインクリメントされ、大きな spte の実体化が妨げられる。 境界がずれたメモリスロットの末尾のフレームは、->disallow_lpage が意図的に水増し されており、決して実体化できない。

MMIO spte の高速無効化

前述の「イベントへの対応」で触れたように、kvm は MMIO 情報をリーフ spte に キャッシュする。新しいメモリスロットが追加されたり、既存のメモリスロットが変更 されたりすると、この情報は古くなる可能性があり、無効化する必要がある。これも 全シャドウページを辿る間 MMU ロックを保持する必要があり、同様の手法で スケーラビリティを高めている。

MMIO spte にはいくつかの予備ビットがあり、世代番号の保存に使う。グローバルな 世代番号は kvm_memslots(kvm)->generation に格納され、ゲストメモリの情報が変わる たびにインクリメントされる。

KVM が MMIO spte を見つけると、その spte の世代番号を確認する。spte の世代番号が グローバルな世代番号と等しくなければ、キャッシュされた MMIO 情報を無視し、 スローパスでページフォールトを処理する。

mmio spte では世代番号の保存に 18 ビットしか使わないため、オーバーフローしたとき は全ページが破棄される。

残念ながら、1 回のメモリアクセスが kvm_memslots(kvm) に複数回アクセスすることが ある。最後のアクセスは、世代番号を取り出して MMIO spte に格納するときである。 そのため、MMIO spte が古い情報に基づいて作られながら、最新の世代番号を持つことが ありうる。

これを避けるため、synchronize_srcu が戻った後で世代番号を再びインクリメントする。 したがって kvm_memslots(kvm)->generation のビット 63 は、メモリスロットの更新中に のみ 1 にセットされ、その間は一部の SRCU リーダが古いコピーを使っていることが ある。奇数世代番号で作られた MMIO spte は使いたくないが、これは MMIO spte の ビットを失わずに実現できる。世代の「更新中」ビットは MMIO spte に格納されない ため、spte から世代を取り出すときには暗黙に 0 になる。KVM が運悪く更新中に MMIO spte を作ってしまうと、その spte への次のアクセスは必ずキャッシュミスに なる。例えば、更新期間中の後続のアクセスは進行中フラグが食い違うためにミスし、 更新期間が閉じた後のアクセスは(その spte と比べて)より大きい世代番号を持つこと になる。

参考資料