KVMの準仮想化機能
KVMにはいくつか準仮想化インタフェースが存在します. KVMはHWによる仮想化支援機構を利用してゲストを実行するので,準仮想化機能を使用しなくても任意のOSが実行できますが,準仮想化機能を利用することでVMのパフォーマンスを向上できる場合があります.以下のような機能があります.
- Paravirtualized clock
- Asynchronous page fault
- Paravirtualized EOI
- Paravirtualized spin lock
- Paravirtualized tlb shootdown
- Paravirtualized send IPI
当然といえば当然ですがLinuxはKVMの準仮想化機能をサポートしているので,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_CLOCKSOURCE
とKVM_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アクセスのオーバヘッドが無視できなくなるようなので,注意が必要です:
- Sven Vermeulen, Comparing performance with sysbench: performance analysis, 2013, http://blog.siphos.be/2013/04/comparing-performance-with-sysbench-part-3/
- Takaaki Fukai, kvm-clock に時間を尋ねるのは間違っているだろうか, 2015, https://www.slideshare.net/DeepTokikane/kernel-vmkvmclock
余裕があれば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)に関しては以下が詳しいです.
- Asynchronous page faults, 2010, https://www.linux-kvm.org/images/a/ac/2010-forum-Async-page-faults.pdf
- KVM日記 - Asynchronous page fault解析, 2011, http://d.hatena.ne.jp/kvm/20110702/1309604602
また,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に関しては以下に参考になります.
- Jun Nakajima, Reviewing Unused and New Features for Interrupt/APIC Virtualization, 2012, https://blog.linuxplumbersconf.org/2011/wp-content/uploads/2012/09/2012-lpc-virt-intel-vt-feat-nakajima.pdf
- Jun Nakajima, Enabling Optimized Interrupt/APIC Virtualization in KVM, 2012, https://www.linux-kvm.org/images/7/70/2012-forum-nakajima_apicv.pdf, 2012
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
- 導入時期
- Linux 4.16 (2017)
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
から分かります.
以下に開発者による説明があります.
- Wanpeng Li, Torwards a more Scalable KVM Hypervisor, KVM Forum 2018, https://events.linuxfoundation.org/wp-content/uploads/2017/12/Update_Wanpeng-LI_Torwards-a-more-Scalable-KVM-Hypervisor.pdf
もともと仮想化環境でTLB shootdownが性能的に問題になるというのは,いくつかの研究でも報告されていました.
- K.T.Raghavendra, et al., Paravirtualization for Scalable Kernel-Based Virtual Machine (KVM), IEEE CCEM'12, https://ieeexplore.ieee.org/document/6354619
- J.Ouyang, et al., Shoot4U: Using VMM Assists to Optimize TLB Operations on Preempted vCPUs, VEE'16, https://dl.acm.org/citation.cfm?id=2892245
また,2012年には実際にLKMLでもアイディアが提示されています.
2012年の段階で何故入らなかったのか詳しい議論は追っていませんが,コア数の多いサーバの普及が進んだことで最近はこれらの問題がより鮮明になってきているのではないかと思います.
完全な予想ですがクラウド事業者はこの辺りはパッチ当てて運用してたんじゃないかなぁというような気もします.
Paravirtualized IPI
- 関連するFeature
KVM_FEATURE_PV_SEND_IPI
- 導入時期
- Linux 4.19 (2018)
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と同じ以下の資料に説明があります.
- Wanpeng Li, Torwards a more Scalable KVM Hypervisor, KVM Forum 2018, https://events.linuxfoundation.org/wp-content/uploads/2017/12/Update_Wanpeng-LI_Torwards-a-more-Scalable-KVM-Hypervisor.pdf
Paravirtualized driver
いわゆるvirtioドライバです.準仮想化というとこのことを思い浮かべる方も多いかもしれません. 複雑な実際のデバイスのエミュレーションをする代わりに,仮想化環境での動作に十分なインタフェースを提供して仮想環境でのI/Oを高速化しようというのが基本的な考えです. また,ホストとゲスト間の共有メモリを利用してデータ転送を効率化することもあります.
以下のようなドライバがあります.
これらのデバイスは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)をコンパイルする必要があるかもしれません. 多コアかつ過剰にオーバコミットしている環境で特に効果があると思われます.