grafi.jp

基本的にLinuxデスクトップ環境で過ごしているが、WindowsでBlu-rayを再生したいこともある。 この要求を満たす環境の構成としては

  1. Windowsの下でLinuxを仮想マシンのゲストとして動かす
  2. デュアルブート
  3. 物理的に二台動かす

といった選択肢が考えられますが、1はLinux側で使えるリソースが減るのが気に入らず、2は面倒くさい、3はハードウェアと場所と電気が必要と、それぞれ大きなデメリットがあります。

このような難点を解消することはできないか。実はディスクリートGPUが挿さったマシンにおいては、PCIパススルーを行うことで、Linuxの下で仮想マシンのゲストとして動くWindowsから直接GPUにアクセスさせることができます。Linuxの方ではCPU内蔵のGPU(あるいは別のディスクリートGPU)を使います。SCSIパススルーを行うことで、WindowsからBlu-rayの読み込みなどもできます。こうすれば全ての難点はクリアされ、実際かなり快適に過ごすことが可能となりました。

始めは取っつきにくく、実際数日がかりの試行錯誤になりましたが、なんとなくわかってしまえばシンプルです。むしろこんなにすっきりしていて正常に動作する足回りが開発されていることがすごいなあと感じます。

必要なハードウェア

  • (Intelの場合)VT-xとVT-dをサポートしたチップセット・CPU
  • UEFI(GOP)をサポートするVGA

VT-dはちょっと前までローエンドや倍率ロックフリーのCPUだとサポートされてなかったので注意が必要です。他はすごく古くなければ問題なさそうです。

このあたりの機能がない場合、色々無理をしないとGPUのパススルーはできないです。技術的な詳細を追っていないので不正確かとは思いますがわたしの理解を書いておきます。

VT-dはIntelによるIOMMUの仕様です。IOMMUは、MMUがプロセスがアクセスする仮想メモリアドレスを物理メモリアドレスにマップするのと同様に、PCIデバイスがDMAする先を物理メモリアドレスにマップします。これによって、PCIデバイスが仮想マシンの下にある物理アドレスに正しくDMAできるようになると同時に、メモリ保護を実現できます。また、IOMMUはPCIデバイスが発生させる割り込みをVMに送信する機能も提供しています。

QEMUはVFIOというLinuxの機構を通じてPCIパススルーを行います。VFIOとはPCIデバイスに対応するioctlで操作できるファイルディスクリプタをユーザー空間に公開することで、ユーザー空間からPCIデバイスをフルに操作できるようにする機構です。ユーザー空間からDMAリクエストを行ったときに好き勝手なメモリアクセスをしないようIOMMUが使われます。QEMUはVFIOファイルディスクリプタをPCIデバイスとして仮想マシンに見せるわけです。

しかしGPUは普通のPCIデバイスみたいに単純ではありません。OSが起動する前に画面に何か描いてる地点でそれは明らかなのですが、特にBIOSで起動する場合はVGA BIOSというレガシーなABIを用いることとなり、システム全体にわたって固定されたアドレスを使ってデバイスとやり取りすることになります。なので複数のVGAを使う場合VGA BIOSをがんばって切り替える必要が生じてきて、さらにVGA BIOS領域にVFIO fdを介した変換される前のMMIOアドレスが見えてしまうのをなんとかしないといけません。このためパッチが必要になったりします。

一方でUEFIではそういったアドレス空間に関する問題はなく、OS起動前の画面表示はGOPというプロトコルで行っています。なのでより安全にパススルーができることになります。

参考

その他有ったほうがいいハードウェア

要するにこれは二台のマシンを動かすのと大体同じような構成です。 よって以下のものは有ったほうがいいです。

ディスプレイ関連

ホストとゲストが別々に画面出力を行うことになるので、それぞれをディスプレイに接続する必要があります。このために

  • 複数の入力端子があるディスプレイ
  • 複数枚のディスプレイ
  • ディスプレイ切替器

のいずれかが必要となります。

キーボード・ポインティングデバイス関連

ゲストは仮想スクリーンに描画するのではなく全く独自に画面出力をしているのですから、カーソルをゲストのスクリーンの上に持っていくとゲストを操作できるというような、空気を読んだ挙動はできません。なので、ホストとゲストの双方に、別々に入力機器をつなぐ必要があります。

具体的には

  • 複数のUSBコントローラの一部をゲストに渡した上で、USB切替器を使って入力機器を接続するUSBポートを切り替える
  • 入力機器一式を二つ用意して、片方をゲストで使う

のいずれかが考えられます。後者は力技ですが単純で、特定のUSBデバイスをゲストに渡してやるだけです。前者では、一部のUSBコントローラのPCIデバイスを丸ごとPCIパススルーしてやり、切替器を使うことでホスト・ゲストそれぞれが制御するUSBコントローラに対応するポートにUSBデバイスを繋ぎ替えます。

わたしはUSBオーディオインターフェースの切り替えも行いたい都合でhttps://www.amazon.co.jp/gp/product/B000RHH590[サンワサプライの2x2スイッチ]を使いました。また自分が使っているキーボード(HHKB)にはUSBハブがついているので、そこにトラックボールをぶら下げることにしました。外部電源を取っているおかげで無理がないからか、以前使っていたスイッチよりもキーボードが安定して認識されていて好調です。

           +----------+
ポート1----|          |----USBオーディオIF
           | スイッチ |
ポート2----|          |----キーボード(HHKB)----トラックボール
           +----------+

複数のUSBコントローラを用意するには、PCIeやPCIでUSBポートを増設すればいいです。一部のポートだけがUSB3.0であるようなチップセットではUSB3.0のコントローラが内部的に分かれています。なのでわたしのMBでは、USBポートの数や片方ではUSB3.0が使えなくなることを気にしなければ、増設の必要はありませんでした。(PCIeスロットが埋まっていることに気づかずPCIe機器を買ったり、その後PCI機器を買ってhttp://vfio.blogspot.jp/2015/05/vfio-gpu-how-to-series-part-1-hardware.html[PCI-PCIeブリッジによる割り込みの問題で使えなかったり]と、無駄足を踏みましたが。)

理論的にはハードウェアに依らなくとも、特定のキーボードショートカットやコマンドを実行したときに適当なUSBデバイスをゲストにアタッチ・デタッチさせるよう、ホスト・ゲストの環境をそれぞれ設定すれば、USBデバイスの切り替えは可能です。ゲスト側からのQEMUの操作にはhttp://wiki.qemu.org/Features/QAPI/GuestAgent[GuestAgent]が使えます。しかし確実に動くよう設定するのは中々大変そうです。

PCIデバイス・IOMMUグループの確認

PCI passthrough via OVMF - ArchWikiを参考にやりました。

わたしのマシンは

MB
ASUS P8H77-M
CPU
Core i5-3550 (IvyBridge)
GPU
GTX750Ti

です。ArchWikiにあるように

$ for iommu_group in $(find /sys/kernel/iommu_groups/ -maxdepth 1 -mindepth 1 -type d); do echo "IOMMU group $(basename "$iommu_group")"; for device in $(ls -1 "$iommu_group"/devices/); do echo -n $'\t'; lspci -nns "$device"; done; done

を実行すると、

IOMMU group 0
        00:00.0 Host bridge [0600]: Intel Corporation Xeon E3-1200 v2/3rd Gen Core processor DRAM Controller [8086:0150] (rev 09)
IOMMU group 1
        00:01.0 PCI bridge [0604]: Intel Corporation Xeon E3-1200 v2/3rd Gen Core processor PCI Express Root Port [8086:0151] (rev 09)
        01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GM107 [GeForce GTX 750 Ti] [10de:1380] (rev a2)
        01:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:0fbc] (rev a1)
IOMMU group 2
        00:02.0 VGA compatible controller [0300]: Intel Corporation Xeon E3-1200 v2/3rd Gen Core processor Graphics Controller [8086:0152] (rev 09)
IOMMU group 3
        00:14.0 USB controller [0c03]: Intel Corporation 7 Series/C210 Series Chipset Family USB xHCI Host Controller [8086:1e31] (rev 04)
IOMMU group 4
        00:16.0 Communication controller [0780]: Intel Corporation 7 Series/C210 Series Chipset Family MEI Controller #1 [8086:1e3a] (rev 04)
IOMMU group 5
        00:1a.0 USB controller [0c03]: Intel Corporation 7 Series/C210 Series Chipset Family USB Enhanced Host Controller #2 [8086:1e2d] (rev 04)
IOMMU group 6
        00:1b.0 Audio device [0403]: Intel Corporation 7 Series/C210 Series Chipset Family High Definition Audio Controller [8086:1e20] (rev 04)
IOMMU group 7
        00:1c.0 PCI bridge [0604]: Intel Corporation 7 Series/C210 Series Chipset Family PCI Express Root Port 1 [8086:1e10] (rev c4)
IOMMU group 8
        00:1c.4 PCI bridge [0604]: Intel Corporation 7 Series/C210 Series Chipset Family PCI Express Root Port 5 [8086:1e18] (rev c4)
IOMMU group 9
        00:1c.5 PCI bridge [0604]: Intel Corporation 82801 PCI Bridge [8086:244e] (rev c4)
        04:00.0 PCI bridge [0604]: ASMedia Technology Inc. ASM1083/1085 PCIe to PCI Bridge [1b21:1080] (rev 03)
        05:00.0 USB controller [0c03]: VIA Technologies, Inc. VT82xx/62xx UHCI USB 1.1 Controller [1106:3038] (rev 62)
        05:00.1 USB controller [0c03]: VIA Technologies, Inc. VT82xx/62xx UHCI USB 1.1 Controller [1106:3038] (rev 62)
        05:00.2 USB controller [0c03]: VIA Technologies, Inc. USB 2.0 [1106:3104] (rev 65)
IOMMU group 10
        00:1d.0 USB controller [0c03]: Intel Corporation 7 Series/C210 Series Chipset Family USB Enhanced Host Controller #1 [8086:1e26] (rev 04)
IOMMU group 11
        00:1f.0 ISA bridge [0601]: Intel Corporation H77 Express Chipset LPC Controller [8086:1e4a] (rev 04)
        00:1f.2 SATA controller [0106]: Intel Corporation 7 Series/C210 Series Chipset Family 6-port SATA Controller [AHCI mode] [8086:1e02] (rev 04)
        00:1f.3 SMBus [0c05]: Intel Corporation 7 Series/C210 Series Chipset Family SMBus Controller [8086:1e22] (rev 04)
IOMMU group 12
        02:00.0 Multimedia controller [0480]: Altera Corporation Device [1172:4c15] (rev 01)
IOMMU group 13
        03:00.0 Ethernet controller [0200]: Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller [10ec:8168] (rev 09)

となっています。GPUはIOMMU group 1です。USBは、結局使ってないもののPCIでUSB2.0ポートを増設してしまっていたり、背面ポートと前面用のポートが別のコントローラにぶら下がってたりして見にくいですが、IOMMU group 10とIOMMU group 5をゲストに渡します。

ホストのLinuxの設定

わたしの環境は

  • カーネルを自分でビルド
  • initramfsは使わない
  • systemdで起動

というものです。ディストリビューションはGentoo Linuxですが、ディストリビューション依存の設定は特に行っていません。

カーネルコンフィグ・モジュール読み込み

QEMU-KVMに必要なオプションはhttps://wiki.gentoo.org/wiki/QEMU[QEMU - Gentoo Wiki]に書いてます。PCIパススルーに関係するコンフィグはhttps://wiki.installgentoo.com/index.php/PCI_passthrough[PCI passthrough - InstallGentoo Wiki]を参考に

  • CONFIG_INTEL_IOMMU y
  • CONFIG_IRQ_REMAP y
  • CONFIG_VFIO m
  • CONFIG_VFIO_PCI m
  • CONFIG_VFIO_PCI_VGA y

とします(Intel CPUの場合)。

さらに

  • CONFIG_INTEL_IOMMU_DEFAULT_ON

も有効にすることで起動時のカーネルオプションが不要となります。

vfio-pciモジュール読み込み、モジュールへのオプション指定、VFIO fdのパーミッション設定を行います。

/etc/modules-load.d/pci-thru.conf
vfio-pci
/etc/modprobe.d/pci-thru.conf
options vfio-pci ids=10de:1380,10de:0fbc,8086:1e26,8086:1e2d
/etc/udev/rules.d/10-vfio.rules
SUBSYSTEM=="vfio", OWNER="root", GROUP="kvm"

そして、vfio-pciモジュールが指定されたPCIデバイスを先に制御下に置くことができるよう、パススルーするデバイスに対応するモジュールをカーネル組み込みから外します。わたしの場合は

  • CONFIG_SND_HDA_INTEL
  • CONFIG_USB_XHCI_HCD
  • CONFIG_USB_EHCI_HCD
  • CONFIG_USB_UHCI_HCD

をモジュールにすることとなります。またNVidiaのグラフィックドライバは、オープンソース・プロプライエタリなもの両方とも消しました。

こういうアドホックなカーネルコンフィグをしなくても、オプションを適当に渡すとか後からモジュールをunbindするとかすれば大丈夫なようです(PCI passthrough via OVMF - ArchWikiなど参考)。

ネットワーク周り

仮想マシンをLANに公開したかったので、ブリッジの下にTAPインターフェースをぶら下げることにします。TAPインターフェースの説明はhttps://www.kernel.org/doc/Documentation/networking/tuntap.txt[Linuxカーネルソース添付のtuntap.txt]など。

まずブリッジを作って物理NICを入れて、ブリッジインターフェースにIPアドレスを割り当てます。

/etc/systemd/network/00-bridge.netdev
[NetDev]
Name=br0
Kind=bridge
/etc/systemd/network/10-eth-bridge.network
[Match]
Name=eth0

[Network]
Bridge=br0
/etc/systemd/network/50-bridge-static.network
[Match]
Name=br0

[Network]
Address=192.168.1.40/24
Gateway=192.168.1.1

QEMUのWikiには、適当なオプションを付けてQEMUを実行したら、non-rootユーザーからでも自動でTAPインターフェースを作ってブリッジに入れることができると書いていますが、Debianではセキュリティを懸念してqemu-bridge-helperのsetuidを外しているのでrootでしか使えないようで、Gentooでもsetuidは外れていました。あんまりrootで起動したくなかったので、自分でkvmグループからアクセスできるTAPインターフェースを作っておきます。

/etc/systemd/network/00-tap.network
[NetDev]
Name=tap0
Kind=tap
User=root
Group=kvm
/etc/systemd/network/10-tap-bridge.network
[Match]
Name=tap0

[Network]
Bridge=br0

QEMUの起動

QEMUのオプションは正直なところアレでした。短いけど汎用性のない古いオプションと、より汎用的な新しいオプションがあります。暗黙に色んなことが設定されて、それを上書きしたり、取り消したりするオプションが色々あります。こういう指定をするときにこう書くというパターンが確立されてるわけでもなさそうで、人によってバラバラです。ユーザーガイドに載っていないことも結構あります。直接起動するよりlibvirtを使うのがいいのかもしれません。

ともあれ、わたしの起動スクリプトは以下です。

#!/bin/sh

## to add the optical drive
# device_add scsi-block,bus=scsi0.0,drive=sr0dr,id=sr0
## to remove the optical drive
# device_del sr0

qemu-system-x86_64 \
        -nographic -nodefaults -monitor stdio \
        -rtc base=localtime -enable-kvm -cpu host,kvm=off -smp 3 -m 4096 \
        -drive file="$HOME/edk2/Build/OvmfX64/DEBUG_GCC49/FV/OVMF.fd",readonly,if=pflash \
        -drive file=/dev/sdb,format=raw,cache=none,if=virtio \
        -device virtio-scsi-pci,id=scsi0 \
        -drive file=/dev/sr0,format=raw,cache=none,if=none,id=sr0dr \
        -vga none -device vfio-pci,host=01:00.0,x-vga=on -device vfio-pci,host=01:00.1 \
        -device vfio-pci,host=00:1a.0 -device vfio-pci,host=00:1d.0 \
        -netdev tap,id=vmnic,ifname=tap0,script=no,downscript=no -device virtio-net,netdev=vmnic

-drive file=/dev/sdb,format=raw,cache=none という指定は、/dev/sdb として認識されているSSD一枚を丸ごと仮想マシンに割り当てるための設定です。 光学ドライブ関係は後で書きます。

Windowsのインストール

WindowsにはvirtioドライバがないのでRedHatが署名したドライバを入れる必要があったり、Windows 10へのアップグレードが上手くいかなかったりします。QEMU - ArchWiki: Preparing a Windows guestに書いてます。

SCSIパススルー

virtioでHDDや光学ディスクドライブを半仮想化する方法として、半仮想化されたブロックデバイスをゲストに見せるvirtio-blkと、半仮想化されたSCSIコントローラをゲストに見せるvirtio-scsiがあります。SCSIパススルーに使うのは後者です。/dev/sdX とか /dev/srX とかをファイル名として指定してやると、自動的にSCSIコマンドのパススルーが行われます。

しかしメディアが入っていない光学ドライブを指定して起動できなかったりと、メディアの出し入れ周りが完全にはならないようです。なのでQEMUのホットプラグ機能を用いて、device_add コマンドと device_del コマンドでデバイス自体を付けたり外したりすることで動かしました。