十分に枯れていてあまりトラブルになることは無いけれど、今後も使い続けられる技術なので、 その挙動を知っておくことに意味はあると思う。

NICの完全仮想化・準仮想化

おさらい。完全仮想化は、ゲスト自身が仮想環境で動作していることを認識できないような仮想化のこと。 ホストは、ゲストが利用するデバイスの挙動をすべて模倣する必要がある。 デバイスの模倣のために、大量のトラップが発生し、CPUがホスト側の処理に使われてしまう。 また、デバイスのエミュレーションをホスト側ユーザプロセスで行う場合には、 スケジューリング待ちによる遅延も発生してしまう。

一方、準仮想化では、ゲスト自身が仮想環境で動作していることを認識した上で、 ホストと協力してパフォーマンスを向上の図る。 virtio は準仮想化専用のデバイスドライバによって、仮想的なデバイスを操作する。

アーキテクチャ

フロントエンドドライバとバックエンドドライバを、vringが繋ぐ構成になっている。

フロントエンドドライバ
ゲスト側で動作する。ゲストOSが発行したIOをvringを経由して、バックエンドドライバへ送る。
バックエンドドライバ
ホスト側で動作する。vring経由で受け取ったIOを、物理デバイスへ送る。

virtio.png

https://www.cs.cmu.edu/~412/lectures/Virtio_2015-10-14.pdf

Virtio 仕様

SAMLやebXMLなどを標準化している団体 OASIS が Virtual I/O Device (VIRTIO) Version 1.1 として仕様をまとめている。 コールバックや各種データ構造の仕様を決めている。

Virtqueue

実際のIOを担当する部分で、ゲスト物理メモリの一部をホスト側と共有することで、 双方向での読み書きを実現できる。 また、双方向に通知を送るための仕組みも持っており、ホスト側へはMMIO経由で、 ゲスト側へは割り込みを使って通知を送る。 disable_cdenable_cbといった関数で、割り込み抑制をもう一方へ伝えることができる。 例えば、ゲスト側ドライバで一定期間の間は割り込みが不要な場合には、 その旨を disable_cd でホスト側へ伝える。 通知を送る際には kick を呼ぶ。

struct virtqueue_ops {
  int (*add_buf)(struct virtqueue *vq,
         struct scatterlist sg[],
         unsigned int out_num,
         unsigned int in_num,
         void *data);
  void (*kick)(struct virtqueue *vq);
  void *(*get_buf)(struct virtqueue *vq, unsigned int *len);
  void (*disable_cb)(struct virtqueue *vq);
  bool (*enable_cb)(struct virtqueue *vq);
};

https://elixir.bootlin.com/linux/v2.6.31/source/include/linux/virtio.h#L61

Vring

Virtqueueの仕様をリングキュー構造を使って実装したものがVring。 3つのデータ領域から構成される。 それぞれのデータ領域の詳細な動きについてはまた今度まとめる。

vring_desc
「ゲスト物理アドレスと長さ」の配列
vring_avail
どの descriptor を利用可能なのか
vring_used
どの descriptor を利用済みなのか

vring.png

https://www.cs.cmu.edu/~412/lectures/Virtio_2015-10-14.pdf

コードリーディング

struct virtio_driver がフロントエンドドライバに対応する。 Virtioデバイスはstruct virtio_deviceに対応し、 struct virtio_config_opsstruct virtiqueue を辿れるようになっている。

// フロントエンドドライバに対応
struct virtio_driver {
  struct device_driver driver;
  const struct virtio_device_id *id_table;
  const unsigned int *feature_table;
  unsigned int feature_table_size;
  int (*probe)(struct virtio_device *dev);
  void (*remove)(struct virtio_device *dev);
  void (*config_changed)(struct virtio_device *dev);
};

// virtio デバイスに対応
struct virtio_device {
  int index;
  struct device dev;
  struct virtio_device_id id;
  struct virtio_config_ops *config;
  struct list_head vqs;
  unsigned long features[1];
  void *priv;
};

https://elixir.bootlin.com/linux/v2.6.31/source/include/linux/virtio.h#L111

PCI Configuration Space

VirtioデバイスはPCIで接続される。Device ID は 0x1Af4 、Vendor IDは 0x10000x1040 に対応する。 Subsystem ID を見ると、Virtio の種別が分かる。種別の例は以下の通り。

  1. virtio-net
  2. virtio-blk
  3. virtio-console

PCI I/O Space

先頭24バイトが VirtioHeader に対応し、その直後に種別ごとの設定(virtio-netであれば virtio_net_config)が続く。 VirtioHeaderにはホストおよびゲストの Feature bits や、Virtqueueのサイズ、デバイスのステータスが含まれる。 virtio_net_config にはNICキュー数の最大値、MTU、MACアドレス等が格納される。

lspci -s xx:yy.z -vvv | grep "I/O port"
cat /proc/ioports

# このコマンドの結果、先頭24バイトがVirtioHeader
# それ以降が virtio_net_config に対応する
hexdump -s $((16#XXXX)) -n 64 /dev/port

vhost-net

virtio-net ホストドライバの問題点として、ゲストがIOを発行するときに vCPU の処理が止まり、 vmexit でホストへ制御が遷移してしまうという問題があった。 例えば、ゲストがパケットを外部へ送信するときにこの問題が発生する。 そこで、vhost-$pidカーネルスレッドがその処理を肩代わりする仕組み vhost-net ができた。 このカーネルスレッドは、NICキューごとに1つずつ作られる。

vhost-net.png

https://www.redhat.com/ja/blog/deep-dive-virtio-networking-and-vhost-net

用語

ややこしいので整理しておく。

virtio
virtio の API仕様
vqueue(virt_queue、virtqueue)
トランスポート(実際にデータが流れるキュー)のAPI仕様
vring(virtio_ring)
vqueueをリングキューを使って実装したもの
VirtioHeader
PCI IO空間の先頭にあって、設定のためのフィールド
virtio-net
vringを使って仮想NICを提供する仕組み。ゲスト側あるいはホスト側ドライバの実装を指すこともある
vhost(vhost-net)
virtio-net のうちホスト側の実装をQEMUから切り離したもの
vhost-user
vhost-net をホスト側のユーザプロセスで置き換えたもの

作者

Linux ipchains やその後継 netfilter/iptablesの開発者である Rusty Russel 氏が作った。 x86ハイパーバイザ lguest の開発者でもある。

参考