はじめに

前回は、ゲストOSのI/Oパフォーマンスを大きく改善する「virtio」準仮想化ドライバの概要と、virtioのコンポーネントの1つである「Virtio PCI」について解説しました。今回はVirtqueueとこれを用いたNIC(virtio-net)の実現方法について見ていきます。

virtioのおさらい

virtioは、大きく分けてVirtio PCIとVirtqueueの2つのコンポーネントからなります。Virtio PCIはゲストマシンに対してPCIデバイスとして振る舞い、次のような機能を提供します。

これを利用してキュー長やキュー数、キューのアドレスなどを通知する、

があります。

Virtqueueはデータ転送に使われるゲストメモリ空間上のキュー構造です。デバイスごとに1つまたは複数のキューを持つことができます。たとえば、virtio-netは送信用キュー, 受信用キュー, コントロール用キューの3つを必要とします。ゲストOSは、PCIデバイスとしてvirtioデバイスを検出して初期化し、Virtqueueをデータの入出力に、割り込みとI/Oポートアクセスをイベント通知に用いてホストに対してI/Oを依頼します。本稿では、Virtqueueについてより詳しく見ていきましょう。

Virtqueue

Virtqueueは送受信するデータをキューイング先のDescriptorが並ぶDescriptor Table、ゲストからホストへ受け渡すdescriptorを指定するAvailable Ring、ホストからゲストへ受け渡すdescriptorを指定するUsed Ringの3つからなります(図1)。

Virtqueueの構造

Virtqueueの構造

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 TableはDescriptorがQUEUE_NUM個2並んでいる配列です。Descriptorはデータ転送を行う都度動的にアロケートされるのではなく、Descriptor Table内の空きエントリを探して使用します。空きエントリを管理する構造はVirtqueue上にないため、ゲストドライバは空きDescriptorを記憶しておく必要があります(後述)。

Descriptorは転送するデータ1つに対して1つ使われ、データのアドレス、データ長などが含まれます(表1)。

データのアドレスはゲスト上の物理アドレスが用いられるため、仮想アドレス上で連続する領域でも物理ページがばらばらな場合、物理ページごとにDescriptorが1つ必要です。 このように複数のDescriptorを連続して転送したい場合には、nextで次のDescriptorの番号を指定してflagsに0x1をビットセットします。

Descriptorの構造
type member description
u64 addr データのアドレス(ゲスト物理アドレス)
u32 len データ長
u16 flags フラグ
(0x1: 次のDescriptorがあるかどうか
0x2: ホストから見てWrite OnlyのDescriptorかどうか
0x4: Indirect Descriptorかどうか)
u16 next 次のDescriptor番号

Indirect 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

Available Ringはゲストからホストへ渡したいDescriptorを指定するのに使用します(表2)。ゲストはリング上の空きエントリへDescriptor番号を書き込んでidxをインクリメントします。idxは単純にインクリメントし続ける使い方が想定されているため、リング長を超えるidx値が指定された時はidxをリング長で割った余りをインデックス値として使用します。

ホストは最後に処理したリング上のエントリの番号を記憶しておき(後述)、idxと比較して新しいエントリが指しているDescriptorを処理します。

Available Ringの構造
type member description
u16 flags フラグ(0x1: 割り込みの一時的な抑制)
u16 idx リング上で一番新しいエントリの番号
u16[QUEUE_NUM] ring Descriptor番号を書き込むリングの本体
u16 used_event ここで指定した番号のDescriptorが処理されるまで割り込みを抑制

Used Ring

Used Ringはホストからゲストへ渡したいDescriptorを指定するのに使用されます(表3)。

構造と使用方法は基本的にAvailable Ringと同じですが、リング上のエントリの構造がAvailable Ringと異なり、連続するDescriptorを先頭番号(id)と長さ(len)で範囲指定するようになっています(表4)。

Used Ringの構造
type member description
u16 flags フラグ(0x1: ゲストからの通知の一時的な抑制)
u16 idx リング上で一番新しいエントリの番号
UsedRingEntry[QUEUE_NUM] ring Descriptor番号を書き込むリングの本体
u16 avail_event ここで指定された番号のDescriptorが処理されるまで割り込みを抑制
Used Ringエントリの構造
type member description
u32 id 先頭のDescriptor番号
u32 len Descriptorチェーンの長さ

Virtqueueに含まれない変数

Virtqueueを用いてデータ転送を行うために、Virtqueueに含まれない次の変数が必要です。

ゲスト->ホスト方向のデータ転送方法

ゲストからホストへデータを転送するために、Descriptor Table, Available Ring, Used Ringをどのように使うかを次に示します(図2)。

この方向のデータ転送では、Available Ringは転送データを含むDescriptorの通知に使われ、Used Ringは処理済みDescriptorの回収に使われます。

ゲスト->ホスト方向データ転送のイメージ

ゲスト->ホスト方向データ転送のイメージ

ゲストドライバ

図2の番号にそって解説します。

  1. ドライバの初期化時にあらかじめすべてのDescriptorのnextの値を隣り合ったDescriptorのエントリ番号に設定し空きDescriptorのチェーンを作成、チェーンの先頭Descriptorの番号をfree_headに代入しておく
  2. free_headの値から空きDescriptor番号を取得
  3. Descriptorのaddrにデータのアドレス、lenにデータ長を代入
  4. Descriptorのnextが指す次の空きDescriptorの番号をfree_headへ代入
  5. Available Ringのidxが指す空きエントリにDescriptorの番号を代入
  6. Available Ringのidxをインクリメント(新しい空きエントリ)
  7. Virtio HeaderのQUEUE_SELにキュー番号を書き込み
  8. 未処理データがあることをホストへ通知するためVirtio HeaderのQUEUE_NOTIFYへ書き込み4

ホストドライバ

図2の番号にそって解説します。

  1. ゲストからの通知を受けてlast_avail_idxとAvailable Ringのidxを比較、新しいエントリが指しているDescriptorを順に処理、last_avail_idxをインクリメント
  2. Used Flagsのidxが指す次の空きエントリに処理済みDescriptorの番号を代入
  3. Used Flagsのidxをインクリメント
  4. 処理が終わったことを通知するためゲストへ割り込み

ゲストドライバ

図2の番号にそって解説します。

  1. ホストからの割り込みを受けてlast_used_idxとUsed Ringのidxを比較、新しいエントリが指している処理済みDescriptorを順に回収、last_used_idxをインクリメント
  2. 回収対象のDescriptorを空きDescriptorのチェーンへ戻し、free_headを更新

ホスト->ゲスト方向のデータ転送方法

ホストからゲストへデータを転送するために、Descriptor Table, Available Ring, Used Ringをどのように使うかを次に示します(図3)。

この方向のデータ転送では、Available Ringは空きDescriptorの受け渡しに使われ、Used Ringは転送データを含むDescriptorの通知に使われます。

ホスト->ゲスト方向データ転送のイメージ

ホスト->ゲスト方向データ転送のイメージ

ゲストドライバ

図3の番号にそって解説します。

  1. ドライバの初期化時にあらかじめすべてのDescriptorのnextの値を隣り合ったDescriptorのエントリ番号に設定し空きDescriptorのチェーンを作成、 チェーンの先頭Descriptorの番号をfree_headに代入しておく
  2. Available Ringのidxが指す次の空きエントリに空きDescriptorチェーンの先頭番号を代入
  3. Available Ringのidxをインクリメント
  4. Virtio HeaderのQUEUE_SELにキュー番号を書き込み
  5. 未処理データがあることをホストへ通知するためVirtio HeaderのQUEUE_NOTIFYへ書き込み

ホストドライバ

図3の番号にそって解説します。

  1. データ送信要求を受けてAvailable Ringを参照、必要な数のDescriptorを取り出す
  2. DescriptorをAvailable Ring上の、Descriptorチェーンから切り離す
  3. Descriptorのaddrにデータのアドレス、lenにデータ長を代入
  4. Used Ringのidxが指す次の空きエントリにDescriptorの番号を代入
  5. Used Ringのidxをインクリメント
  6. 未処理データがあることを通知するためゲストへ割り込み

ゲストドライバ

図3の番号にそって解説します。

  1. ホストからの割り込みを受けてlast_used_idxとUsed Ringのidxを比較、新しいエントリが指している処理済みDescriptorを順に処理、last_used_idxをインクリメント
  2. 処理済みDescriptorを空きDescriptorのチェーンへ戻し、Available Ringを更新

virtio-netの実現方法

virtio-netは受信キュー、送信キュー、コントロールキューの3つのVirtqueueからなります。 送信キューとコントロールキューはゲスト->ホスト方向のデータ転送方法で解説した手順でデータを転送します。受信キューはホスト->ゲスト方向のデータ転送方法で解説した手順でデータを転送します。受信キュー, 送信キューでは、パケットごとに1つのDescriptorを使用します。

Descriptorのaddrには直接パケットのアドレスを指定しますが、ホストドライバからゲストドライバへいくつかの情報を通知するため、パケットの手前に専用の構造体を追加しています(表5、図4)。

struct virtio_net_hdr
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)にコマンド名を設定してゲストからホストへメッセージ送出します。コマンドに付属データが必要な場合は、コマンド用構造体の直後に続いてデータを配置します。コマンドはクラス(大項目)とコマンド(小項目)で整理されており、次のような種類があります。

struct virtio_net_ctrl_hdr
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 国際 ライセンスの下に提供されています。


  1. ページサイズ = 4KB

  2. Virtio HeaderのQUEUE_NUMで指定する。

  3. 1つのDescriptorの長さが16bytesであるため。

  4. QUEUE_NOTIFYへ書き込むことによりVMExitが発生し、ホスト側へ制御が移ることを意図している。