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つが思いつくと思います.
- 0から割り当てる
- 仮想アドレスをそのまま使う
- 物理アドレスを使う
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の対応付けをおこないます.
MSIやMSI-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のドライバを書く際の参考ソースをあげておきます.
- Intel 82574 GbE Controller Family Datasheet, https://www.intel.ca/content/dam/doc/datasheet/82574l-gbe-controller-datasheet.pdf
- バイブル
- MINIX e1000, https://github.com/Stichting-MINIX-Research-Foundation/minix/tree/master/minix/drivers/net/e1000
- 基本部分が簡潔にまとまっています.おそらく一番分かりやすいと思います.
- FreeBSD e1000, https://github.com/freebsd/freebsd/tree/master/sys/dev/e1000
- Intelによる実装.実際にe1000のちゃんとしたドライバが欲しいならこれを移植した方がいいと思いますが,巨大なので最初に読むには向いてません.
- Redox e1000, https://github.com/redox-os/drivers/blob/master/e1000d/
- 必要最小限の実装 (rust)
- snabb driver, https://github.com/snabbco/snabb/blob/master/src/apps/intel_mp/intel_mp.lua
- old version, https://github.com/anttikantee/snabbswitch/blob/master/src/apps/intel/intel.lua
- こちらも簡潔にまとまっています (lua)