KVMの準仮想化機能

KVMにはいくつか準仮想化インタフェースが存在します. KVMはHWによる仮想化支援機構を利用してゲストを実行するので,準仮想化機能を使用しなくても任意のOSが実行できますが,準仮想化機能を利用することでVMのパフォーマンスを向上できる場合があります.以下のような機能があります.

  • Paravirtualized clock
  • Asynchronous page fault
  • Paravirtualized EOI
  • Paravirtualized spin lock
  • Paravirtualized tlb shootdown
  • Paravirtualized send IPI

当然といえば当然ですがLinuxKVMの準仮想化機能をサポートしているので,KVM上でLinuxを実行する際はこれらの機能が利用可能です.

以下,x86についての話です.Linux v4.20, QEMU v3.10のコードをベースにしています.

KVM準仮想化機能の確認

KVM上のゲストはcpuid命令を利用してKVMの準仮想化機能が利用できるかどうかを調べることができます. 以下がKVMのcpuidに関するドキュメントです.

KVMではcpuidの0x40000000と0x40000001を利用して情報をゲストに伝えます. 本来この領域はcpuid的にはreserved領域です. cpuid 0x40000001のeaxにKVMの準仮想化機能のどれが利用可能化のフラグが格納されています.

cpuidの値はcpuidコマンドを利用して取得できます.(Ubuntuの場合はapt install cpuid)

% cpuid
...
   hypervisor_id = "KVMKVMKVM   "
   hypervisor features (0x40000001/eax):
      kvmclock available at MSR 0x11          = true
      delays unnecessary for PIO ops          = true
      mmu_op                                  = false
      kvmclock available a MSR 0x4b564d00     = true
      async pf enable available by MSR        = true
      steal clock supported                   = true
      guest EOI optimization enabled          = true
      stable: no guest per-cpu warps expected = true
...

ただし,このcpuidコマンドはこの記事執筆段階ではKVMのfeatures全てに対応していません. 例えば,上の出力例からはparavirtualized spinlockやparavirtualized TLB shootdown等の有効無効は分かりません.

cpuid -rを実行すると実際のレジスタの値が確認できるのでこれを使用するのが確実です.

% cpuid -r
...
   0x40000000 0x00: eax=0x40000001 ebx=0x4b4d564b ecx=0x564b4d56 edx=0x0000004d
   0x40000001 0x00: eax=0x01000afb ebx=0x00000000 ecx=0x00000000 edx=0x00000000
...

また,一部機能はMSRを利用してデータをやりとりします.MSRに関しては以下にドキュメントがあります.

以下でKVMが提供する準仮想化インタフェースについて簡単に説明します.

Paravirtualized clock

  • 関連するFeature
    • KVM_FEATURE_CLOCKSOURCE
    • KVM_FEATURE_CLOCKSOURCE2

カーネルのclocksourceとして準仮想化クロックを提供します.いわゆるkvm-clockです. kvm-clockはMSRを利用してホストにメモリアドレスを通知し,ホストがそのメモリに必要に応じて時刻を書き込むことで時刻を取得する仕組みです. 利用するMSRのアドレスはKVM_FEATURE_CLOCKSOURCEKVM_FEATURE_CLOCKSOURCE2で異なりますが,多分KVM_FEATURE_CLOCKSOURCEはdeprecatedです.

clocksourceは以下のように確認できます.

$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
kvm-clock tsc hpet acpi_pm
$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
kvm-clock

kvm-clockが利用可能な環境であればkvm-clockが利用されるようになっています.

なお,頻繁にclocksourceにアクセスするような場合はkvm-clockアクセスのオーバヘッドが無視できなくなるようなので,注意が必要です:

余裕があればkvm-clockについてはもう少し調べたいと思っています.

Asynchronous page fault

  • 関連するFeature
    • KVM_FEATURE_ASYNC_PF, KVM_FEATURE_ASYNC_PF_VMEXIT

これはゲスト的にはpage faultではないがhost側でpage faultした際,そのpage fault処理中にゲストのvCPUが別の処理を実行することを可能にする機能です.

Asynchronous page fault (APF)に関しては以下が詳しいです.

また,KVM_FEATURE_ASYNC_PF_VMEXITという機能がありますが,これはnested virtualization時にL1のKVMに対してL2のAPFを#PF VMEXITとして伝える機能のようです.

Paravirtualized EOI

  • 関連するFeature
    • KVM_FEATURE_PV_EOI

割り込みが発生した際,OSはAPICのEOI (End of Interrupt)レジスタに書き込むことで割り込み処理の完了を通知します. 仮想環境においてはAPICは仮想化されているので,ゲストがEOIにアクセスするとVMEXITが発生します.

PV-EOIはこのVMEXITを削減する機能です.

  • KVM_FEATURE_PV_EOIが利用可能な場合,特定のMSRにゲストのメモリアドレスを書き込む
  • ホストは割り込みをゲストに挿入する際,ゲストのEOIレジスタへの書き込みが必要なければMSRが指すメモリの最下位ビットに1をたてる
  • ゲストは割り込み時処理時にホストが書き込んだ内容をチェックして,その値が1ならばビットをクリアする.EOIにはアクセスしない
  • 必要であればあとでホストがそのフラグを確認して処理をおこなう.(実際のEOI書き込みをおこなうなど)

ちなみにこれはVT-xにあるAPICvとは別の機能です. APICvを利用することでAPICレジスタそのものがHW的に仮想化され多くの場合VMEXITなしでアクセスできるようになります. APICvに関しては以下に参考になります.

TODO: PV-EOIとAPICvの現在の使われ方の調査

Paravirtualized spin lock

  • 関連するFeature
    • KVM_FEATURE_PV_UNHALT

この機能を利用すると,ゲストのあるvCPUがspinlockを獲得しようとする際,ある一定時間spinしてもlockが獲得できなければ一旦そのvCPUはスリープします. 別のvCPUがlockを解放した際,もしlock待ちでsleepしているvCPUがいたら,そのvCPUをhypercall(KVM_HC_KICK_CPU)で起こします.(実際の動作はもう少し複雑)

これにより,スピン時間を他のvCPUの処理に当てることができる他,Lockを待っているvCPUがうまくスケジューリングされないという問題(Lock Waiter Preemption)も改善できます.

仮想化環境においてスピンロックを以下に効率よくハンドリングするかというのは仮想化における課題の一つで,ここ十年ほど研究が盛んにおこなわれています. VMのspinlockの話はまた別途記事にしようと思います.

Paravirtualized TLB flush

  • 関連するFeature
    • KVM_FEATURE_PV_TLB_FLUSH
  • 導入時期

x86ではコア間でTLBのコヒーレンシが保たれません. すなわち,あるコアでページテーブルを変更した場合,別のコアからもそのページテーブルを利用する場合はそのコアのTLBを明示的にフラッシュする必要があります. これをTLB shootdownと呼びます.

LinuxではTLB shootdownはIPIを用いておこないます. TLB shootdown IPIを送信したコアは,別のコアからの応答を待ちます. しかしながら,仮想化環境では以下のような問題があります.

  • あるvCPUがIPI送信時に実際に動作しているとは限らない
  • 結果としてIPIの応答が遅くなる.最悪の場合IPIを送信したvCPUがスケジューリングされてしまい,より遅延が増大する

そこで,Paravirtualized TLB flushは以下のように動作します.

  • 現在実行中のvCPUに対してはIPIを送信する
  • それ以外のvCPUに対しては次回vCPUがスケジューリングされたときに最初にTLBフラッシュするようにハイパーコールでハイパーバイザに指示する

現在どのvCPUが実行中かどうかはMSR_KVM_STEAL_TIMEから分かります.

以下に開発者による説明があります.

もともと仮想化環境でTLB shootdownが性能的に問題になるというのは,いくつかの研究でも報告されていました.

また,2012年には実際にLKMLでもアイディアが提示されています.

2012年の段階で何故入らなかったのか詳しい議論は追っていませんが,コア数の多いサーバの普及が進んだことで最近はこれらの問題がより鮮明になってきているのではないかと思います.

完全な予想ですがクラウド事業者はこの辺りはパッチ当てて運用してたんじゃないかなぁというような気もします.

Paravirtualized IPI

  • 関連するFeature
    • KVM_FEATURE_PV_SEND_IPI
  • 導入時期

x2APICでIPIを送信する際,ICRというレジスタにアクセスしますが,これはVMEXITが発生します. x2APICでIPIを送信する方法は,physical modeとcluster modeの二種類あり,後者の場合は複数のCPUに一斉にIPIを送ることができます. しかし,実際には前者が主に用いられているようです. この場合,複数のvCPUにIPIを送信する度にVMEXITが発生します.

Paravirtualized IPIでは直接ゲストから逐一IPIを送る代わりに,ハイパーコール(KVM_HC_SEND_IPI)でvCPUに対するIPIをハイパーバイザに依頼します. これにより一回のVMEXITで複数のvCPUにIPIを送ることができます.

この機能に関しても前述のPV TLB Flushと同じ以下の資料に説明があります.

Paravirtualized driver

いわゆるvirtioドライバです.準仮想化というとこのことを思い浮かべる方も多いかもしれません. 複雑な実際のデバイスのエミュレーションをする代わりに,仮想化環境での動作に十分なインタフェースを提供して仮想環境でのI/Oを高速化しようというのが基本的な考えです. また,ホストとゲスト間の共有メモリを利用してデータ転送を効率化することもあります.

以下のようなドライバがあります.

  • ネットワーク
    • virtio-net
    • vhost-net
  • ストレージ
  • その他
    • virtio-balloon

これらのデバイスvirt-managerのインタフェース等からvirtioドライバを追加して,必要に応じてドライバをインストールすれば使えるようになると思います.

KVMのvirtioデバイスの処理に関してはまた別途書こうと思います.

Linuxでの取り扱い

Linuxはブート時にハイパーバイザ上で動作しているかどうかを確認し,ハイパーバイザ上で動作していた場合はそれに応じた初期化処理を実施します.

ハイパーバイザの検知

arch/x86/kernel/setup.c:setup_arch() => kernel/cpu/hypervisor.c:init_hypervisor_platform()

ここでコールバック関数を利用してハイパーバイザの検知をしますが,KVMの場合はarch/x86/kernel/kvm.cで定義されています. 0x40000000のCPUIDが"KVMKVMKVM"であればKVM上で動作していると判断します.

準仮想化機能の利用

初期化時に,kvm_para_has_feature()を使ってそのfeatureが利用できるか確認し,もし利用できたらそれを使うように設定しています. 例えば,PV tlb flushの設定はkvm_setup_pv_tlb_flush()でおこなわれています

ブートオプションで特定の準仮想化機能をオフにできるような感じにはなっていないように見えます.

QEMUでの取り扱い

前回説明したように,KVMにおけるcpuid命令はKVM_SET_CPUID ioctlで制御されます. これを変更することで,ゲストに特定の準仮想化機能を見せないことが可能です. コードの正確な確認はできていませんが,基本的にQEMUはホストが対応している機能はゲストに見せているような気がします.

QEMUで特定の機能をオフにしたい場合は,-cpu host,-kvm-pv-tlb-flushなどと指定すれば対応するCPUIDのfeature flagがクリアされると思います. QEMUが利用するオプションについてはこちらで分かります.

またQEMUのオプションで-cpu host,kvm=off (libvirt的には<kvm><hidden state="on"></kvm>)をすると0x40000000のCPUIDはKVMを隠蔽するので,結果としてKVMの準仮想化機能は利用できなくなります.

まとめ

KVMの準仮想化機能について簡単に説明しました. 2018年にはPV TLB ShootdownやPV Send IPIなどが導入されました. 比較的最近の変更なので,現時点で利用するには自分でカーネル(とQEMU)をコンパイルする必要があるかもしれません. 多コアかつ過剰にオーバコミットしている環境で特に効果があると思われます.