ゲストが 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種類の「退出」は分離する必要がある
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 を処理する。
{"id":"kvm-exit-boundaries","title":"1回のハードウェア退出で、KVM_RUN は戻るのか?","summary":"ハードウェア退出はまず KVM に入る;カーネルが処理できる場合はゲストへ直接再進入できる。KVM_RUN が正常に返って初めて exit_reason がディスパッチされ、エラー返却の場合は先に errno を処理する。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"scenario\"><button data-mode=\"arithmetic\" data-i18n=\"arithmetic\"></button><button data-mode=\"cpuid\" data-i18n=\"cpuid\"></button><button data-mode=\"mmio\" data-i18n=\"mmio\"></button><button data-mode=\"signal\" data-i18n=\"signal\"></button></div><label class=\"check\"><input type=\"checkbox\" id=\"handled\"><span data-i18n=\"handled\"></span></label><div class=\"track-row\"><span data-i18n=\"route\"></span><div id=\"route\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"hardware\"></span><strong id=\"hardware\"></strong></div><div><span data-i18n=\"returns\"></span><strong id=\"returns\"></strong></div></div><div class=\"card\"><header data-i18n=\"result\"></header><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"running\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"userspace\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"error\"></p></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n#route{display:grid;grid-template-columns:1fr}#route .cell{min-height:65px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let mode='arithmetic';function draw(){const inKernel=$('#handled').checked,hw=mode==='cpuid'||mode==='mmio',returned=mode==='signal'||(mode==='mmio'&&!inKernel);cells('route',[{text:mode==='signal'?'guest —':'guest → '+(hw?'KVM':'guest'),cls:mode==='signal'?'empty':'on'},{text:hw?'KVM → '+(returned?'VMM':'guest'):mode==='signal'?'KVM → VMM':'KVM —',cls:hw||returned?'gold':'empty'},{text:returned?'VMM ●':'VMM —',cls:returned?'on':'empty'}]);set('hardware',Number(hw));set('returns',Number(returned));$('#handled').disabled=mode!=='mmio';set('explanation',t(mode==='signal'?'error':returned?'userspace':'running'));pressed(mode);}document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{mode=e.dataset.mode;draw();});$('#handled').onchange=draw;draw();","audio":false,"strings":{"arithmetic":"通常の算術演算","cpuid":"CPUID","error":"ret=-1; errno=EINTR。エラーを先に処理し、共有領域に残っていた前回の exit_reason を新しいデバイスリクエストとして扱わない。","handled":"MMIO にカーネルハンドラがある","hardware":"この例のハードウェア VM exit 回数","heading":"1回のハードウェア退出で、KVM_RUN は戻るのか?","intro":"実行シナリオを1つ選び、ゲスト、KVM、VMM の間で実際に跨ぐ境界を比較する。MMIO はカーネル処理の有無で切り替えられる。","mmio":"デバイス MMIO","reset":"リセット","result":"VMM が読み取れる返却情報","returns":"この例の KVM_RUN ユーザーモード返却回数","route":"このシナリオの境界パス","running":"KVM_RUN はまだ戻っていない;ゲスト実行を継続。","scenario":"実行シナリオを選択","scope":"各オプションは独立した実行スナップショットであり、操作回数は累積されない。x86 の通常の算術演算はインターセプトされない。CPUID はインターセプトされ、カーネルによって処理されるように設定されている。MMIO ハンドラの存在はオプションである。EINTR シナリオはゲスト進入前にシグナルを観察するため、この例のハードウェア退出は発生しない。これは KVM 設定の固定パスではない。","scopeLabel":"モデル範囲と規約","signal":"進入前のシグナル","userspace":"ret=0; exit_reason=KVM_EXIT_MMIO。VMM が処理した後、対応する操作を完了するために再度 KVM_RUN に入る必要がある。"}}
ファイルディスクリプタの階層
| ハンドル | 取得方法 | 主な用途 |
| KVM システム fd | open /dev/kvm | API の照会、能力の確認、VM の作成 |
| VM fd | KVM_CREATE_VM | メモリの登録、vCPU とデバイスの作成 |
| vCPU fd | KVM_CREATE_VCPU | CPU 状態の設定、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 の戻り値 | 最初に処理すること |
| -1 | errno に従ってシグナル割り込みなどのエラーを処理 |
| 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 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 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" 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 を参照してください。