VFIOによるデバイス操作

VFIO (Virtual Function I/O)Linuxにおいてユーザスペースでデバイスを操作するためのフレームワークの一つです. ユザースペースドライバといえばuioもありますが,uioとVFIOの主要な違いの一つはVFIOはIOMMUを利用するという点です*1.uioはLinux 2.6.23から,VFIOは3.6から本体にマージされています.

VFIOはもともとはKVM/QEMUで安全にドライバを作成するのが主目的だったようですが,もちろんそれ以外でも使えます.KVM/QEMU以外でVFIOを利用している主要なプロジェクトにはDPDKがあります*2

あまりVFIOでデバイスを操作する例が見つからなかったので,動作確認のために簡単にIntel 82574Lのドライバを書いてみました(ドライバと呼んでいいレベルなのか分かりませんが..).

VFIOは情報が少なく最初は戸惑いましたが,慣れれば便利に使えると思います.

以下,主要点をまとめます.

事前準備

VFIOを利用する場合にはCONFIG_VFIOなどのカーネルオプションを有効にする必要があります. といっても主要なディストリビューションならおそらく有効になってると思います.

VFIOでPCIバイスを操作するためには,vfio-pciドライバをデバイスにbindする必要があります. これは例えば,以下のようにしてできます.

% lspci -nn | grep -i Ether
86:00.0 Ethernet controller [0200]: Intel Corporation 82574L Gigabit Network Connection [8086:10d3]
% sudo modprobe vfio-pci
% echo 0000:86:00.0 | sudo tee -a /sys/bus/pci/devices/0000:86:00.0/driver/unbind
% echo 8086 10d3 | sudo tee -a /sys/bus/pci/drivers/vfio-pci/new_id

バイスにvfio-pciを割り当てると,/dev/vfio/vfio および, /dev/vfio/<num> というファイルが作成されます. <num>はIOMMUのグループ番号です.IOMMUは基本デバイスごとに設定できますが,場合によってはIOMMUがデバイスを区別できない場合があり,そうしたものがまとめられてIOMMUグループになります*3.例えば,マルチファンクションのPCIバイスは一つのIOMMUグループに属します. ここで注意点として,VFIOでデバイスを操作するためには,そのデバイスが属するIOMMUグループに含まれる全てのデバイスに対してvfio-pciをbindする必要があります.

あるデバイスのIOMMUグループの確認は,

readlink /sys/bus/pci/devices/<ssss:bb:dd.f>/iommu_group

あるデバイスが属するIOMMUグループに含まれるデバイスは,

ls -l /sys/bus/pci/devices/<ssss:bb:dd.f>/iommu_group/devices

で確認できます.

/dev/vfio/<num>ファイルはデフォルトではrootのみしかopenできませんが,ファイル所有者をchownしてあげればsudoいらずでデバイスが操作できるようになります.

ということでvfio-pciの割り当ては少々面倒ですが,vfio-pci-bind.shを使うと指定したデバイス + 同一のIOMMUグループに属するデバイス全てにvfio-pciをbindし,さらに/dev/vfio/<num>chownしてくれるので便利です(unbindの機能はないようですが).

ちなみにIOMMUグループの番号はどうやら<ssss:bb:dd.f>の昇順につけられているようです.

バイス操作の基本

VFIOでのデバイス操作する場合, /dev/vfio/<num>をopenし,得られたfdに対してioctl経由で設定をおこないます. また,コンテナとIOMMU Typeの設定も必要です.

コンテナはIOMMUグループをまとめたもので,VFIOではこのコンテナに対して同一のIOMMUの設定が適用されます. また,IOMMU TypeはVT-dの場合はVFIO_TYPE1_IOMMUを指定します.Type1というなんとも安直なネーミングですね..

この辺りはopen_vfio()でやっています.

基本情報の取得

バイスのconfiguration spaceやBAR空間などの情報の取得は,ioctl(fd, VFIO_DEVICE_GET_REGION_INFO, idx)でできます. メモリ空間がread/write/mmap 可能かなどの情報が取得できます. このとき,idx0-5がBAR-5の情報,idx 6がconfiguration spaceの情報になります.

また,割り込みの情報はioctl(fd, VFIO_DEVICE_GET_IRQ_INFO, idx)で取得できます. ここでVRIO_IRQ_INFO_EVENTFDフラグがついているものはeventfdを使って割り込みを受け取ることができます(後述). idx 0がINTx, idx 1がMSI, idx 2がMSI-Xです.

この辺りはget_device_info()でやっています.

バイスのメモリ空間へのアクセス

Configuration spaceやBAR空間へは,/dev/vfio/<num>をopenしたデスクリプタに対して,適当なオフセットでread/writeすることでおこないます. オフセットはioctl(VFIO_DEVICE_GET_REGION_INFO)で取得できます.

領域によってはmmapできます.mmapは自動でキャッシュが無効化されるようにmmapされます.ちなみに,configuration spaceはmmapできません.これは,VFIOがconfiguration spaceのフィールドの一部を仮想化する場合があるからです.

dump_configuration_space()でconfiguration spaceのアクセスをおこなっています.

DMA有効化

VFIOとは直接関係ありませんが,DMAを使用するにはPCI configuration spaceのcommand registerを操作してbus masterをenableにする必要があります(enable_bus_master()). これをうっかり忘れるとちゃんとドライバを書いたはずなのにDMAが動かないという悲しい現象が発生します.

DMA用IOMMUの設定

init_rx_buf()init_tx_buf()でパケット送受信用のバッファを割り当てています. ここで,ringの各descriptorはそれぞれ自身のバッファを保持しますが,このアドレスはIOMMUに対する仮想アドレス(IOVA)になります. そして,IOVAとvirtual addressの対応付けをioctl(VFIO_IOMMU_MAP_DMA)でおこないます. VFIOによって仮想アドレスが物理アドレスへ変換され,iovaとphysical addressのマッピングがIOMMUに登録されます.また,登録した領域は自動でpinningされます.

ここで,IOVAをどうするかという問題がでてきます.IOVAは任意に割り当てられますが*4,ぱっと以下の3つが思いつくと思います.

  1. 0から割り当てる
  2. 仮想アドレスをそのまま使う
  3. 物理アドレスを使う

1.の方法はIOVAを0から順番に降るという単純な方法です.

2.は,仮想アドレスをIOVAとして使うので,何も変換がいらず楽です.が,IOMMUの種類によってはIOVAのアドレス幅がCPUの仮想アドレス幅よりも小さいものがあります. VT-dでは,IOMMUによってサポートするIOVAのアドレス幅が39bit (3-level paging)のものと,48bit (4-level paging)のものがあります. IOMMUがサポートするアドレス幅はVT-dのcapability registerから分かります.Linuxではdmesgからcapabilityが確認できます.

% dmesg | grep DMAR
[    0.000000] ACPI: DMAR 0x000000007E8D6550 0000A8 (v01 INTEL  KBL      00000001 INTL 00000001)

ここで, 0x000000007E8D6550がcapability regisetrの値で,この21:16がMGAW (Maximum Guest Address Width)です. この場合MGAW = 100110 = 38 なので,これはアドレス幅が39bitを意味します.従ってこの環境だと仮想アドレスはそのままではIOVAとして使用することはできません. i7-7700, Z270 という比較的最近の構成だと思うんですが..

3.の物理アドレスを使う方法は,ようするにIOMMU的にはIOVA = physical addressのマッピングを作成することになります. これの何が嬉しいかというと,IOMMUをサポートしていないマシンでIOMMUを使用しない場合とアドレスの処理を同じにすることができます.DPDKではこうしています. Linuxではユーザ空間から物理アドレスを求めるための関数は提供されていないので,/proc/self/pagemapをパースすることになります. この場合/proc/self/pagemapの読み込みはCAP_SYS_ADMINが必要なので,要するにsudoが必要になります.

ちなみに,今回は最大でも4096byteの領域しか割り当てていないので関係ないですが,IOMMUを使用しているのでDMA領域は連続である必要はありません. また,本来であればhuge pageを使うべきです.

一般ユーザがカーネル内でロックできるメモリ量は通常制限されています.これはulimit -mで確認できます. 容量を増やすには,一時的にはsudo prlimit --melcok=-1, 恒久的には /etc/security/limits.conf に設定を書きます.(無制限にするなら * - memlock -1

割り込み

VFIOでは割り込みはeventfdで処理します. ioctl(VFIO_DEVICE_SET_IRQS)によって,割り込みとeventfdの対応付けをおこないます. MSIMSI-Xは複数の割り込みベクタを持つことができますが,この場合各割り込みベクタごとにevnetfdが対応します.

この辺りの処理をおこなっているのがenable_intx(), enable_msi(), enable_msix()になります. 82574Lの場合,MSIは一つの割り込みベクタのみなので,INTxとソフトウェア的にはほとんど変わりません. MSI-Xでは5つベクタが持てるので,それぞれに対してeventfdを割り当てています.

eventfdの処理はepollでおこなっています.epollを使うことで,複数のeventfdの中からイベントがあったfdを検知することができます. INTx, MSIの場合にはfdが一つしかないので,単純にeventfdをblockn readして待つことも可能です.

この辺りの処理がhandle_intr()です.

デバッグ

IOMMUの処理にはいくつかtracepointが定義されており,以下のようにして確認できます.

cd /sys/kernel/debug/tracing/
echo 1 > events/iommu/enable
cat trace

例えば,IOMMUに設定したmappingの情報が確認できます.

また,eventにはIOMMU page fautのeventもありますが,どうやらVT-dの場合はこのiommu page fault eventが発火することはないようです. その代わりに,IOMMU page faultが発生した場合はdmesgから確認できます.

後処理

今回書いたソースでは手抜きのため後処理してませんが,本来であればちゃんとioctl(VFIO_IOMMU_UNMAP_DMA)をすべきでしょう. ただ,明示的にunmapしなくてもプログラム終了時に自動でunmapしてくれます.

e1000参考ソース

最後に,e1000のドライバを書く際の参考ソースをあげておきます.

*1:IOMMUを利用しない no IOMMUモードもあることはあります

*2:DPDKにはVFIOとuio,双方のユーザスペースドライバが含まれています

*3:このIOMMUグループはPCの構成に固有のもので変更できません

*4:実際にはDMAに使用不可能なメモリ領域がRMRRとしてACPI DMARの中で通知されますが,普通RMRRの領域とアドレスが被ることは99%ないと思います