bobuhiro11/gokvm - GitHub

はじめに

gokvm開発 1 2 3 の続き。

gokvm 上のVMからPCIデバイスを取り扱えるよう開発を進めてきた。 道のりは長いだろうが、最終的には virtio-net を経由して、VMと外部の間でIP疎通を取りたい。 現時点では virtio-net デバイスをゲストカーネルのネットワークインターフェイスとして認識させることができたので、 ひとまずそこまでのログを残しておく。 やったことを大きく分けると、(1) ゲストのLinuxカーネルに対してvirtio-netデバイスをPCIデバイスとして認識させ、 (2) virtio-netデバイス初期化を完了させることでネットワークインターフェイスとして登録させることの2点。 virt queue上の操作やパケットのやり取りについては、この記事には含まれない。 例によって、コミット単位で実装の経過を残しておく。

fc02176d lspciコマンドの追加

busyboxにはlspciコマンドが同梱されているが、pci.ids ファイル 4 が存在しない。 pci.ids はベンダIDやデバイスIDなどの数値と、それに対応する文字列が組になっているようなファイルである。 このファイルがあれば、人間に読みやすいフォーマットで出力できる。 後々のデバッグをスムーズに進めたいので、対応させておいた。

e126392e PCI Config空間に対するIOエミュレーション

カーネルがPCIデバイスを認識するための重要なフェーズ。 PCI Config 空間を読む方法はいくつかあるようだが、ここではタイプ1 5 と呼ばれる方法でアクセスした。 ここで使われるIOポートのアドレスは以下の通り。

  • 0xcf8:アドレスレジスタに対応する。バス番号、デバイス番号、Function番号、PCI Config 空間内のオフセットに対応する。
  • 0xcfc ~ 0xcff:データに対応する。

アドレスレジスタは32bit幅で以下のように解釈される。

位置 内容
Bit 31 Enable Bit
Bit 30-24 Reserved
Bit 23-16 Bus Number
Bit 15-11 Device Number
Bit 10-8 Function Number
Bit 7-0 Register Offset

ざっくり PCI Config 空間のあるオフセットにあるデータを読みたいときは次のような手続きになる。

  1. 0xcf8に紐づくアドレスレジスタに、アクセスしたいPCIデバイスのバス番号、デバイス番号、Function番号、PCI Config 空間内のオフセットを表に記載の形式で書き込む。
  2. 0xcfc ~ 0xcff に対してIOを要求し、アドレスレジスタが指すデータを読み書きする。

ゲストカーネルは pci_check_type1 関数 5 で正常性をチェックするが、この関数がなぜか通らない。 この時点では原因がよく分からなかったので、次コミットで対応することにした。

5952d2dc pci_sanity_check を通す

調査を進めるとどうやらPCI Config 空間内のオフセットの取り扱いを間違っていたことに気づいた。 アドレスレジスタの Bits 7-0 にある Register Offset は4バイトのアライメントでのみ指定することができ、 それより細かい粒度はデータにアクセスする際のポート番号(0xcfc ~ 0xcff)の0xcfcからのオフセットになるようだ。 つまり、以下の式によって正しいオフセットを得られる。

(アドレスレジスタの下位8bit) & 0xfc + (データ読み書きのためのIOポート番号) - 0xCFC

修正すると、無事PCIデバイスの検出処理へと進ませることができた。

[    0.673777][    T1] PCI: Probing PCI hardware
[    0.674359][    T1] PCI: root bus 00: using default resources
[    0.675125][    T1] PCI: Probing PCI hardware (bus 00)
[    0.675895][    T1] PCI host bridge to bus 0000:00
[    0.676143][    T1] pci_bus 0000:00: root bus resource [io  0x0000-0xffff]
[    0.677058][    T1] pci_bus 0000:00: root bus resource [mem 0x00000000-0x7fffffffff]
[    0.678079][    T1] pci_bus 0000:00: No busn resource found for root bus, will use [bus 00-ff]
[    0.679222][    T1] pci_bus 0000:00: scanning bus

6a63e515 PCIデバイスを登録する

PCIデバイスは複数存在し得るので、PCI Config 空間を構造体として取り扱えるようにした。 さらに、virtio-net デバイスとホストブリッジの2つのデバイスを登録してみると、正しく検出された。良い感じ。

/ # lspci
00:00.0 Non-VGA unclassified device: Intel Corporation Device 0000
00:01.0 Non-VGA unclassified device: Red Hat, Inc. Virtio network device

e4c89328 ethtool コマンドの追加

これ以降、virtio-netデバイスをネットワークインターフェイスとして登録させていくのだが、ethtoolコマンドがないと困るので追加した。 例によって static にビルドして、initrd に組み込んだ。

b66267c1 virtio-netを別パッケージに分離

リファクタリング。 PCIデバイス全体を扱う pci パッケージと、virtio-net を扱う virtio パッケージの2つに分離させた。 PCIデバイスはインターフェイス(Go言語の仕様の一つ)として抽象化できるようにした。

a039f674 dyndbgにvirtio_net.cを追加

ゲストカーネルのカーネルパラメータ dyndbg に drivers/net/virtio_net.c 6 を追加した。 これを設定しておくとデバッグログを得られるので実装が捗る。

779cbcd8 PCI Config 空間に割り込みとBARを設定

PCI Config 空間には以下のようにInterupt Lineを設定できる。 ここで、virtio-net デバイスに対して9番を割り当てた(空いていたので使ったが良かったのだろうか?)。 BARにはそのデバイス専用のIO空間と物理メモリ空間のアドレスを登録することができる。 下位1ビットが1ならIO空間、0なら物理メモリ空間のアドレスに対応する。 virtio-netデバイスの場合(legacyモードの時?)は、BAR0 が virtio-net 専用のヘッダを指すような構造になっている。 そこで、BAR0に空いているIOアドレス帯 0x6200 ~ 0x6300 を割り当てた。

出典:Header Type 0x0 - wiki.osdev.org

77713d06 BAR0のサイズ判定

PCI Config空間を眺めてみるとBAR0のベースアドレスは存在するが、そのサイズはどこにも書かれていない。 では、ゲストはPCIデバイスのBAR0のサイズをどうやって判定しているのか。 実はBAR0に対して特殊な値 0xffffffff を書き込むと、PCIデバイスはサイズを返すような応答をする 7

  1. まずBAR0(オフセット0x10)に 0xffffffff を書き込む
  2. その後BAR0(オフセット0x10)のデータを読む。例えば 0xffffff00 が返ってきたとする
  3. BAR0のサイズは 0xffffff00 の0と1を反転して、その後1を加算することで求められる。今回の場合BAR0のサイズは 0xff + 1 = 0x100 バイトとなる

7b4d9f10 PCIデバイス向けのIOハンドラを追加

BAR0の指す先はIO空間になる(BAR0の最下位ビットで判定可能)ので、KVMからホストプロセスにEXITIOINやEXITIOOUTで処理が戻ってきたときに、 適切なIOハンドラを実行できるように登録しておく。

8cb14d5a virtio-netデバイスをネットワークインターフェイスとして登録

virtioデバイスではBAR0の先がVirtio Header 8 に対応する(要出典)ので、それを構造体として実装した。 virtio-netデバイスをネットワークインターフェイスとして認識されるには、Virtio Headerの QUEUE_PFN と QUEUE_SEL を適切に取り扱う必要がある。 ざっくり言うと、ゲストカーネルは初期化のフェーズで、 QUEUE_SELを使ってキュー番号を選択し、QUEUE_PFNにそのキューの物理ページ番号(PFN)を書き込む。 ここで、キューの物理ゲストアドレスはQUEUE_PFN * ページサイズ(4096バイト)で求められる。 gokvm側ではこの挙動をエミュレートすれば良い。 本当はFEATURESやSTATUSも気にする必要があるだろうが、今の段階では不要だったので一旦スキップする。

出典:BitVisorのvirtio-netドライバの解説

終わりに

やっと virtio-net デバイスをゲストから認識させることができた。 ゲスト・ホスト間の通知方法や virt queue の位置がわかったので、 粛々と実装を進めていきたい。

/ # ethtool -i eth0
driver: virtio_net
version: 1.0.0
firmware-version:
expansion-rom-version:
bus-info: 0000:00:01.0
supports-statistics: yes
supports-test: no
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: no

/ # ip link set eth0 up
[   77.178801][    C1] xmit_skb:1648: eth0: xmit 00000000bddcbde5 33:33:00:00:00:02
Queue Notify was written!

  1. KVMを使ったVMMを自作してLinuxを起動するまでの記録 ↩︎

  2. KVMを使ったVMMを自作してLinuxを起動するまでの記録2 ↩︎

  3. KVMを使った自作VMMのSMP対応 ↩︎

  4. pci.ids ↩︎

  5. pci_check_type1() in Linux ↩︎

  6. virtio_net.c in Linux ↩︎

  7. address and size of the BAR - wiki.osdev.org ↩︎

  8. ハイパーバイザの作り方~ちゃんと理解する仮想化技術~ 第11回 virtioによる準仮想化デバイス その1「virtioの概要とVirtio PCI」 ↩︎