virtioとパラ仮想化I/Oの仕組み
ゲストI/Oが遅い原因はVM exitの乱発。virtioが共有リングでI/Oをまとめ、vhostでデータ経路をカーネルへ降ろす原理を押さえれば、仮想マシンの性能勘所が掴めます。
- 1.virtioは実機の模倣をやめ、ゲストとホストが共有メモリ上のvirtqueue(記述子テーブル+available/used ring)でI/Oをやり取りする準仮想化デバイスである。
- 2.ゲストは記述子をリングへ積んでから1回だけkickで通知し、ホストはまとめて処理してused ringへ完了を積む。これでVM exitと割り込みをバッチ化して削減する。
- 3.vhost-net/vhost-blkはvirtqueueの処理をホストカーネル内のスレッドへ降ろし、QEMUへの往復を消す。通知はeventfd(kick/call)、データはDMA的にカーネルが直接触る。
なぜエミュレートデバイスは遅いのか
KVM/QEMUが e1000 のような実在 NIC をエミュレートすると、ゲストは本物のハードウェアと同じ手順でデバイスを叩きます。送信ディスクリプタの設定、レジスタ書き込み、ステータスの読み出し——その一つひとつが MMIO アクセスとなり、VT-x/AMD-Vの VM exit を引き起こします。VM exit はゲストの実行を止め、CPU 状態を退避してホストへ制御を戻す高コストな遷移です。1 パケット送るだけで何度も exit が発生すれば、スループットは出ません。
問題の根は「ゲストが自分を実機だと思い込んでいる」点にあります。準仮想化(パラ仮想化)はこの前提を捨て、ゲストに「自分は仮想環境だ」と認めさせることで、仮想化に最適化した I/O 経路を設計します。virtio はその標準化された枠組みで、実機レジスタの忠実な模倣をやめ、ゲスト・ホスト間で合意した抽象デバイスを使います。
virtio は特定の実装ではなく、デバイス検出(virtio-pci など)、設定、データ転送の作法を定めた規格です。virtio-net(NIC)、virtio-blk(ブロックデバイス)、virtio-scsi、virtio-gpu などが同じ転送機構を共有します。共通の心臓部が次に述べる virtqueue です。
virtqueue:共有メモリ上のリングバッファ
virtio の核心は virtqueue という共有リングです。ゲスト物理メモリ上に確保され、ホスト(QEMU や vhost)も同じ領域を参照します。split virtqueue は次の三つの部分からなります。
| 構成要素 | 書く側 | 役割 |
|---|---|---|
| 記述子テーブル | ゲスト | バッファのゲスト物理アドレス・長さ・読み書き方向・連結 |
| available ring | ゲスト | ホストに処理してほしい記述子の先頭indexを並べる |
| used ring | ホスト | 処理を終えた記述子のindexと書き込み長を返す |
流れは一方通行の二本のリングで表せます。ゲストはデータバッファを記述子テーブルに登録し、その先頭 index を available ring へ積みます。記述子は next で連結でき、1 つの論理要求を複数バッファ(ヘッダ+データ+ステータス)に分けて表現します。ホストは available ring を読んで記述子をたどり、バッファに対して読み書きし、完了したら index と実際の転送長を used ring へ書き戻します。ゲストは used ring を見て完了を回収します。
ゲスト virtqueue(共有メモリ) ホスト(QEMU/vhost)
│ バッファを記述子に登録
│ 先頭indexを積む ───────────► available ring
│ kick(1回の通知)─────────► ───► 記述子をたどり処理
│ used ring ◄───── 完了index+長さを書く
│ used ringから回収 ◄─────────
│ 完了割り込み(call) ◄────────────────────────── interrupt注入
肝は バッチ化 です。多数の I/O 記述子を available ring へ積んでから、1 回だけ kick(通知) で相手に知らせます。これにより 1 要求あたりの VM exit が償却され、ホスト側も複数要求をまとめて処理できます。完了側も同様で、used ring に複数積んでから 1 回の割り込みで回収すれば、割り込み回数を抑えられます。
kickとcall:通知をいかに減らすか
リングへ積むだけでは相手は気づきません。ゲスト→ホストの通知が kick、ホスト→ゲストの通知が call です。kick は典型的には virtio-pci の通知レジスタへの書き込みで、これが VM exit(または vhost では eventfd シグナル)になります。call はホストからゲストへの割り込み注入です。
通知は高コストなので、virtio は両方向で抑制機構を持ちます。ゲストは VRING_AVAIL_F_NO_INTERRUPT で「当面、完了割り込みは要らない」とホストへ伝え、ホストは used ring を積んでも割り込みを送りません。逆に ホストは VRING_USED_F_NO_NOTIFY で「kick は要らない」とゲストへ伝え、ゲストはポーリングで処理が進んでいる間 kick を省きます。さらに新しい event_idx 機能では、「ここまで進んだら 1 回だけ通知して」という閾値を相手に渡し、通知を必要最小限にします。
これは NIC の割り込み合体(interrupt coalescing)や blk-mq のポーリングと同じ思想です。負荷が低いときは即座に通知して低遅延を保ち、負荷が高いときは相手がすでに処理ループを回しているので通知を省いてスループットを稼ぐ。トラフィックが増えるほど 1 要求あたりの通知コストが下がる、という性質が望ましいのです。
virtio-netとvirtio-blkのデータフロー
転送機構は共通でも、virtqueue の使い方はデバイスごとに異なります。virtio-net は最低 2 本のキュー、受信用(RX)と送信用(TX)を持ちます。送信ではゲストが virtio_net_hdr(チェックサムオフロードや GSO の指示を含む)とパケット本体を記述子に積んで kick し、ホストが取り出してカーネルのネットワークデータパスへ流します。受信では、ゲストがあらかじめ空バッファを available ring に並べて「ここへ書いてくれ」と提示し、ホストが届いたパケットを書き込んで used ring で返す——受信は事前にバッファを供給しておくのが要点です。マルチキュー(virtio-net-mq)では vCPU ごとに RX/TX 対を持ち、並列にさばきます。
virtio-blk は 1 本(または複数)のキューで、各記述子チェーンが 1 つの I/O 要求を表します。先頭に種別(read/write)とセクタ番号を持つヘッダ、続いてデータバッファ、末尾に 1 バイトのステータスを置く三段構成が典型です。ゲストは要求を積んで kick、ホストが実ストレージへ読み書きしてステータスとデータを返します。複数要求を同時に in-flight にできるため、深いキュー深度で並列 I/O を引き出せます。
vhost:データ経路をカーネルへ降ろす
virtqueue の処理をユーザー空間 QEMU で回すと、kick のたびにゲスト→ホストカーネル→QEMU と戻る往復が残ります。vhost-net / vhost-blk はこのデータ経路をホストカーネル内のスレッドへ移します。QEMU はセットアップ時に、virtqueue のゲスト物理アドレス、通知用の eventfd(kick 用 / call 用)、メモリ配置テーブルをカーネルへ登録するだけ。以降の実際の転送はカーネルの vhost スレッドが担います。
[制御パス] QEMU ──ioctl──► vhostカーネルモジュール
(virtqueueアドレス・eventfd・メモリマップを登録)
[データパス] ゲストkick → kick eventfd → vhostスレッドが起床
vhostスレッドがvirtqueueを直接処理(QEMUを経由しない)
完了 → call eventfd → KVMがゲストへ割り込み注入
要点は KVM の irqfd / ioeventfd との連携 です。ゲストの kick は ioeventfd 経由で直接 vhost を起こし、QEMU をまったく経由しません。完了割り込みも irqfd 経由で KVM が直接ゲストへ注入します。制御(セットアップ)は QEMU、データ転送と通知はカーネルで閉じるという二段の分業で、ユーザー空間への戻りが消えてスループットが伸びます。
vhost はユーザー空間往復を消しますが、ゲストバッファとホスト側ソケット/ブロック層の間のデータコピーは原則残ります。これをさらに削るのが vhost-user(DPDK 等のユーザー空間データプレーンへ委譲)や、デバイスを物理 NIC へ近づける vDPA、あるいは IOMMU を使った SR-IOV パススルーです。virtio はソフトウェア準仮想化の中での最適化であり、コピーゼロの万能解ではありません。
virtio=準仮想化I/Oの規格(実機模倣をやめ抽象デバイスを合意)。virtqueue=共有リング(記述子テーブル+available/used ringでI/Oをバッチ化)。kick/call=双方向の通知(抑制機構で高負荷ほど通知を削減)。vhost=データ経路をカーネルへ(eventfd/irqfdでQEMU往復を消す)。
まとめ
- エミュレートデバイスはゲストが実機作法でレジスタを叩くため MMIO 由来の VM exit が乱発し遅い。virtio は準仮想化で実機模倣をやめ、専用の効率的な I/O 経路を使う。
- virtqueue は共有メモリ上のリングで、ゲストが記述子を available ring に積み、ホストが処理して used ring へ完了を返す。複数要求を積んで 1 回 kick することで exit と割り込みをバッチ化する。
- kick(ゲスト→ホスト)と call(ホスト→ゲスト)は抑制フラグや event_idx で削減でき、高負荷時ほど 1 要求あたりの通知コストが下がる。
- vhost はデータ経路をカーネル内スレッドへ降ろし、kick/call を eventfd・irqfd 経由でやり取りして QEMU への往復を消す。制御は QEMU、転送はカーネルという分業である。
OS Article
virtioとパラ仮想化I/Oの仕組みを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
仮想化
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 5
導入後に効く点
ゲストは記述子をリングへ積んでから1回だけkickで通知し、ホストはまとめて処理してused ringへ完了を積む。これでVM exitと割り込みをバッチ化して削減する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 5
判断チェックリスト
- 自社の用途が「仮想化 / virtio」に近いか確認する。
- 強みである「virtioは実機の模倣をやめ、ゲストとホストが共有メモリ上のvirtqueue(記述子テーブル+available/used ring)でI/Oをやり取りする準仮想化デバイスである。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。