authors:
- 'Takuya ASADA [email protected]' title: | ハイパーバイザの作り方~ちゃんと理解する仮想化技術~ 第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デバイスとして振る舞い、次のような機能を提供します。
- デバイス初期化時のホスト<->ゲスト間ネゴシエーションや設定情報通知に使うコンフィギュレーションレジスタ
これを利用してキュー長やキュー数、キューのアドレスなどを通知する、
- 割り込み(ホスト->ゲスト)、I/Oポートアクセス(ゲスト->ホスト)によるホスト<->ゲスト間イベント通知機構
- 標準的なPCIデバイスのDMA機構を用いたデータ転送機能
があります。
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番号
Table: 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が処理されるまで割り込みを抑制
Table: Available Ringの構造
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が処理されるまで割り込みを抑制
Table: Used Ringの構造
type member description
u32 id 先頭のDescriptor番号 u32 len Descriptorチェーンの長さ
Table: Used Ringエントリの構造
Virtqueueを用いてデータ転送を行うために、Virtqueueに含まれない次の変数が必要です。
- ゲストドライバ
- free_head......空きDescriptorを管理するため、空きDescriptorの先頭番号を保持
- last_used_idx......最後に処理したUsed Ring上のエントリの番号
- ホストドライバ
- last_avail_idx......最後に処理したAvailable Ring上のエントリの番号
ゲストからホストへデータを転送するために、Descriptor Table, Available Ring, Used Ringをどのように使うかを次に示します(図2)。
この方向のデータ転送では、Available Ringは転送データを含むDescriptorの通知に使われ、Used Ringは処理済みDescriptorの回収に使われます。
図2の番号にそって解説します。
- ドライバの初期化時にあらかじめすべてのDescriptorのnextの値を隣り合ったDescriptorのエントリ番号に設定し空きDescriptorのチェーンを作成、チェーンの先頭Descriptorの番号をfree_headに代入しておく
- free_headの値から空きDescriptor番号を取得
- Descriptorのaddrにデータのアドレス、lenにデータ長を代入
- Descriptorのnextが指す次の空きDescriptorの番号をfree_headへ代入
- Available Ringのidxが指す空きエントリにDescriptorの番号を代入
- Available Ringのidxをインクリメント(新しい空きエントリ)
- Virtio HeaderのQUEUE_SELにキュー番号を書き込み
- 未処理データがあることをホストへ通知するためVirtio HeaderのQUEUE_NOTIFYへ書き込み4
図2の番号にそって解説します。
- ゲストからの通知を受けてlast_avail_idxとAvailable Ringのidxを比較、新しいエントリが指しているDescriptorを順に処理、last_avail_idxをインクリメント
- Used Flagsのidxが指す次の空きエントリに処理済みDescriptorの番号を代入
- Used Flagsのidxをインクリメント
- 処理が終わったことを通知するためゲストへ割り込み
図2の番号にそって解説します。
- ホストからの割り込みを受けてlast_used_idxとUsed Ringのidxを比較、新しいエントリが指している処理済みDescriptorを順に回収、last_used_idxをインクリメント
- 回収対象のDescriptorを空きDescriptorのチェーンへ戻し、free_headを更新
ホストからゲストへデータを転送するために、Descriptor Table, Available Ring, Used Ringをどのように使うかを次に示します(図3)。
この方向のデータ転送では、Available Ringは空きDescriptorの受け渡しに使われ、Used Ringは転送データを含むDescriptorの通知に使われます。
図3の番号にそって解説します。
- ドライバの初期化時にあらかじめすべてのDescriptorのnextの値を隣り合ったDescriptorのエントリ番号に設定し空きDescriptorのチェーンを作成、 チェーンの先頭Descriptorの番号をfree_headに代入しておく
- Available Ringのidxが指す次の空きエントリに空きDescriptorチェーンの先頭番号を代入
- Available Ringのidxをインクリメント
- Virtio HeaderのQUEUE_SELにキュー番号を書き込み
- 未処理データがあることをホストへ通知するためVirtio HeaderのQUEUE_NOTIFYへ書き込み
図3の番号にそって解説します。
- データ送信要求を受けてAvailable Ringを参照、必要な数のDescriptorを取り出す
- DescriptorをAvailable Ring上の、Descriptorチェーンから切り離す
- Descriptorのaddrにデータのアドレス、lenにデータ長を代入
- Used Ringのidxが指す次の空きエントリにDescriptorの番号を代入
- Used Ringのidxをインクリメント
- 未処理データがあることを通知するためゲストへ割り込み
図3の番号にそって解説します。
- ホストからの割り込みを受けてlast_used_idxとUsed Ringのidxを比較、新しいエントリが指している処理済みDescriptorを順に処理、last_used_idxをインクリメント
- 処理済みDescriptorを空きDescriptorのチェーンへ戻し、Available Ringを更新
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 チェックサムの計算開始位置
Table: struct virtio_net_hdr
コントロールキューでは、コマンド用構造体(表6、図5)にコマンド名を設定してゲストからホストへメッセージ送出します。コマンドに付属データが必要な場合は、コマンド用構造体の直後に続いてデータを配置します。コマンドはクラス(大項目)とコマンド(小項目)で整理されており、次のような種類があります。
type member description
u8 class クラス(大項目) u8 cmd コマンド(小項目)
Table: struct virtio_net_ctrl_hdr
VIRTIO_NET_CTRL_RXクラスは次のようなコマンドを持ち、NICのプロミスキャスモード、ブロードキャスト受信、マルチキャスト受信などの有効/無効化を行います。
- VIRTIO_NET_CTRL_RX_PROMISC
- VIRTIO_NET_CTRL_RX_ALLMULTI
- VIRTIO_NET_CTRL_RX_ALLUNI
- VIRTIO_NET_CTRL_RX_NOMULTI
- VIRTIO_NET_CTRL_RX_NOUNI
- VIRTIO_NET_CTRL_RX_NOBCAST
VIRTIO_NET_CTRL_MACクラスは次のようなコマンドを持ち、MACフィルタテーブルの設定に使用します。
- VIRTIO_NET_CTRL_MAC_TABLE_SET
- VIRTIO_NET_CTRL_MAC_ADDR_SET
VIRTIO_NET_CTRL_VLANクラスは次のようなコマンドを持ち、VLANの設定に使用します。
- VIRTIO_NET_CTRL_VLAN_ADD
- VIRTIO_NET_CTRL_VLAN_DEL
VIRTIO_NET_CTRL_ANNOUNCEクラスは次のようなコマンドを持ち、リンクステータス通知に対してackを返すのに使用します。
- VIRTIO_NET_CTRL_ANNOUNCE
- VIRTIO_NET_CTRL_ANNOUNCE_ACK
VIRTIO_NET_CTRL_MQクラスクラスは次のようなコマンドを持ち、マルチキューのコンフィギュレーションに使用します。
- VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET
- VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MIN
- VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MAX
Virtqueueと、これを用いたNIC(virtio-net)の実現方法について解説しました。次号では、これまでの総集編で、仮想化システムの全体像を振り返ります。
Copyright (c) 2014 Takuya ASADA. 全ての原稿データ は クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。