ハイパーバイザの作り方~ちゃんと理解する仮想化技術~ 第12回 virtioによる準仮想化デバイス その2「Virtqueueとvirtio-netの実現」
前回は、ゲストOSのI/Oパフォーマンスを大きく改善する「virtio」準仮想化ドライバの概要と、virtioのコンポーネントの1つである「Virtio PCI」について解説しました。今回はVirtqueueとこれを用いたNIC(virtio-net)の実現方法について見ていきます。
virtioは、大きく分けてVirtio PCIとVirtqueueの2つのコンポーネントからなります。Virtio PCIはゲストマシンに対してPCIデバイスとして振る舞い、次のような機能を提供します。
これを利用してキュー長やキュー数、キューのアドレスなどを通知する、
があります。
Virtqueueはデータ転送に使われるゲストメモリ空間上のキュー構造です。デバイスごとに1つまたは複数のキューを持つことができます。たとえば、virtio-netは送信用キュー, 受信用キュー, コントロール用キューの3つを必要とします。ゲストOSは、PCIデバイスとしてvirtioデバイスを検出して初期化し、Virtqueueをデータの入出力に、割り込みとI/Oポートアクセスをイベント通知に用いてホストに対してI/Oを依頼します。本稿では、Virtqueueについてより詳しく見ていきましょう。
Virtqueueは送受信するデータをキューイング先のDescriptorが並ぶDescriptor Table、ゲストからホストへ受け渡すdescriptorを指定するAvailable Ring、ホストからゲストへ受け渡すdescriptorを指定するUsed Ringの3つからなります(図1)。
Descriptor Table, Available Ring, Used Ringのエントリ数はVirtio PCIデバイスの初期化時にVirtio headerのQUEUE_NUMへ設定した値で決められます。
また、Virtqueueの領域はページサイズ1へアラインされている必要があります。1つのVirtqueueは片方向の通信に用いられます。このため、双方向通信をサポートするには2つのVirtqueueを使用する必要があります。通信方向によって、Available RingとUsed Ringの使われ方が異なります。
Descriptor TableはDescriptorがQUEUE_NUM個2並んでいる配列です。Descriptorはデータ転送を行う都度動的にアロケートされるのではなく、Descriptor Table内の空きエントリを探して使用します。空きエントリを管理する構造はVirtqueue上にないため、ゲストドライバは空きDescriptorを記憶しておく必要があります(後述)。
Descriptorは転送するデータ1つに対して1つ使われ、データのアドレス、データ長などが含まれます(表1)。
データのアドレスはゲスト上の物理アドレスが用いられるため、仮想アドレス上で連続する領域でも物理ページがばらばらな場合、物理ページごとにDescriptorが1つ必要です。 このように複数のDescriptorを連続して転送したい場合には、nextで次のDescriptorの番号を指定してflagsに0x1をビットセットします。
type | member | description |
---|---|---|
u64 | addr | データのアドレス(ゲスト物理アドレス) |
u32 | len | データ長 |
u16 | flags | フラグ |
(0x1: 次のDescriptorがあるかどうか | ||
0x2: ホストから見てWrite OnlyのDescriptorかどうか | ||
0x4: Indirect Descriptorかどうか) | ||
u16 | next | 次のDescriptor番号 |
ある種のvirtioデバイスは多数のdescriptorを消費するリクエストを大量に並列に発行することにより、性能を向上させることができます。
これを可能にするのがIndirect Descriptorです。Descriptorのflagsに0x4が指定された場合、addrはIndirect Descriptor Tableのアドレスを、lenはIndirect Descriptor Tableの長さ(バイト数)を示すようになります。
Indirect Descriptor TableはDescriptor Tableと同様、Descriptorの配列になっています。Indirect Descriptor Tableに含まれるDescriptorの数はlen/16個になります(3)。
それぞれのデータはIndirect Descriptor Table上のDescriptorへリンクされます。
Available Ringはゲストからホストへ渡したいDescriptorを指定するのに使用します(表2)。ゲストはリング上の空きエントリへDescriptor番号を書き込んでidxをインクリメントします。idxは単純にインクリメントし続ける使い方が想定されているため、リング長を超えるidx値が指定された時はidxをリング長で割った余りをインデックス値として使用します。
ホストは最後に処理したリング上のエントリの番号を記憶しておき(後述)、idxと比較して新しいエントリが指しているDescriptorを処理します。
type | member | description |
---|---|---|
u16 | flags | フラグ(0x1: 割り込みの一時的な抑制) |
u16 | idx | リング上で一番新しいエントリの番号 |
u16[QUEUE_NUM] | ring | Descriptor番号を書き込むリングの本体 |
u16 | used_event | ここで指定した番号のDescriptorが処理されるまで割り込みを抑制 |
Used Ringはホストからゲストへ渡したいDescriptorを指定するのに使用されます(表3)。
構造と使用方法は基本的にAvailable Ringと同じですが、リング上のエントリの構造がAvailable Ringと異なり、連続するDescriptorを先頭番号(id)と長さ(len)で範囲指定するようになっています(表4)。
type | member | description |
---|---|---|
u16 | flags | フラグ(0x1: ゲストからの通知の一時的な抑制) |
u16 | idx | リング上で一番新しいエントリの番号 |
UsedRingEntry[QUEUE_NUM] | ring | Descriptor番号を書き込むリングの本体 |
u16 | avail_event | ここで指定された番号のDescriptorが処理されるまで割り込みを抑制 |
type | member | description |
---|---|---|
u32 | id | 先頭のDescriptor番号 |
u32 | len | Descriptorチェーンの長さ |
Virtqueueを用いてデータ転送を行うために、Virtqueueに含まれない次の変数が必要です。
ゲストからホストへデータを転送するために、Descriptor Table, Available Ring, Used Ringをどのように使うかを次に示します(図2)。
この方向のデータ転送では、Available Ringは転送データを含むDescriptorの通知に使われ、Used Ringは処理済みDescriptorの回収に使われます。
図2の番号にそって解説します。
図2の番号にそって解説します。
図2の番号にそって解説します。
ホストからゲストへデータを転送するために、Descriptor Table, Available Ring, Used Ringをどのように使うかを次に示します(図3)。
この方向のデータ転送では、Available Ringは空きDescriptorの受け渡しに使われ、Used Ringは転送データを含むDescriptorの通知に使われます。
図3の番号にそって解説します。
図3の番号にそって解説します。
図3の番号にそって解説します。
virtio-netは受信キュー、送信キュー、コントロールキューの3つのVirtqueueからなります。 送信キューとコントロールキューはゲスト->ホスト方向のデータ転送方法で解説した手順でデータを転送します。受信キューはホスト->ゲスト方向のデータ転送方法で解説した手順でデータを転送します。受信キュー, 送信キューでは、パケットごとに1つのDescriptorを使用します。
Descriptorのaddrには直接パケットのアドレスを指定しますが、ホストドライバからゲストドライバへいくつかの情報を通知するため、パケットの手前に専用の構造体を追加しています(表5、図4)。
type | member | description |
---|---|---|
u8 | flags | フラグ(Checksum offload) |
u8 | gso_type | GSOによるパケットタイプ情報 |
u16 | hdr_len | Ethernet + IP + TCP/UDPヘッダの長さ |
u16 | gso_size | データ長 |
u16 | csum_start | チェックサムフィールドの位置 |
u16 | csum_offset | チェックサムの計算開始位置 |
コントロールキューでは、コマンド用構造体(表6、図5)にコマンド名を設定してゲストからホストへメッセージ送出します。コマンドに付属データが必要な場合は、コマンド用構造体の直後に続いてデータを配置します。コマンドはクラス(大項目)とコマンド(小項目)で整理されており、次のような種類があります。
type | member | description |
---|---|---|
u8 | class | クラス(大項目) |
u8 | cmd | コマンド(小項目) |
VIRTIO_NET_CTRL_RXクラスは次のようなコマンドを持ち、NICのプロミスキャスモード、ブロードキャスト受信、マルチキャスト受信などの有効/無効化を行います。
VIRTIO_NET_CTRL_MACクラスは次のようなコマンドを持ち、MACフィルタテーブルの設定に使用します。
VIRTIO_NET_CTRL_VLANクラスは次のようなコマンドを持ち、VLANの設定に使用します。
VIRTIO_NET_CTRL_ANNOUNCEクラスは次のようなコマンドを持ち、リンクステータス通知に対してackを返すのに使用します。
VIRTIO_NET_CTRL_MQクラスクラスは次のようなコマンドを持ち、マルチキューのコンフィギュレーションに使用します。
Virtqueueと、これを用いたNIC(virtio-net)の実現方法について解説しました。次号では、これまでの総集編で、仮想化システムの全体像を振り返ります。
Copyright (c) 2014 Takuya ASADA. 全ての原稿データ は クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。