hidemium's blog

日々学んだことをアウトプットする。

EVCモードで仮想マシンにCPU機能がどのように公開されるか追ってみる

vSphere環境のクラスタ内に異なるCPU世代のサーバーがあった場合に、異なるCPU世代のサーバー間でvMotionができるようにするため、Enhanced vMotion Compatibility (EVC)という機能があります。EVCを利用した場合に、ゲストOS上でどのようにCPU機能が公開されているのか追ってみたので書いてみようと思います。

構成

  • vCenter 7.0 U3
  • ESXi 7.0 Update 3
  • Intel NUC NUC7i3BNH
    • Intel(R) Core(TM) i3-7100U CPU @ 2.40GHz
  • Ubuntu 20.04

CPUの仮想化について

仮想マシンは、CPUの仮想化とハードウェアの仮想化によって実現しています。

CPUの仮想化は、ソフトウェアベースのCPU仮想化とハードウェアアシストによるCPU仮想化という方法があります。

ソフトウェアベースのCPU仮想化は、仮想マシン上で動作するOSが、実際のハードウェアリソースを直接アクセスする代わりに、仮想化ソフトウェア(ハイパーバイザー)を介してリソースにアクセスします。ソフトウェアベースのCPU仮想化は、ハードウェアリソースに依存しないため、柔軟性がありますが、パフォーマンスの低下が課題となる場合があります。

ハードウェアアシストによるCPU仮想化は、ハイパーバイザーをサポートするハードウェア機能を使用して仮想化を実現する方法です。この方式では、ハイパーバイザーはハードウェア機能を使用して、仮想マシン上で実行されるOSが直接ハードウェアリソースにアクセスできるようにします。

仮想化にハードウェア アシストを使用すると、コードを変換する必要がなくなります。その結果、システム呼び出しやトラップを多用するワークロードが、ネイティブに近い速度で実行されます。

Intel CPUで利用可能なハードウェアアシストによるCPU仮想化は、Intel VT-x(Virtualization Technology)と呼ばれています。

Intel VT-xは、仮想マシンの動作に必要なCPUリソースを割り当てるための仮想マシンモニター(VMM)と連携することによって、CPUの仮想化を実現します。

VMMと仮想マシン

VMMは、仮想マシンを制御するソフトウェアであり、CPUの仮想化を実現するために必要です。VMMは、仮想マシンの制御と監視を行い、仮想マシンがハードウェアにアクセスするための仮想的なインターフェースを提供します。

CPUのモードには、リング(Ring)と呼ばれる階層があり、0から3のレベルに分かれています。リング0は最も特権の高いレベルであり、オペレーティングシステムカーネルなどのシステムソフトウェアが実行されます。一方、リング3は最も特権の低いレベルであり、一般的なアプリケーションソフトウェアが実行されます。

VMMと仮想マシンの切り替えは、CPUのモードを変更することで実現されます。

VMMはVMX rootモードで動作し、仮想マシンはVMX non-rootモードで動作します。

VMX rootモードは、VMMが動作する特権モードであり、物理CPUに直接アクセスすることができます。このモードでは、VMMが全ての物理リソースをコントロールでき、仮想マシンの作成、削除、スケジューリングなどの制御を行うことができます。

VMX non-rootモードは、仮想マシンが動作する非特権モードです。このモードでは、仮想マシンが割り当てられた仮想リソース(CPU、メモリ、デバイスなど)にアクセスすることができます。

VMMは、VMX non-rootモードで動作する仮想マシンの動作を監視し、割り込みなどにより必要に応じて「VM Exit」という命令を発行し、CPUはVMX non-rootモードからVMX rootモードに切り替えられ、以降の処理はVMMが行います。

VMMは必要な処理が終了すると、またVMX non-rootモードに戻すため、「VM Entry」という命令を発行し、CPUに動作モードを切り替えさせます。

このモードの切り替えにはオーバーヘッドが伴いますが、Intel VT-xの仮想化支援機能によりVMMと仮想マシンの切り替えを高速化しています。

仮想CPUについて

VMMは、仮想マシンに提供するCPU情報を仮想デバイスとしてエミュレートすることで実現されます。具体的には、VMMは仮想マシンに対して、物理CPUの情報を模擬した仮想CPUを提供します。

この仮想CPUは、ハードウェアアシストによるCPU仮想化が使用されている場合は、実際の物理CPUによってバックエンドで実行されます。

仮想CPUは、物理CPUと同様に、プロセッサの機能を提供するレジスタを持ちます。VMMは、仮想マシンに提供するCPU情報を設定するために、これらのレジスタを制御します。たとえば、VMMは、仮想CPUのレジスタに物理CPUの識別子を設定したり、仮想CPUが使用できる拡張命令セットを制限することができます。

EVCについて

Enhanced vMotion Compatibility (EVC)は、vSphere環境のクラスタ内に異なるCPUのサーバーがあった場合に、異なるCPU世代を搭載したサーバー間でvMotionができるようにするためのクラスタの機能になります。

EVCは、仮想マシンが動作するCPU世代の最低限の世代を特定し、仮想CPUが使用できるCPUの機能を制限することで、異なるCPU世代を搭載したサーバー間を移行できるようになります。

vSphere6.7以降、仮想ハードウェア バージョン 14 以降では、仮想マシンごとにEVCを設定できるようになっています。

ゲストOSからCPU情報の取得方法

Linuxカーネルは、/proc/cpuinfoファイルでCPU情報を提供します。このファイルには、物理CPUの情報とともに、仮想CPUの情報が含まれています。

CPU情報を確認するには、CPUID命令を利用します。

CPUID命令は、CPUの機能やパフォーマンスを調べるために使用される命令です。CPUID命令を実行すると、EAXレジスタに値を設定し、CPUID命令を実行すると、EAXレジスタの値に基いて特定の機能を実行します。

EAXレジスタに1を設定しCPUID命令を実行すると、EAXにプロセッサのファミリータイプやモデルなどの情報(プロセッサ・シグネチャ)、EBXとEDXとECXに機能フラグを返してくれます。

EAXに格納される情報のフォーマットは以下のとおりです。

3:0 - ステッピング
7:4 - モデル
11:8 - ファミリー
13:12 - プロセッサタイプ
19:16 - 拡張モデル
27:20 - 拡張ファミリー

では、ゲストOS上で、CPUID命令を使ったCPU情報を取得してみます。EVCを設定しない状態でまずは見てみます。

PythonにCPUID命令を実行できる、cpuidというパッケージがあるため、こちらを利用します。

インタラクティブモードで、以下のように実行してみます。

$ python3 -m venv venv
$ source /home/username/project_directory/venv/bin/activate
(venv) $ pip install cpuid
(venv) $ python
Python 3.8.10 (default, Nov 14 2022, 12:59:47) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import cpuid
>>> eax, ebx, ecx, edx = cpuid.cpuid(1) # EAXレジスタに1を設定
>>> print(f"EAX: {eax:08x}")
EAX: 000806e9
>>> print(f"EBX: {ebx:08x}")
EBX: 00010800
>>> print(f"ECX: {ecx:08x}")
ECX: fffa3203
>>> print(f"EDX: {edx:08x}")
EDX: 0f8bfbff

000806e9をフォーマットに変換すると以下のようになります。

stepping id     = 0x9 (9)
model           = 0xe (14)
family          = 0x6 (6)
processor type  = primary processor (0)
extended model  = 0x8 (8)
extended family = 0x0 (0)
Extended Family Family Extended Model Model Stepping
0 0x6 0x8 0xE 0x9 Family 6 Model 142 Stepping 9

modelはExtended ModelとModelを合わせた8Eの142になります。

Modelの142はKaby Lakeに該当します。

CPUID - Intel - WikiChip

cpuinfoの値は以下のようになります。

modelが142と表示されることが分かります。

$ cat /proc/cpuinfo 
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 142
model name      : Intel(R) Core(TM) i3-7100U CPU @ 2.40GHz
stepping        : 9
microcode       : 0x84
cpu MHz         : 2399.999
cache size      : 3072 KB
physical id     : 0
siblings        : 1
core id         : 0
cpu cores       : 1
apicid          : 0
initial apicid  : 0
fpu             : yes
fpu_exception   : yes
cpuid level     : 22
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single pti ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 invpcid rdseed adx smap clflushopt xsaveopt xsavec xgetbv1 xsaves arat arch_capabilities
bugs            : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit srbds mmio_stale_data retbleed
bogomips        : 4799.99
clflush size    : 64
cache_alignment : 64
address sizes   : 45 bits physical, 48 bits virtual
power management:

EVC環境でのCPU情報の見え方

クラスタのEVCをHaswellに変更してゲストOSの状態を確認してみます。

(venv) $ python
Python 3.8.10 (default, Nov 14 2022, 12:59:47) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import cpuid
>>> eax, ebx, ecx, edx = cpuid.cpuid(1)
>>> print(f"EAX: {eax:08x}")
EAX: 000306f0
>>> print(f"EBX: {ebx:08x}")
EBX: 00010800
>>> print(f"ECX: {ecx:08x}")
ECX: fffa3203
>>> print(f"EDX: {edx:08x}")
EDX: 0f8bfbff

000306f0をフォーマットに変換すると以下のようになります。

stepping id     = 0x0 (0)
model           = 0xf (15)
family          = 0x6 (6)
processor type  = primary processor (0)
extended family = 0x0 (0)
extended model  = 0x3 (3)
Extended Family Family Extended Model Model Stepping
0 0x6 0x3 0xF 0 Family 6 Model 63 Stepping 0

Modelの63はHaswellに該当します。

CPUID - Intel - WikiChip

cpuinfoの値は以下のようになります。

modelが63と表示されることが分かります。

$ cat /proc/cpuinfo 
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 63
model name      : Intel(R) Core(TM) i3-7100U CPU @ 2.40GHz
stepping        : 0
microcode       : 0x84
cpu MHz         : 2399.999
cache size      : 3072 KB
physical id     : 0
siblings        : 1
core id         : 0
cpu cores       : 1
apicid          : 0
initial apicid  : 0
fpu             : yes
fpu_exception   : yes
cpuid level     : 13
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm cpuid_fault invpcid_single pti ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 invpcid xsaveopt arat arch_capabilities
bugs            : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit mmio_stale_data retbleed
bogomips        : 4799.99
clflush size    : 64
cache_alignment : 64
address sizes   : 45 bits physical, 48 bits virtual
power management:

機能フラグにも違いが生じていることが分かります。

EVCにより、仮想CPUのモデル名や機能を制限されていることを確認できました。

また、vCenterのクラスタVMware EVCの画面を見ると、CPU 機能セットに以下の設定があり、こちらの値で上書きされていることが分かります。EVCのVal:6やVal:0x3fはEAXの値と一致することが分かります。

名前 キー
CPU ファミリ cpuid.FAMILY Val:6
CPU モデル cpuid.MODEL Val:0x3f
CPU ステッピング cpuid.STEPPING Val:0

EVCのキーや値を見ていると、EVCで上書きしているのは、modelやfamily、機能フラグで、model nameは上書きしていないようでした。

vSphere6.5ではCPUID機能フラグという画面がありましたが、vSphere7.0では表示が変わったようです。

Linuxカーネルでの動作

Linuxカーネルでの/proc/cpuinfoのCPU情報の表示する処理について見ていきます。CPU情報を表示する処理はこちらになるようです。

show_cpuinfo()

CPUID命令を実行してCPUのmodel名やfamily名を取得しているのはこのあたりのように見えます。

cpu_detect()

model nameはこのあたりで取得してそうに見えました。

get_model_name()

VMware仮想マシンで動作しているか判別する方法

Linuxカーネルの中に、vmware_platform()という関数があり、EAXに CPUID_VMWARE_INFO_LEAF を渡すと、hyper_vendor_idに VMwareVMware と返すことが分かります。

vmware_platform()

実際に、以下のコマンドで確認すると、ハイパーバイザーとしてVMwareが使われていることを確認できます。

$ lscpu
:
Hypervisor vendor:               VMware
:

参考