KVM アーキテクチャ

このページの目次

ゲストが KVM に退出しても、必ずしも KVM_RUN がユーザー空間に戻るわけではありません。実行ループでは、⁠ハードウェア退出、カーネル処理、VMM 処理を明確に区別する必要があります。KVM は実行インターフェースを提供しており、QEMU は一般的な利用側ですが、唯一の規定ではありません。

ハードウェアの例は x86 VMX/SVM が中心ですが、他のアーキテクチャでは異なる実行レベルとメカニズムを使用します。

参照目標読書位置
責任と退出の階層最初の2節
インターフェース探知とホストスケジューリングファイルディスクリプタ、vCPU
実行と通知の境界実行ループ、割り込み注入

カーネルが実行境界を担い、VMM がマシン全体を構成する

KVM は Linux のスケジューリング、メモリ管理などのインフラストラクチャを使用し、アーキテクチャ実装を通じてハードウェア仮想化を制御します。ユーザー空間の VMM は通常、マシンの設定、ファームウェア、デバイスモデル、移行の調整を担当します。一部のデバイスデータパスはカーネル内または独立したバックエンドに入ることもあります。

x86 上の kvm モジュールにも対応するアーキテクチャコードが含まれており、kvm_intel/kvm_amd がさらにベンダーメカニズムと連携するため、kvm.ko 全体を純粋にアーキテクチャ非依存のコードと簡略化することはできません。ディレクトリ virt/kvm と arch/x86/kvm はソースコードの責任を区別するのに役立ちます。

2種類の「退出」は分離する必要がある

ハードウェア VM exit は必ずしもユーザー空間に戻らない ユーザー空間 VMM KVM_RUN を呼び出す KVM カーネル ゲストへの準備と進入 ゲスト実行 退出が必要になるまで、または中断されるまで KVM がハードウェア退出を処理 処理可能:ゲストへ再進入 ユーザー空間での処理が必要な場合にのみ戻る KVM_RUN はシグナルなどによりエラーを返す場合もあるため、ioctl の戻り値を必ず確認すること。

VMX の root/non-root とゲスト内部の CPL0/CPL3 は異なる次元です。ゲストカーネルは依然として独自の特権レベルで実行できます。通常の命令は通常、CPU によって直接実行され、ハードウェア制御により、どのイベントや操作が退出を必要とするかが指定されます。

ハードウェア VM exit はまず KVM に戻ります。KVM が処理できる場合(例えば、特定のページマッピングや割り込み状態の更新など)、ゲストへ直接再進入できます。ユーザー空間での処理が必要な場合などにのみ、KVM_RUN は戻ります。1回の KVM_RUN の間に、複数のハードウェア進入と退出が発生する可能性があります。

したがって、ioctl の回数をカウントすることは、ハードウェア VM exit のカウントに代わるものではありません。exit は必ずしも「センシティブな命令」によってトリガーされるわけではなく、外部イベントやネストされたページングの問題でもトリガーされる可能性があります。

1回のゲスト演算、CPUID、MMIO、またはホストシグナルを独立して選択し、ハードウェア退出と KVM_RUN の戻りをそれぞれカウントします。2つの境界は1つだけ跨ぐこともありますが、同じ「exit 回数」で混合してカウントすることはできません。

図を準備しています
1回のハードウェア退出で、KVM_RUN は戻るのか?

ハードウェア退出はまず KVM に入る;カーネルが処理できる場合はゲストへ直接再進入できる。KVM_RUN が正常に返って初めて exit_reason がディスパッチされ、エラー返却の場合は先に errno を処理する。

ファイルディスクリプタの階層

ハンドル取得方法主な用途
KVM システム fdopen /dev/kvmAPI の照会、能力の確認、VM の作成
VM fdKVM_CREATE_VMメモリの登録、vCPU とデバイスの作成
vCPU fdKVM_CREATE_VCPUCPU 状態の設定、KVM_RUN
デバイス fd対応するデバイス作成インターフェース具体的なカーネル仮想デバイスの設定

異なる ioctl は、正しいカテゴリの fd に使用しなければなりません。インターフェースの能力は KVM_CHECK_EXTENSION によって照会され、カーネルバージョン番号だけを見てはいけません。一部の拡張機能は明示的な有効化も必要です。VM のライフサイクルはそれに関連する参照と結びついており、重複した fd を閉じても、必ずしも即座に VM 全体が破棄されるわけではありません。KVM API

以下では VM を作成せずに、インターフェースのみを探知します。

#include <fcntl.h>
#include <linux/kvm.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>

int main(void) {
    int fd = open("/dev/kvm", O_RDWR | O_CLOEXEC);
    if (fd == -1) { perror("open /dev/kvm"); return 1; }
    int version = ioctl(fd, KVM_GET_API_VERSION, 0);
    if (version < 0) { perror("KVM_GET_API_VERSION"); close(fd); return 1; }
    if (version != KVM_API_VERSION) {
        fprintf(stderr, "unsupported KVM API: %d\n", version);
        close(fd); return 1;
    }
    int memory = ioctl(fd, KVM_CHECK_EXTENSION, KVM_CAP_USER_MEMORY);
    if (memory < 0) { perror("KVM_CHECK_EXTENSION"); close(fd); return 1; }
    int bytes = ioctl(fd, KVM_GET_VCPU_MMAP_SIZE, 0);
    if (bytes < 0) { perror("KVM_GET_VCPU_MMAP_SIZE"); close(fd); return 1; }
    printf("API=%d user_memory=%d vcpu_mmap=%d\n", version, memory, bytes);
    close(fd);
    return 0;
}

kvm_probe.c として保存し、cc -D_GNU_SOURCE -Wall -Wextra kvm_probe.c -o kvm_probe でコンパイルします。/dev/kvm が存在しない、権限がない、インターフェースの能力がサポートされていないは異なる問題であり、診断時には具体的なエラーを保持してください。

vCPU はホストによってスケジューリングされる実行コンテキスト

QEMU の一般的な KVM モードでは、各 vCPU に対応するホストスレッドが用意されます。vCPU の数は物理コアの数ではなく、スレッドはプリエンプトされたり、異なる CPU に移行したり、I/O や割り込みを待ったりする可能性があります。CPU ピンニングや NUMA 配置は結果に影響を与えます。

KVM はスケジューリング通知などのメカニズムを使用して、ホストの切り替え時にアーキテクチャの実行状態を維持します。steal time、仮想クロック、TSC offset/scaling にはそれぞれ具体的なインターフェースがあり、スレッド切り替えのたびに「すべての時間オフセットを再計算する」と記述することはできません。

HLT は、ゲストが一時的に作業を持っていないことを示し、カーネル内でブロックするかユーザー空間に戻るかは設定に依存します。これはホストの消費電力を制限するためのアイドルインジェクションとは異なります。

実行ループではエラーと完了プロトコルを処理する

KVM_RUN の戻り値最初に処理すること
-1errno に従ってシグナル割り込みなどのエラーを処理
0次に今回の exit_reason に従ってディスパッチ

エラー返却時、まだ前回の退出に属している可能性のある共有 exit_reason を盲目的に読んではいけません。

0 が返された後、ユーザー空間は KVM_EXIT_IO、KVM_EXIT_MMIO などの退出原因に従って処理を行います。一部の I/O 操作は、再度 KVM_RUN に入って初めてカーネル側で完了します。状態の保存や移行の準備をする際には、これらの未完了の操作を考慮する必要があります。

1つの入力命令が跨ぐ必要がある完了の境界

ゲストが「ポートから1バイト読み取り、結果に1を足す」を実行すると仮定します。デバイスモデルがユーザー空間にある場合、最初の KVM_RUN が KVM_EXIT_IO で返された後、VMM はそのポートの仮想デバイスを見つけ、共有実行領域に値 7 を書き込みます。しかし、「バッファに 7 が書き込まれた」と「ゲストの入力命令が結果を提出した」は、依然として2つの状態です。

段階VMM が完了したことこれに基づいて断定できないこと
I/O 退出を受信方向、ポート、幅、数量がわかるゲストがデバイスの戻り値を取得した
入力バッファの記入デバイスモデルが値 7 を提供したレジスタと命令の進行状況が移行可能な完全な状態を形成した
KVM_RUN の再呼び出しカーネルが保留中の操作を完了した後のゲスト命令が確実に実行された
実行の許可ゲストが結果を使用して加算を完了できるこの vCPU の状態だけでマシン全体を移行できる
デバイス結果を記入した後、KVM に保留中の命令を完了させる必要がある I/O 退出 ゲストが入力を待機 VMM が 7 を記入 共有バッファが準備完了 再進入して早期返却 入力は完了したが、まだ加算されていない 再度実行を許可 ゲストが計算して 8 を得る レジスタ、デバイスバッファ、カーネルの保留状態を一緒に考慮する必要があることがわかる 早期返却の例では immediate_exit を使用している;対応する能力を照会し、ioctl の戻り値を処理する必要がある。

KVM API は、このような操作がカーネルに再進入して完了することを明確に要求しています。完了後、次のゲスト命令の前に停止する必要がある場合は、能力サポートに応じて immediate_exit または保留中の未マスクシグナルを使用できます。保留中の操作の完了は、後のシグナルチェックの前に発生します。immediate_exit は KVM_CAP_IMMEDIATE_EXIT によってサポートされているか照会する必要があります。移行には、他の vCPU、デバイス、メモリとの調整も必要であり、ここでは見落としがちな局所的な境界の1つだけを処理します。KVM 実行領域と I/O 完了規約

以下は、3つの抽象命令 IN → ADD → HALT の状態機械です。pc は命令番号であり、x86 のバイトアドレスではありません。これは完了順序を示すために使用され、実際の命令デコード、ハードウェア退出、または KVM ioctl をシミュレートするものではありません。

import errno

class GuestModel:
    def __init__(self):
        self.pc = 0
        self.acc = 0
        self.waiting_input = False
        self.input_byte = None
        self.exit_reason = None

    def run(self, immediate_exit=False):
        # 前回の退出による入力操作を先に完了させ、その後で今回の早期返却を処理する。
        if self.waiting_input:
            if self.input_byte is None or not 0 <= self.input_byte <= 255:
                raise ValueError("先に入力バイトを1つ提供する必要がある")
            self.acc = self.input_byte
            self.pc += 1
            self.waiting_input = False
            self.input_byte = None
        if immediate_exit:
            return -1, errno.EINTR  # exit_reason は今回の新しい退出を表さない
        while True:
            if self.pc == 0:
                self.waiting_input = True
                self.exit_reason = "IO_IN"
                return 0, 0
            if self.pc == 1:
                self.acc = (self.acc + 1) & 255
                self.pc += 1
            else:
                self.exit_reason = "HALT"
                return 0, 0

for value in (0, 7, 255):
    guest = GuestModel()
    assert guest.run() == (0, 0) and guest.exit_reason == "IO_IN"
    guest.input_byte = value
    assert (guest.pc, guest.acc, guest.waiting_input) == (0, 0, True)
    assert guest.run(immediate_exit=True) == (-1, errno.EINTR)
    assert (guest.pc, guest.acc, guest.waiting_input) == (1, value, False)
    assert guest.exit_reason == "IO_IN"  # 古いフィールドはまだ残っており、新しい I/O として再度処理してはならない
    assert guest.run() == (0, 0) and guest.exit_reason == "HALT"
    assert (guest.pc, guest.acc) == (2, (value + 1) & 255)

値 7 の軌跡は、入力が完了した後、pc=1, acc=7 で停止し、実行を継続して初めて 8 が得られます。バッファへの書き込み後、カーネルに再進入する前にレジスタを保存すると、保留操作の隠れた状態が通常の可視状態によって完全に表現されないため、復元側で同じ操作を継続できない可能性があります。

モデル内の2回目の呼び出しは、もう1つの詳細を示しています。EINTR は、それまでのすべての作業が発生しなかったことを意味するわけではありません。このラウンドでは、前回の保留操作が完了している可能性があります。しかし、それは新しい成功した退出を提供するものでもなく、残っていた IO_IN フィールドに基づいてデバイスモデルを再度呼び出すことはできません。通常の運行ループでは、まず ioctl の戻り値を確認し、どの退出フィールドを読むかを決定する必要があります。

I/O データはオフセットと数量に基づいて解釈される

KVM_EXIT_IO の data_offset は、struct kvm_run のマッピング起点に対する相対位置であり、データは count 個、各 size バイトのコンパクトな配列です。例えば size=2、count=3 は 6 バイトを意味し、最初の 16 ビット値だけを処理するわけではありません。OUT 時、VMM はこのデータを読み取り、IN 時、VMM はデバイスの戻り値を同じ領域に書き戻します。

オフセットはゲスト仮想アドレスでもゲスト物理アドレスでもありません。これを直接ゲストページテーブルの照会に使用すると、間違ったアドレス空間を参照してしまいます。MMIO 退出は、物理アドレス、長さ、data を含む別のフィールドセットを使用しており、ポート I/O のレイアウトと混同してはいけません。共有領域の処理中は、同じ vCPU の実行とデバイス処理が事前に定義されたスレッドプロトコルに従って直列化されることを保証し、KVM_RUN に再進入しながら前のラウンドのデータを書き換えないようにしてください。

共有実行領域は完全なレジスタミラーではない

KVM_GET_VCPU_MMAP_SIZE は vCPU 共有マッピングのサイズを提供しますが、1ページであると勝手に想定してはいけません。struct kvm_run は実行制御と退出データのために使用され、汎用の完全なレジスタミラーではありません。レジスタは通常、対応する ioctl、または明確にサポートされ有効化された同期メカニズムを通じて処理されます。

割り込み注入はゲストが受信可能かどうかにかかっている

デバイスの完了、仮想タイマー、vCPU 間の IPI はすべて仮想割り込みを生成できます。保留状態の設定は、次のゲスト命令の前に即座に配信されることを保証するものではなく、ゲストの割り込みマスク、優先度、およびハードウェア注入条件に適合する必要があります。kick は vCPU が新しい作業を処理することを促すために使用されますが、無条件で即座に処理プログラムを実行するわけではありません。

カーネル irqchip、irqfd、およびハードウェア割り込み仮想化は、一部のユーザー空間の往復を削減できますが、パスとアーキテクチャの能力に関連しています。アドレス変換については メモリとデバイスの仮想化 を、スレッドと移行の協調については QEMU と KVM を参照してください。