NVDIMMとACPI

DIMMに刺せる不揮発性メモリ(NVDIMM*1 )はACPIからどのように扱われるかというメモ.

TL;DR

ACPIのNFIT (NVDIMM Firmware Interaface Table)に情報が入っています.

QEMUでのエミュレーション

QEMUのNVDIMMエミュレーションで確認します.

今回は以下のようなオプションで試しました.

     -machine pc,nvdimm \
     -m 16G,slots=2,maxmem=32G \
     -object memory-backend-file,id=mem1,share,mem-path=/home/work/vm/nvdimm-2,size=4G,align=128M \
     -device nvdimm,memdev=mem1,id=nv1,label-size=2M \
     ...

オプションの詳細は上記のQEMUのドキュメントを確認ください. 詳細は確認していませんが,どうもQEMU的には最初hotplug可能なメモリスロットを用意しておいて,そこにNVDIMMのデバイスを接続するという形態を取っているようです.

mem-pathで指定したパスに,指定したサイズだけのファイルが作成されます.

これでLinuxを起動すると,自動でNVDIMMが認識されます.

$ dmesg | grep pmem
[    7.284102] pmem0: detected capacity change from 0 to 4160749568
$ ls /dev/pmem0
/dev/pmem0

ACPIの情報は以下のように確認できます(iaslubuntuならsudo apt install acpica-toolsで入ります).

% sudo cp /sys/firmware/acpi/tables/NFIT .
% sudo iasl -d NFIT
% cat NFIT.dsl

/*
 * Intel ACPI Component Architecture
 * AML/ASL+ Disassembler version 20180105 (64-bit version)
 * Copyright (c) 2000 - 2018 Intel Corporation
 * 
 * Disassembly of NFIT, Tue Apr  2 06:10:17 2019
 *
 * ACPI Data Table [NFIT]
 *
 * Format: [HexOffset DecimalOffset ByteLength]  FieldName : FieldValue
 */

[000h 0000   4]                    Signature : "NFIT"    [NVDIMM Firmware Interface Table]
[004h 0004   4]                 Table Length : 000000E0
[008h 0008   1]                     Revision : 01
[009h 0009   1]                     Checksum : 40
[00Ah 0010   6]                       Oem ID : "BOCHS "
[010h 0016   8]                 Oem Table ID : "BXPCNFIT"
[018h 0024   4]                 Oem Revision : 00000001
[01Ch 0028   4]              Asl Compiler ID : "BXPC"
[020h 0032   4]        Asl Compiler Revision : 00000001

[024h 0036   4]                     Reserved : 00000000

[028h 0040   2]                Subtable Type : 0000 [System Physical Address Range]
[02Ah 0042   2]                       Length : 0038

[02Ch 0044   2]                  Range Index : 0002
[02Eh 0046   2]        Flags (decoded below) : 0003
                   Add/Online Operation Only : 1
                      Proximity Domain Valid : 1
[030h 0048   4]                     Reserved : 00000000
[034h 0052   4]             Proximity Domain : 00000000
[038h 0056  16]           Address Range GUID : 66F0D379-B4F3-4074-AC43-0D3318B78CDB
[048h 0072   8]           Address Range Base : 0000000440000000
[050h 0080   8]         Address Range Length : 00000000F8000000
[058h 0088   8]         Memory Map Attribute : 0000000000008008

[060h 0096   2]                Subtable Type : 0001 [Memory Range Map]
[062h 0098   2]                       Length : 0030

[064h 0100   4]                Device Handle : 00000001
[068h 0104   2]                  Physical Id : 0000
[06Ah 0106   2]                    Region Id : 0000
[06Ch 0108   2]                  Range Index : 0002
[06Eh 0110   2]         Control Region Index : 0003
[070h 0112   8]                  Region Size : 00000000F8000000
[078h 0120   8]                Region Offset : 0000000000000000
[080h 0128   8]          Address Region Base : 0000000000000000
[088h 0136   2]             Interleave Index : 0000
[08Ah 0138   2]              Interleave Ways : 0001
[08Ch 0140   2]                        Flags : 0000
                       Save to device failed : 0
                  Restore from device failed : 0
                       Platform flush failed : 0
                            Device not armed : 0
                      Health events observed : 0
                       Health events enabled : 0
                              Mapping failed : 0
[08Eh 0142   2]                     Reserved : 0000

[090h 0144   2]                Subtable Type : 0004 [NVDIMM Control Region]
[092h 0146   2]                       Length : 0050

[094h 0148   2]                 Region Index : 0003
[096h 0150   2]                    Vendor Id : 8086
[098h 0152   2]                    Device Id : 0001
[09Ah 0154   2]                  Revision Id : 0001
[09Ch 0156   2]          Subsystem Vendor Id : 0000
[09Eh 0158   2]          Subsystem Device Id : 0000
[0A0h 0160   2]        Subsystem Revision Id : 0000
[0A2h 0162   1]                 Valid Fields : 00
[0A3h 0163   1]       Manufacturing Location : 00
[0A4h 0164   2]           Manufacturing Date : 0000
[0A6h 0166   2]                     Reserved : 0000
[0A8h 0168   4]                Serial Number : 00123456
[0ACh 0172   2]                         Code : 0301
[0AEh 0174   2]                 Window Count : 0000
[0B0h 0176   8]                  Window Size : 0000000000000000
[0B8h 0184   8]               Command Offset : 0000000000000000
[0C0h 0192   8]                 Command Size : 0000000000000000
[0C8h 0200   8]                Status Offset : 0000000000000000
[0D0h 0208   8]                  Status Size : 0000000000000000
[0D8h 0216   2]                        Flags : 0000
                            Windows buffered : 0
[0DAh 0218   6]                    Reserved1 : 000000000000

Raw Table Data: Length 224 (0xE0)

  0000: 4E 46 49 54 E0 00 00 00 01 40 42 4F 43 48 53 20  // NFIT.....@BOCHS 
  0010: 42 58 50 43 4E 46 49 54 01 00 00 00 42 58 50 43  // BXPCNFIT....BXPC
  0020: 01 00 00 00 00 00 00 00 00 00 38 00 02 00 03 00  // ..........8.....
  0030: 00 00 00 00 00 00 00 00 79 D3 F0 66 F3 B4 74 40  // ........y..f..t@
  0040: AC 43 0D 33 18 B7 8C DB 00 00 00 40 04 00 00 00  // .C.3.......@....
  0050: 00 00 00 F8 00 00 00 00 08 80 00 00 00 00 00 00  // ................
  0060: 01 00 30 00 01 00 00 00 00 00 00 00 02 00 03 00  // ..0.............
  0070: 00 00 00 F8 00 00 00 00 00 00 00 00 00 00 00 00  // ................
  0080: 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00  // ................
  0090: 04 00 50 00 03 00 86 80 01 00 01 00 00 00 00 00  // ..P.............
  00A0: 00 00 00 00 00 00 00 00 56 34 12 00 01 03 00 00  // ........V4......
  00B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  // ................
  00C0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  // ................
  00D0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  // ................

ちなみに,Proximity DomainがNUMAドメインの情報です.

(補足) SRATとの関連

ACPIのSRAT (System Resource Affinity Table)に,NUMAノードの情報や,メモリの物理アドレス,サイズなどの情報が格納されています.このテーブルはNUMAなマシンでないと存在しないかもしれません.

Linuxでは以下のようにしてSRATのMemory Affinityの情報が確認できます. 先ほどと同じQEMU上のゲストで実行すると以下のようになります.

% sudo cp /sys/firmware/acpi/SRAT .
% sudo iasl -d SRAT
% cat SRAT.dsl | grep -A13 "Memory Affinity" | grep -B10 -A3 "Enabled : 1"
[1B0h 0432   1]                Subtable Type : 01 [Memory Affinity]
[1B1h 0433   1]                       Length : 28

[1B2h 0434   4]             Proximity Domain : 00000000
[1B6h 0438   2]                    Reserved1 : 0000
[1B8h 0440   8]                 Base Address : 0000000000000000
[1C0h 0448   8]               Address Length : 00000000000A0000
[1C8h 0456   4]                    Reserved2 : 00000000
[1CCh 0460   4]        Flags (decoded below) : 00000001
                                     Enabled : 1
                               Hot Pluggable : 0
                                Non-Volatile : 0
[1D0h 0464   8]                    Reserved3 : 0000000000000000

[1D8h 0472   1]                Subtable Type : 01 [Memory Affinity]
[1D9h 0473   1]                       Length : 28

[1DAh 0474   4]             Proximity Domain : 00000000
[1DEh 0478   2]                    Reserved1 : 0000
[1E0h 0480   8]                 Base Address : 0000000000100000
[1E8h 0488   8]               Address Length : 00000000BFF00000
[1F0h 0496   4]                    Reserved2 : 00000000
[1F4h 0500   4]        Flags (decoded below) : 00000001
                                     Enabled : 1
                               Hot Pluggable : 0
                                Non-Volatile : 0
[1F8h 0504   8]                    Reserved3 : 0000000000000000

[200h 0512   1]                Subtable Type : 01 [Memory Affinity]
[201h 0513   1]                       Length : 28

[202h 0514   4]             Proximity Domain : 00000000
[206h 0518   2]                    Reserved1 : 0000
[208h 0520   8]                 Base Address : 0000000100000000
[210h 0528   8]               Address Length : 0000000340000000
[218h 0536   4]                    Reserved2 : 00000000
[21Ch 0540   4]        Flags (decoded below) : 00000001
                                     Enabled : 1
                               Hot Pluggable : 0
                                Non-Volatile : 0
[220h 0544   8]                    Reserved3 : 0000000000000000

[228h 0552   1]                Subtable Type : 01 [Memory Affinity]
[229h 0553   1]                       Length : 28

[22Ah 0554   4]             Proximity Domain : 00000000
[22Eh 0558   2]                    Reserved1 : 0000
[230h 0560   8]                 Base Address : 0000000440000000
[238h 0568   8]               Address Length : 0000000440000000
[240h 0576   4]                    Reserved2 : 00000000
[244h 0580   4]        Flags (decoded below) : 00000003
                                     Enabled : 1
                               Hot Pluggable : 1
                                Non-Volatile : 0
[248h 0584   8]                    Reserved3 : 0000000000000000

で,ここのMemory Affinity StructureにNonVolatileというフィールドがあります. なのでこのフィールドからもNVDIMMかどうか判定できるのでは? という気がしますが,とりあえずQEMUでは上記のようにNonVolatileな領域はありません(一番最後のフィールドがアドレス的にNVDIMMデバイスが追加されるhotplug可能領域だと思います).

これはQEMUがhotplug可能領域にNVDIMMデバイスを指すという構成を取っているからであり,他のデバイスでは異なるのかもしれません.よく分かりません.

とりえず,NVDIMMの情報を取得するにはNFITを見るのが確実そうです.

(補足) Linuxでのエミュレーション

Linuxはブートオプションでmemmap=16G!16Gなどとすると,LinuxDRAMのアドレス16Gから始まる16GBの領域をPMEMの領域として管理するようになります(パラメータの詳細はこちら *2).

これはACPI的には何も変化しておらず,あくまでLinuxがメモリの一部の領域をNVDIMMとして扱っていることになります. このことは/sys/firmware/acpi/tables以下にNFITがないことからも分かります.

参考文献

*1:最近はNVDIMMよりpersistent memoryとかNVMM(Non-volatile main memory)という言い方の方が多いような気がしなくもないですが,ACPIはNVDIMMという単語を使っているのでそれに倣います.

*2:ちなみにmemmapオプションで特定のNUMAノードから領域の確保が指定できないのか探してみたのですが,よく分かりませんでした.自分でSRATの物理アドレスを確認して指定するしかないのでしょうかね .

クロスプラットフォームなハイパーバイザ Intel HAXM

HAXM (Hardware Accelerated Execution Manager)Intelが作成しているハイパーバイザです.もとはAndroid Emulatorのバックエンドとして開発が開始されたもののようです. 以下のような特徴があります.

細かい違いは当然ありますが,HAXMはKVMやHypervisor.Frameworkのようなインタフェースを提供します. WindowsではデバイスドライバMacではkernel extension, Linuxではカーネルモジュールとして実装されており,それに対してioctlでやりとりすることになります.

HAXMはもともとWindowsMacがサポート対象でしたが,気づいたら昨年にLinuxのサポートが追加されていたので,今回試してみました.

ドキュメント

HAXMといえばドキュメントが全く無くて一体どうやって使うんだという感じでしたが,Linuxサポート追加と同時期にドキュメントも(少し)追加されて,人間に多少は優しくなりました.

特に,HAXMのAPIに関するドキュメントはここにあります.

Linuxでの利用

インストール

マニュアルに従ってインストール(make && make install)するだけです.簡単.

インストールが完了すると,/dev/HAXというデバイスが作成されています. Linuxではこのデバイスに対してioctlでやりとりすることになります.

QEMUからの利用

QEMUのHAXMサポートはもともとWindows用に前からありましたが,Linux向けの対応は昨年の11月と比較的最近に入ったので,おそらく自分でQEMUコンパイルする必要があると思います. ./configure --enable-haxをつけてコンパイルします.

QEMUからHAXMを利用する場合は起動オプションとして-accel haxをつけます (KVMの場合は-accel kvm). とりあえず,Ubuntuなゲストは普通に起動しました.

ちなみに,原因は調査していませんが,vcpuを17個以上作成しようとするとsegvしました (2019-3-23追記: QEMUのソースの中で最大のvCPU数が16個になっていました). また,-cpu hostKVMでないと使用できないようです. KVMとHAXMの共存(同時実行)はできません.

HAXMを使ってVMを起動すると,以下のようなデバイスが作成されます.

/dev/hax_vm/vm00
/dev/hax_vm00/vcpu0

それぞれVMやvCPUの設定に利用します.

性能比較

KVMとHAXMで簡単なベンチマークを取ってみました. いま手元にNAS Parallel Benchmarks (NPB)の評価スクリプトがあったので,それを利用しました. vcpu数は16,メモリは32GB, KVMはVMCS shadowing及びAPICVや準仮想化機能を有効にしています. ホストのLinuxは5.0, HAXM及びQEMUは現時点のmasterのものを利用しています.

f:id:mm_i:20190322210330p:plain
NPB-OMP ベンチマーク結果

左側のグラフは実行時間のグラフ,右側はKVMを基準とした相対値のグラフ(数字はKVMの実行時間)です.3回実行の中央値をプロットしています. NPBは基本的にOpenMPを利用した数値計算ベンチマークであまりI/Oは関係ないので,今回は単純に計算中に生じたタイマ割り込みなどのVMEXITのハンドリングの速度差が出るのかと思いますが,HAXMの方が20%程度遅いベンチマークが結構あります.

機能・性能的にはやはりKVMの方がいろいろと上のようです. コードを軽くみた感じ,VMCS ShadowingやAPICVなどの機能や,KVMの準仮想化機能のようなものは入っていないように思います. ネステッドのサポートも現時点ではありません.

本当はもっといろんなマイクロベンチマークや,sysbenchなどの結果を比較をして,速度低下の要因が特定できるとよいのですが,まぁそれはまたの機会に.

APIについて

ドキュメントによると以下のようなAPIがあるようです.

  • System IOCTLs
    • HAX_IOCTL_VERSION
    • HAX_IOCTL_CAPABILITY
    • HAX_IOCTL_SET_MEMLIMIT
    • HAX_IOCTL_CREATE_VM
  • VM IOCTLs
    • HAX_VM_IOCTL_VCPU_CREATE
    • HAX_VM_IOCTL_ALLOC_RAM
    • HAX_VM_IOCTL_ADD_RAMBLOCK
    • HAX_VM_IOCTL_SET_RAM
    • HAX_VM_IOCTL_SET_RAM2
    • HAX_VM_IOCTL_NOTIFY_QEMU_VERSION
  • VCPU IOCTLs
    • HAX_VCPU_IOCTL_SETUP_TUNNEL
    • HAX_VCPU_IOCTL_RUN
    • HAX_VCPU_IOCTL_SET_MSRS
    • HAX_VCPU_IOCTL_GET_MSRS
    • HAX_VCPU_IOCTL_SET_FPU
    • HAX_VCPU_IOCTL_GET_FPU
    • HAX_VCPU_SET_REGS
    • HAX_VCPU_GET_REGS
    • HAX_VCPU_IOCTL_INTERRUPT

EPTの細かい設定だったり,VMCALLの追加だったりは直接ソースをいじる必要があるみたいです. コンパイル自体はKVMより楽だと思います.

所感

QEMUが対応していてマルチプラットフォームかつBSDライセンスなハイパーバイザはHAXMだけだと思います. 個人的にはIntel CPUに新しい機能が入った時に,リファレンス実装的な感じでその機能のサポートがHAXMに入るといろんなプロジェクトで参考にできて便利になるなと思いました. 現状は特別機能的なアドバンテージがある訳ではないようですけれども.

IntelといえばIoT向けのARCN hypervisorも作っていて,Intel的にHAXMの優先度がどうなのかはよく分かりませんが,今後も開発続くと良いですね.

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)をコンパイルする必要があるかもしれません. 多コアかつ過剰にオーバコミットしている環境で特に効果があると思われます.

KVMにおけるcpuid命令の取り扱い

前提

x86/Intelの話です.AMDでも多分同様.

cpuid命令の取り扱い

cpuid命令はKVM内で処理されます.

具体的には arch/x86/kvm/vmx.c:handle_cpuid() => arch/x86/kvm/cpuid.c:kvm_emulate_cpuid() => arch/x86/kvm/cpuid.c:kvm_cpuid() => arch/x86/kvm/cpuid.c:kvm_find_cpuid_entry()

ここで,kvm_find_cpuid_entry()で対応するcpuidのエントリがあるか探し,あれば それを返します.

このcpuidのエントリはKVM_SET_CPUIDあるいはKVM_SET_CPUID2 ioctlによって登録します.

KVM_SET_CPUID2はデータを連結リストの形で受け取るので,一回のioctlで複数の cpuidエントリが登録できます.

また,KVM_GET_SUPPORTED_CPUID iotclにより,KVMが実際にサポートするcpuidを取得できます.

QEMUでの取り扱い

target/i386/kvm.c:kvm_arch_init_vcpu()内でcpuidの設定をしています.

例えば,cpu->expose_kvmのとき (-cpu kvm=onのとき) 0x40000000にのクエリに対して"KVMKVMKVM"を返すように設定しています.

本来この領域はcpuid的にはreserved領域です.KVMはゲストにKVMの存在を伝えるために利用しています.

rustのIteratorの実装

関連するトレイト

Iterator

libcore/iter/traits/itetarot.rs

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    ...
}
  • イテレータの関数のためのトレイト
  • next()さえ実装すれば,あとはデフォルト定義が存在

IntoIterator

libcore/iter/traits/collect.rs

pub trait IntoIterator {
    type Item;
    type IntoIter: Iterator<Item=Self::Item>;
    fn into_iter(self) -> Self::IntoIter;
}

impl<I: Iterator> IntoIterator for I {
    type Item = I::Item;
    type IntoIter = I;

    fn into_iter(self) -> I {
        self
    }
}
  • IntoIteratorイテレータを得るためのトレイト
  • for _ in x としたとき,xに対してinto_iter()が呼ばれる
  • IteratorにはIntoIteratorが実装されている (単純に自分自身を返すだけ)

Slice ([T])

iter(), iter_mut()

libcore/slice/mod.rs

impl<T> [T] {
    ...
    #[stable(feature = "rust1", since = "1.0.0")]
    #[inline]
    pub fn iter(&self) -> Iter<T> {
        unsafe {
            let ptr = self.as_ptr();
            assume(!ptr.is_null());

            let end = if mem::size_of::<T>() == 0 {
                (ptr as *const u8).wrapping_add(self.len()) as *const T
            } else {
                ptr.add(self.len())
            };

            Iter {
                ptr,
                end,
                _marker: marker::PhantomData
            }
        }
    }
    ...
}
  • sliceのiter()Iter<T>を返す
    • iter_mut()も同様
  • Iter<T>の定義は以下の通り

libcore/slice/mod.rs

pub struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T, // If T is a ZST, this is actually ptr+len.  This encoding is picked so that
                   // ptr == end is a quick test for the Iterator being empty, that works
                   // for both ZST and non-ZST.
    _marker: marker::PhantomData<&'a T>,
}

macro_rules! iterator {
    ...
        impl<'a, T> Iterator for $name<'a, T> {
            type Item = $elem;

            #[inline]
            fn next(&mut self) -> Option<$elem> {
                // could be implemented with slices, but this avoids bounds checks
                unsafe {
                    assume(!self.ptr.is_null());
                    if mem::size_of::<T>() != 0 {
                        assume(!self.end.is_null());
                    }
                    if is_empty!(self) {
                        None
                    } else {
                        Some(& $( $mut_ )* *self.post_inc_start(1))
                    }
                }
            }
        ...
        }
    ...
}

iterator!{struct Iter -> *const T, &'a T, const, {/* no mut */}, {...
  • Iter<T>に対してIteratorが実装されている
    • 各要素の参照を返す
  • iter()iter_mut()で実装を共通化するためマクロで定義されている

into_iter()

  • iter(), iter_mut()を呼ぶ形で&'a [T], &'a mut [T]に実装
    • let x = [1,2,3];のとき,for loopにおいては,for _ in x.iter()for _ in &xも同じ意味
      • 前者の場合, x.iter()Iteratorを実装するIter<T>を返し,それに対してIntoIteration::into_iter()が呼ばれる
      • 後者の場合は&[T]に対してIntoIteration::into_iter()が呼ばれる.
    • [T]に対してはinto_iter()の実装なし (そもそもsliceで実際に使えるのはshared (&[T]) かmutable (&mut [T]) の形のみ)

libcore/slice/mod.rs

#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, T> IntoIterator for &'a [T] {
    type Item = &'a T;
    type IntoIter = Iter<'a, T>;

    fn into_iter(self) -> Iter<'a, T> {
        self.iter()
    }
}

#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, T> IntoIterator for &'a mut [T] {
    type Item = &'a mut T;
    type IntoIter = IterMut<'a, T>;

    fn into_iter(self) -> IterMut<'a, T> {
        self.iter_mut()
    }
}

Array ([T; N])

iter(), iter_mut()

  • coerceによってsliceのiter(), iter_mut()が呼ばれる

into_iter()

  • sliceと同様に,iter(), iter_mut()を呼ぶ形で&'a [T; N], &'a mut [T; N]に実装
    • [T; N]に対する実装はない

libcore/array.rs

#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, T> IntoIterator for &'a [T; $N] {
    type Item = &'a T;
    type IntoIter = Iter<'a, T>;

    fn into_iter(self) -> Iter<'a, T> {
        self.iter()
    }
}

#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, T> IntoIterator for &'a mut [T; $N] {
    type Item = &'a mut T;
    type IntoIter = IterMut<'a, T>;

    fn into_iter(self) -> IterMut<'a, T> {
        self.iter_mut()
    }
}

Vec (Vec<T>)

iter(), iter_mut()

  • deref coerceによってsliceのiter(), iter_mut()が呼ばれる

into_iter()

src/liballoc/vec.rs

impl<T> IntoIterator for Vec<T> {
    type Item = T;
    type IntoIter = IntoIter<T>;

    #[inline]
    fn into_iter(mut self) -> IntoIter<T> {
        unsafe {
            let begin = self.as_mut_ptr();
            assume(!begin.is_null());
            let end = if mem::size_of::<T>() == 0 {
                arith_offset(begin as *const i8, self.len() as isize) as *const T
            } else {
                begin.add(self.len()) as *const T
            };
            let cap = self.buf.cap();
            mem::forget(self);
            IntoIter {
                buf: NonNull::new_unchecked(begin),
                phantom: PhantomData,
                cap,
                ptr: begin,
                end,
            }
        }
    }
}

#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, T> IntoIterator for &'a Vec<T> {
    type Item = &'a T;
    type IntoIter = slice::Iter<'a, T>;

    fn into_iter(self) -> slice::Iter<'a, T> {
        self.iter()
    }
}

#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, T> IntoIterator for &'a mut Vec<T> {
    type Item = &'a mut T;
    type IntoIter = slice::IterMut<'a, T>;

    fn into_iter(self) -> slice::IterMut<'a, T> {
        self.iter_mut()
    }
}
  • &'a Vec<T>, &'a mut Vec<T>の場合はiter(), iter_mut()を呼ぶ
  • Vec<T>の場合はIntoIter<T>を返す
    • IntoIter<T>は以下の様に定義
    • これにより,Vec<T>では要素そのものをに対してイテレーションすることが可能 (vec自身は消費される)

src/liballoc/vec.rs

#[stable(feature = "rust1", since = "1.0.0")]
pub struct IntoIter<T> {
    buf: NonNull<T>,
    phantom: PhantomData<T>,
    cap: usize,
    ptr: *const T,
    end: *const T,
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;

    #[inline]
    fn next(&mut self) -> Option<T> {
        unsafe {
            if self.ptr as *const _ == self.end {
                None
            } else {
                if mem::size_of::<T>() == 0 {
                    // purposefully don't use 'ptr.offset' because for
                    // vectors with 0-size elements this would return the
                    // same pointer.
                    self.ptr = arith_offset(self.ptr as *const i8, 1) as *mut T;

                    // Make up a value of this ZST.
                    Some(mem::zeroed())
                } else {
                    let old = self.ptr;
                    self.ptr = self.ptr.offset(1);

                    Some(ptr::read(old))
                }
            }
        }
    }
    ...
}

Arrayに[T; N]IntoIteratorが実装されていない理由

まとめ

  • trait/struct
    • Iterationトレイト
      • next()を実装することでイテレータとして利用できるようになる
    • IntoIteratorトレイト
      • イテレータが欲しい時にtrait boundとして利用する
      • for loopはIntoIteratorが実装されているものを受け取るようになっている
    • Iter
      • sliceの&T, &mut Tに対するIterationのために存在
    • IntoIter
      • vecのTに対するIterationのために存在
  • 一般にstdでのcollectionでは
  • 現状Arrayに関してはTに対してイテレーションはできない

rustのArcについてその2

前回

以下で利用したplaygroundのリンク

スライスからのArcの作成

Arcのソースをみていて気づきましたが,impl<T: Clone> From<&[T]> for Arc<[T]>などが実装されており,CopyあるいはCloneが実装されているスライスからArcを作成することができます.(RCも同様です)

例えばこれを使うと,Arc<str> (=Arc<[u8]>)が作れます.

Arc<[T]>はfat pointerとなり,構造的には以下の様になります.

f:id:mm_i:20190214233307p:plain
Arc<T>

x86_64環境なら最初の8byteがデータへのポインタ,そのあと8byteがデータのサイズです.

作成例

let s: Arc<str> = Arc::from("str");
println!("{:?}", as_raw_bytes(&s));
println!("{:?}", as_raw_bytes(&*s));
[64, 10, 247, 79, 223, 85, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[115, 116, 114]

Arc<String>だと文字列にアクセスするまで二回ポインタを辿らないといけませんが(前回の記事参照),これなら一回で済みます.

BoxからのArcの作成

impl<T: ?Sized> From<Box<T>> for Arc<T>も実装されています. 内部動作としてはBoxの中身をArc側にmemcpyして,box側のメモリを解放することになります.

こちらの場合,CopyあるいはCloneが実装されていなくてもArcが作成できます.T: ?SizedなのでArc<[T]>が作れます.

struct A(i32);
let s: Box<[A]> = Box::new([A(0),A(1),A(2)]);
let s: Arc<[A]> = Arc::from(s);
println!("{:?}", as_raw_bytes(&s));
println!("{:?}", as_raw_bytes(&*s));
[224, 11, 247, 79, 223, 85, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0]

まぁこの場合そもそも普通に Arc::new([A(0), A(1), A(2)])すればいいんですが.

Arc<str>Stringから作成したい場合は,String::into_boxed_str()を使ってBox<str>にしてからArcにできます.

let s: Arc<str> = Arc::from(format!("str").into_boxed_str());
println!("{:?}", as_raw_bytes(&s));
println!("{:?}", as_raw_bytes(&*s));
[192, 11, 247, 79, 223, 85, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[115, 116, 114]

VecからのArcの作成

impl<T> From<Vec<T>> for Arc<[T]>もあります.

let s: Arc<[i32]> = Arc::from(vec![0,1,2]);
println!("{:?}", as_raw_bytes(&s));
println!("{:?}", as_raw_bytes(&*s));
[16, 12, 247, 79, 223, 85, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0]

こちらも動作的にはArcが新規に割り当てたメモリ領域にVecのデータを全部移すことになります.

servo_arc

servoにはservo_arcという,Arcの派生実装が含まれています.

以下の様な特徴があります.

  • weakカウントがなくてstrongカウントのみ
  • dynamically-sized type (DST)のサポート

DSTに関しては以前記事を書きましたservo_arcを使うと,以下の様なことができます.

#[derive(Debug)]
struct Header {
    a: u32,
    b: u32,
    c: u32,
};
let header = Header { a: 1, b: 2, c: 3 };
let data: Vec<u32> = vec![4, 5, 6];
let a = servo_arc::Arc::from_header_and_iter(header, data.into_iter());

println!("{:?}", &a);
println!("{:?}", as_raw_bytes(&a));
println!("{:?}", as_raw_bytes(&*a));
HeaderSlice { header: Header { a: 1, b: 2, c: 3 }, slice: [4, 5, 6] }
[240, 44, 64, 90, 168, 127, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0]

Arc::from_header_and_iter()Arc<HeaderSlice<H, [T]>>を作成します.HeaderSliceは以下の様に定義されます.

pub struct HeaderSlice<H, T: ?Sized> {
    /// The fixed-sized data.
    pub header: H,

    /// The dynamically-sized data.
    pub slice: T,
}

Arc<HeaderSlice<H, [T]>>は以下の様な構造になります.

f:id:mm_i:20190214234050p:plain
Arc<HeaderSlice<H, [T]>>

これの何が嬉しいのかというと,zero-length arrayを受け取るCの関数をFFIで呼ぶ時なんかに便利かなと思います.servoではどう使われてるんでしょう(未確認)

rustのMutexの内部構造

前回に引き続き,今度はMutexの話です.

Raph Levien氏の図を引用すると,Mutexは以下のような構造になっています.

f:id:mm_i:20190213182929p:plain
Raph Levien, Copyright 2017 Google Inc., released under Creative Commons BY

また,データ構造としては以下のように定義されています(一部抜粋).

pub struct Mutex<T: ?Sized> {
    inner: Box<sys::Mutex>,
    poison: poison::Flag,
    data: UnsafeCell<T>,
}

ここから,以下のことが分かります.

  1. Mutex自体はデータをヒープに置かない
  2. mutex本体(sys::Mutex)は,ヒープに置かれる

sys::Mutexは,*nix環境ではpthread_mutex_tです. したがって,Mutexのlock/unlockはpthread_mutex_lock()およびpthread_mutex_unlock()に対応しています. mutex本体をヒープに置くのは,OSによっては(pthreadが?)mutex本体のアドレスが変わらないことを前提としているからだそうです(このあたりの実装は未確認). ヒープに置いておかないと,Mutexをmoveしたときmutex本体のアドレスが変わってしまいます.

ちなみに,RwLockMutexとほぼ同じ構造をしています.

https://github.com/rust-lang/rust/blob/1.32.0/src/libstd/sync/rwlock.rs

pub struct RwLock<T: ?Sized> {
    inner: Box<sys::RWLock>,
    poison: poison::Flag,
    data: UnsafeCell<T>,
}

Mutexを利用するときはマルチスレッドで何かしたい場合なので,実際に利用する場合は,Arcと組み合わせてArc<Mutex<T>>のような形で利用されることが多いと思います.

parking_lot

さて,上のMutexの図には"parking_lotを使ってみたら?"と書いてあります. parking_lotはスピンロックを使ったmutexを提供します.

以下のような感じでMutexが定義されます(一部抜粋).

// lock_api/src/mutex.rc
pub struct Mutex<R: RawMutex, T: ?Sized> {
    raw: R,
    data: UnsafeCell<T>,
}

// src/mutex.rc
type Mutex<T> = lock_api::Mutex<RawMutex, T>;

// src/raw_mutex.rs
pub struct RawMutex {
    state: AtomicU8,
}

unsafe impl RawMutexTrait for RawMutex {
    ...
    #[inline]
    fn lock(&self) {
        if self
            .state
            .compare_exchange_weak(0, LOCKED_BIT, Ordering::Acquire, Ordering::Relaxed)
            .is_err()
        {
            self.lock_slow(None);
        }
        unsafe { deadlock::acquire_resource(self as *const _ as usize) };
    }
    ...
}

stdのMutexと比較すると,以下の特徴があります.

  • pthreadではなくて(adaptive)スピンロックを利用する
  • mutex本体をヒープに割り当てる必要がない
  • poisonフィールドもない
    • このフィールドは,ロックを保持したスレッドがpanicしたかどうか検出するためのもの
    • parking_lotの場合,panicしたらlockは解放されるようになっている

さて,こんなparking_lotですが,ちょうど今parking_lotをlibstdに取り込もうという動きがあるようです.

深く議論は追っていませんが,しばらくするとstdに追加されてるかもしれません(というか,OS nativeなmutexの代わりにバックエンドとして利用されることになりそう?).