Virtioと完全仮想化・準仮想化
はじめに
Virtioは、仮想化環境において効率的にI/O操作を行うための準仮想化フレームワークです。Virtioデバイスを利用することで、完全仮想化デバイスに比べてパフォーマンスを大きく向上させることができます。
本記事では、完全仮想化・準仮想化やVirtioの基本概念やアーキテクチャ、実装について広く説明していきます。
完全仮想化と準仮想化
完全仮想化と準仮想化
Virtioについて説明する前に、完全仮想化と準仮想化の概念について軽く説明しておきます。
ハイパーバイザ型の仮想化には、「完全仮想化」と「準仮想化」の二つの方式があります。
完全仮想化
完全仮想化の簡略図
完全仮想化では、ハイパーバイザがハードウェア環境をソフトウェアで完全に再現(エミュレート)することで、ゲストOSに修正を加えることなくそのまま仮想化環境で動作させることができます。ゲストOSは疑似的なハードウェア環境を物理ハードウェア環境と認識して動作します。
上図の動作の流れは以下のようになっています。
- ハードウェアアクセス: ゲストOSがハードウェアリソースにアクセスしようとする。
- トラップ: ハイパーバイザがそのアクセスを捕捉し、制御を受け取る(VMExit)。
- エミュレーション: ハイパーバイザがハードウェア操作をソフトウェアでエミュレートする。
- 物理ハードウェア操作: エミュレーションの結果、必要に応じて物理ハードウェアへの操作(読み書きなど)を行う。
- ゲストOSに制御を戻す: ハイパーバイザがゲストOSに制御を戻し(VMEntry)、エミュレーション結果を返却する。
ゲストOSは物理デバイスを操作しているかのように動作しますが、実際はハイパーバイザがその動作をソフトウェアでエミュレートしています。
このような動作が、以下のような利点と欠点を生み出します。
利点
- ゲストOSの修正不要: ゲストOSに実装されている既存のデバイスドライバをそのまま使用できるため、ゲストOS側に修正を加える必要がない。
- 互換性: レガシーOSや特殊なOSを含め、幅広いOSをサポートすることができる。
- セキュリティ: ゲストOSが直接ハードウェアにアクセスできないので、セキュリティリスクが低減される。
- 柔軟性: 異なるハードウェア構成を容易にエミュレートできる。
欠点
- オーバーヘッドが大きい: ハードウェアアクセスの度にトラップとエミュレーションが発生するため、オーバーヘッドが大きく、性能が低下する。
- VMExitの頻発: VMExitもハードウェアアクセスの度に発生するため、I/O性能が低下する。
準仮想化
準仮想化の簡略図
準仮想化は、ゲストOSを仮想化環境で動作させることを前提として、仮想マシンのパフォーマンスを向上させるためにゲストOSに修正を加える手法です。この修正により、ゲストOSとハイパーバイザが直接連携できるようになります。
上図の動作の流れは以下のようになっています。
- ハイパーバイザと通信: ゲストOSがハードウェアリソースにアクセスする代わりに、ハイパーバイザに対して用意された専用インターフェースを通じて直接通信を行う。
- エミュレーション: ゲストOSから送られてきたリクエストを、ハイパーバイザがソフトウェアでエミュレートする。
- 物理ハードウェア操作: エミュレーションの結果、必要に応じて物理ハードウェアへの操作(読み書きなど)を行う。
- ゲストOSに制御を戻す: ハイパーバイザがエミュレーション結果をゲストOSに直接送信し、制御をゲストOSに戻す。
完全仮想化とは異なり、準仮想化では、ゲストOSは仮想化環境であることを認識した上で、用意された専用インターフェースを用いてハイパーバイザと直接通信を行います。
このような動作が、以下のような利点と欠点を生み出します。
利点
- トラップのオーバーヘッド削減: ゲストOSは仮想化環境を認識しているため、ハードウェアへの直接アクセスを試みない。代わりに、ハイパーバイザと直接通信を行う。これによって、ハイパーバイザがハードウェアアクセスをトラップする必要がなくなり、関連するオーバーヘッドが大幅に削減される。
- VMExitの回数削減: ハイパーバイザとの直接通信により、一回のVMExitで多くの操作をまとめてエミュレーションすることが可能になる。これにより、VMExitの発生頻度が大幅に減少し、全体的なパフォーマンスが向上する。
- I/O性能の改善: 特にI/O操作において、完全仮想化と比較して大幅なパフォーマンス向上が見込める。これは、I/O操作が頻繁に発生するワークロード(ネットワークデバイスやストレージデバイスなど)で特に顕著な利点となる。
欠点
- ゲストOSの修正が必要: 準仮想化に対応するために、ゲストOSのカーネルに修正を加える必要がある。
- 互換性の制限: 修正が必要なため、すべてのOSで使用できるわけではない。特に、ソースコードが利用できないOSでは適用が困難。
- セキュリティの懸念: ゲストOSとハイパーバイザ間の直接通信インターフェースにバグや脆弱性が存在した場合、セキュリティリスクが生じる可能性がある。
Virtio
基本概念
Virtioは、仮想化環境で効率的にI/O操作を行うための準仮想化フレームワークです。
効率的にI/O操作を行うために、ゲストとホストで共有されるメモリ上に配置されるリングバッファ(Virtqueue)を介して、ハイパーバイザとゲストOSのデバイスドライバが直接通信を行います。直接通信により多くの無駄なオーバーヘッドを削減でき、高速な I/O 操作が可能となります。
後で詳しく紹介しますが、Virtioの簡略化した動作の仕組みは以下のようになっています。
- 処理要求の書き込み: ゲストOSのドライバが処理要求をリングバッファに書き込み、新しい処理要求があることをハイパーバイザに通知する。
- 処理開始: ハイパーバイザがリングバッファから処理要求を読み取り、処理を開始する。
- 処理完了の通知: 処理を終えたら、割り込みを発生させてゲストOSに通知する。
- 結果の取得: ゲストOSは割り込みを受信すると、処理結果をリングバッファから取り出す。
完全仮想化デバイスのようにハードウェアに直接アクセスしようとするのではなく、Virtqueueのような特別に用意された通信手段を介してゲストOSとハイパーバイザが直接やり取りを行うのが、Virtioデバイスをはじめとする準仮想化デバイスの特徴です。
Virtioデバイスの種類
Virtioデバイスには様々な種類がありますが、いくつか代表的なものを紹介します。
- virtio-net: ネットワーク通信を実現する仮想ネットワークデバイス。高効率なパケット転送を可能にし、仮想マシンのネットワークパフォーマンスを向上させる。
- virtio-blk: ディスクI/Oを提供する仮想ブロックデバイス。ストレージの読み書き操作を最適化し、仮想マシンのディスクI/O性能を改善する。
- virtio-balloon: メモリリソースの効率的な利用を実現するメモリバルーンデバイス。ホストシステムとゲストOS間でメモリの動的な割り当てと解放を可能にし、システム全体のメモリ利用効率を高める。
- virtio-scsi: SCSIデバイスの仮想化に使用されるデバイス。複数のディスクやその他のSCSIデバイスを効率的に管理する。
- virtio-console: シリアルコンソールの仮想化に使用されるデバイス。ゲストOSとホスト間の高速なテキスト通信を可能にする。
- virtio-rng: 乱数生成デバイス。ゲストOSに高品質な乱数を提供し、暗号化操作などのセキュリティ関連タスクを支援する。
- virtio-gpu: グラフィックス処理ユニット(GPU)の仮想化に使用されるデバイス。2Dおよび3Dグラフィックス機能を仮想マシンに提供する。
アーキテクチャ
Virtioは主に三つの構成要素から成り立っています。
Frontend virtio driver
Frontend virtio driverは、ゲストOSに実装されたVirtioのデバイスドライバです。このドライバの主な役割は以下のようになっています。
- ゲストOS内でのI/O操作を抽象化する。
- Virtqueueを通じて効率的にデータ転送を行う。
- ゲストOSのアプリケーションに標準的なデバイスインターフェースを提供する。
Backend virtio driver
Backend virtio driverは、ハイパーバイザに組み込まれているドライバです。このドライバの主な役割は以下のようになっています。
- Virtqueueを通じてゲストOSのリクエストを受け取る。
- 必要に応じてI/O操作をエミュレートまたは変換する。
- ホストOSを介して物理ハードウェアに対して適切な操作を行う。
Virtqueue
Virtqueueは、Frontend virtio driverとBackend virtio driver間でデータを効率的に転送するための、共有メモリリングバッファです。Virtqueueの主な特徴は以下のようになっています。
- 効率的なI/O操作を実現し、仮想化環境におけるパフォーマンスを向上させる。
- 複数の要求を同時に処理できる非同期通信を可能にする。
- メモリのオーバーヘッドを最小限に抑えつつ、高速なデータ転送を実現する。
Virtqueueの構造
Virtqueueの構造は、以下の三つの領域に分かれています。
Descriptor Area
/**
* @struct virtq_desc
* @brief Virtqueue Descriptor
*
* This structure describes a single descriptor in a Virtqueue.
* @see https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/virtio-v1.3-csd01.html#x1-490006
*/
struct virtq_desc {
uint64_t addr; /* Address of buffer */
uint32_t len; /* Length of buffer */
uint16_t flags; /* The flags as indicated above */
uint16_t next; /* Next field if flags & NEXT */
} __attribute__((packed));
Descriptor Areaは、データ転送に使用される個々のバッファを記述するためのディスクリプタの配列です。各ディスクリプタにはバッファのアドレス、長さ、フラグ、および次のディスクリプタへのリンク情報が含まれます。
Driver Area
/**
* @struct virtq_driver
* @brief Virtqueue Driver Area
*
* This structure describes the driver area of a Virtqueue.
*
* @see https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/virtio-v1.3-csd01.html#x1-490006
*/
struct virtq_driver {
uint16_t flags;
uint16_t index;
uint16_t ring[];
} __attribute__((packed));
Driver Areaは、ゲストOSのデバイスドライバが制御する領域です。ring配列に処理したいディスクリプタのインデックスを追加することで、デバイスに処理を要求します。flagsはドライバの状態を、indexは最後に追加したディスクリプタのインデックスを示します。
Device Area
/**
* @struct virtq_device_elem
* @brief Virtqueue Device Element
*
* This structure describes an element in the device area of a Virtqueue.
*
* @see https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/virtio-v1.3-csd01.html#x1-540008
*/
struct virtq_device_elem {
uint32_t id;
uint32_t len;
} __attribute__((packed));
/**
* @struct virtq_device
* @brief Virtqueue Device Area
*
* This structure describes the device area of a Virtqueue.
*
* @see https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/virtio-v1.3-csd01.html#x1-540008
*/
struct virtq_device {
uint16_t flags;
uint16_t index;
struct virtq_device_elem ring[];
} __attribute__((packed));
Device Areaは、デバイス(またはハイパーバイザ)が制御する領域です。ring配列に処理済みのI/O処理の結果を格納したディスクリプタのインデックスとその長さを追加することで、ゲストOS側に結果が返されます。flagsはデバイスの状態を、indexは最後に処理したディスクリプタのインデックスを示します。
これらの三つの領域を使用して、ゲストOSとデバイス間で効率的なデータ転送が実現されます。
以下に、Linux仮想化環境(KVM)における簡単なVirtioの全体像を示します。
Virtioの全体像(virtio-blkの例)
- I/OリクエストをVirtqueueに追加: ゲストOSのvirtio-blkドライバが、I/OリクエストをVirtqueueに追加する。
- I/Oリクエストを追加したことを通知: ゲストOSがVirtqueueに新しいI/Oリクエストを追加したことをハイパーバイザに通知するために、特定のMMIO領域に書き込みを行う。この操作がVMExitをトリガーし、KVMのVMExitハンドラが呼び出される。
- Virtioバックエンドをスケジュール: KVMが制御を受け取り、Virtioバックエンドをスケジューリングする。
- I/Oリクエストを取得: Virtioバックエンドが、VirtqueueからゲストOS側のドライバが追加したI/Oリクエストを取得する。
- 物理デバイスに対する操作: I/Oリクエストに応じて、Virtioバックエンドはシステムコールを通じてホストOSに読み書き依頼を行う。ホストOSのデバイスドライバが物理デバイスに対して実際の読み書き操作を実行する。
- 処理結果をVirtqueueに追加: 操作の完了後、Virtioバックエンドは処理結果をVirtqueueに追加する。ゲストOSはこの結果を取得することで、デバイスの読み書きが正常に行われたかを確認できる。
利点
- 高いパフォーマンス: 準仮想化技術を用いることで、完全仮想化と比較して大幅にパフォーマンスが向上する。ハードウェアエミュレーションのオーバーヘッドを削減し、I/O性能が高まる。
- リソースの効率的な利用: 共有のメモリを使用することで、ホストとゲスト間のデータコピーを最小限に抑えることができる。これによりメモリ使用量が削減され、システム全体の効率が向上する。
- 柔軟性と拡張性の高さ: ゲストOSで一度Virtioのデバイスドライバを実装してしまえば、Virtioの別のデバイスタイプのデバイスドライバの実装が容易になる[1]。また、ハードウェアに依存しないので移植性も高くなる。
- 標準化: Virtioは、Open Virtualization Alliance (OVA) によって標準化されており、多くの仮想化プラットフォームで採用されているので、異なるベンダー間での互換性が高くなる。
欠点
- ゲストOSのサポート要件: VirtioをサポートしていないゲストOSでは使用することができない。ゲストOSにVirtioドライバが組み込まれている必要がある。
- セキュリティ上の懸念: ゲストOSとホストOS間で直接通信を行うため、悪意のあるゲストOSがVirtioのインターフェースを通じてホストシステムに影響を与える可能性がある。適切な対策が必要。
- デバッグの難しさ: 完全仮想化環境に比べて、準仮想化環境でのデバッグが少し難しく感じた。[2]
実装
私が実装したvirtio-blkのデバイスドライバの実装を以下に載せておきます。[3]にも対応させています。
この実装では、PCIデバイスとして実装されているvirtio-blk-pciのデバイスドライバを実装しています。あとはMSI-X割り込み実装の詳細説明は長くなるため、本記事では割愛しますが、需要があれば別の記事として書こうと思います。
おわりに
完全仮想化・準仮想化とVirtioについて浅く広く紹介しました。またもう少し深掘りした記事を書こうと思いますが、Virtioのデバイスドライバは仕様書も読みやすく、割とすんなり実装できるので、詳しくVirtioについて理解したい方は仕様書を読みながらデバイスドライバを一度実装してみても良いかもしれません。
参考
Discussion