十分に枯れていてあまりトラブルになることは無いけれど、今後も使い続けられる技術なので、 その挙動を知っておくことに意味はあると思う。
NICの完全仮想化・準仮想化
おさらい。完全仮想化は、ゲスト自身が仮想環境で動作していることを認識できないような仮想化のこと。 ホストは、ゲストが利用するデバイスの挙動をすべて模倣する必要がある。 デバイスの模倣のために、大量のトラップが発生し、CPUがホスト側の処理に使われてしまう。 また、デバイスのエミュレーションをホスト側ユーザプロセスで行う場合には、 スケジューリング待ちによる遅延も発生してしまう。
一方、準仮想化では、ゲスト自身が仮想環境で動作していることを認識した上で、 ホストと協力してパフォーマンスを向上の図る。 virtio は準仮想化専用のデバイスドライバによって、仮想的なデバイスを操作する。
アーキテクチャ
フロントエンドドライバとバックエンドドライバを、vringが繋ぐ構成になっている。
- フロントエンドドライバ
- ゲスト側で動作する。ゲストOSが発行したIOをvringを経由して、バックエンドドライバへ送る。
- バックエンドドライバ
- ホスト側で動作する。vring経由で受け取ったIOを、物理デバイスへ送る。
Virtio 仕様
SAMLやebXMLなどを標準化している団体 OASIS が Virtual I/O Device (VIRTIO) Version 1.1 として仕様をまとめている。 コールバックや各種データ構造の仕様を決めている。
Virtqueue
実際のIOを担当する部分で、ゲスト物理メモリの一部をホスト側と共有することで、
双方向での読み書きを実現できる。
また、双方向に通知を送るための仕組みも持っており、ホスト側へはMMIO経由で、
ゲスト側へは割り込みを使って通知を送る。
disable_cd
やenable_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 を利用済みなのか
コードリーディング
struct virtio_driver
がフロントエンドドライバに対応する。
Virtioデバイスはstruct virtio_device
に対応し、
struct virtio_config_ops
やstruct 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は 0x1000
~ 0x1040
に対応する。
Subsystem ID を見ると、Virtio の種別が分かる。種別の例は以下の通り。
- virtio-net
- virtio-blk
- 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つずつ作られる。
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 の開発者でもある。
参考
- virtio: Towards a De-Facto Standard For Virtual I/O Devices
- 仮想化環境におけるパケットフォワーディング
- Virtio-networking series
- ハイパーバイザの作り方
- Virtual I/O Device (VIRTIO) Version 1.1
- OSC2011 Tokyo/Fall 濃いバナ(virtio)
- Virtio: An I/O virtualization framework for Linux
- virtio guest side implementation: PCI, virtio device, virtio net and virtqueue
- The evolution of IO Virtualization and DPDK-OVS implementation in Linux