【回忆录】 Linux Subsystem Window (LSW) 折腾记

前言

装完这台机器之后,我一度觉得自己站在了硬件自由的顶点——核显跑 Linux、独显丢给 Windows VM,鼠标键盘手柄全部直通进去,开机选好系统就完事。这个方案俗称 LSW,Linux Subsystem Windows,就是 WSL 反过来(

这篇是事后整理的,当时折腾的时候没空同步写,好在过程都还记得。本机配置如下:

项目 规格
OS Fedora Linux 43 x86_64 + sway (Wayland)
Kernel 6.19.9-200.fc43.x86_64
CPU AMD Ryzen 9 9950X3D (16c/32t) @ 5.756GHz
宿主显示 AMD Radeon 核显(iGPU)
直通显卡 NVIDIA GeForce RTX 5070 Ti(vfio-pci,01:00.0 + 01:00.1
内存 62GB 总量,分配 32GB 给 VM
主板 ASUSTeK ROG STRIX B450-F GAMING

核显带着 Fedora + Sway 跑日常,RTX 5070 Ti 完整直通给 Windows VM,网络走桥接,鼠键手柄 USB 直通。用起来和裸机 Windows 基本没有区别——除了 Vanguard,那东西不认 Hypervisor,暂时没辙。


0x00 准备

先把 IOMMU 打开

这个没什么好说的,进 BIOS 把 AMD-V 和 IOMMU(AMD-Vi)都打开,然后在内核参数里加两个东西:

amd_iommu=on iommu=pt

编辑 /etc/default/grub,加到 GRUB_CMDLINE_LINUX 末尾,重新生成引导:

sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo reboot

重启之后跑一下 dmesg | grep -i iommu,看到 AMD-Vi 相关的输出就说明开好了。

确认显卡的 IOMMU 分组

VFIO 直通有个前提:你要直通的设备必须和其他不想直通的东西不在同一个 IOMMU 组里,否则得一起带走。RTX 5070 Ti 有两个 function——GPU 本体(01:00.0)和 HDMI Audio(01:00.1),刚好同组,正常直通就行,不用额外折腾 ACS patch 之类的东西。

用这个脚本查一下自己的情况:

for d in /sys/kernel/iommu_groups/*/devices/*; do
  n=${d#*/iommu_groups/*}; n=${n%%/*}
  printf 'IOMMU Group %s ' "$n"
  lspci -nns "${d##*/}"
done | grep -i nvidia

安装依赖

sudo dnf install qemu-kvm libvirt virt-manager edk2-ovmf swtpm \
                 libvirt-daemon-config-network
sudo systemctl enable --now libvirtd
sudo usermod -aG libvirt,kvm $(whoami)

装完记得重新登录一下,不然 libvirt 组的权限不会生效,之后 virt-manager 会各种权限报错。

把 RTX 绑到 vfio-pci 上

这步是整个方案的核心。需要让内核在加载 nouveau(NVIDIA 开源驱动)之前先把显卡抢过来绑给 vfio-pci,这样 NVIDIA 那边就完全碰不到它了。

/etc/modprobe.d/vfio.conf

options vfio-pci ids=10de:2c05,10de:22e9
softdep nouveau pre: vfio-pci
softdep snd_hda_intel pre: vfio-pci

10de:2c05 是 GPU,10de:22e9 是 HDMI Audio。softdep 这个写法比去改 dracut conf 简洁多了,直接声明软依赖顺序,内核自己会处理好。

然后重新生成 initramfs,重启:

sudo dracut -f --kver $(uname -r)
sudo reboot

重启之后用 lspci -k | grep -A2 NVIDIA 确认一下,看到 Kernel driver in use: vfio-pci 就对了。到这里核显接管了 Fedora 的显示,独显处于"待命"状态,等着被 VM 拿走。

配一下桥接网络

用 NAT 的话 VM 会躲在宿主机后面,外面完全看不到它,Moonlight 这类串流软件就没法直接连上去。桥接之后 VM 会在局域网里拿一个真实的 IP,和普通物理机没有区别,串流、远程桌面、局域网联机都能正常用。

建桥接接口 br0

nmcli con add type bridge ifname br0 con-name br0
nmcli con add type bridge-slave ifname enp6s0 master br0
nmcli con up br0

enp6s0 换成你自己的物理网卡名,ip link 查一下就有。Windows 那边装好 Sunshine 之后,局域网内任意设备用 Moonlight 就能直接找到并连接,走的是 VM 自己的 IP,跟宿主机完全独立。

下载镜像

  • Windows 11 ISO:微软官网媒体创建工具下载
  • virtio-win ISO:去 Fedora 项目那边找最新的 stable 版,Windows 里所有半虚拟化驱动都在这里面

0x01 安装系统

创建虚拟磁盘

sudo qemu-img create -f qcow2 /var/lib/libvirt/images/win11.qcow2 120G

用 virt-manager 完成初装,然后手动调 XML

virt-manager 图形界面装系统很方便,但很多细节配置它给不了,装完之后要用 virsh edit win11 手动改 XML。下面是本机最终跑起来的关键配置段。

内存和 CPU

给 VM 分 32GB 内存,CPU 给 16 个 vCPU,绑在物理核 0-7 和它们的 HT 兄弟核 16-23 上:

<memory unit='KiB'>33554432</memory>
<vcpu placement='static' cpuset='0-15'>16</vcpu>

<cputune>
  <!-- 交错绑定:每对 vCPU 共享同一个物理核的 L1/L2 缓存 -->
  <vcpupin vcpu='0'  cpuset='0'/>  <vcpupin vcpu='1'  cpuset='16'/>
  <vcpupin vcpu='2'  cpuset='1'/>  <vcpupin vcpu='3'  cpuset='17'/>
  <vcpupin vcpu='4'  cpuset='2'/>  <vcpupin vcpu='5'  cpuset='18'/>
  <vcpupin vcpu='6'  cpuset='3'/>  <vcpupin vcpu='7'  cpuset='19'/>
  <vcpupin vcpu='8'  cpuset='4'/>  <vcpupin vcpu='9'  cpuset='20'/>
  <vcpupin vcpu='10' cpuset='5'/>  <vcpupin vcpu='11' cpuset='21'/>
  <vcpupin vcpu='12' cpuset='6'/>  <vcpupin vcpu='13' cpuset='22'/>
  <vcpupin vcpu='14' cpuset='7'/>  <vcpupin vcpu='15' cpuset='23'/>
  <!-- emulator 线程单独钉在其他核上,不跟 vCPU 抢时间片 -->
  <emulatorpin cpuset='8-11'/>
</cputune>

<numatune>
  <memory mode='preferred' nodeset='0'/>
</numatune>

交错绑定的意义在于:vCPU 0 和 vCPU 1 同时映射到物理核 0 的两个逻辑线程,Windows 的调度器看到的 SMT 拓扑就和真实硬件一致了,不会傻乎乎地把本该放一起的线程扔到两个不同物理核上。物理核 8-15 整个留给宿主机 Fedora,够用。

CPU 特性

<cpu mode='host-passthrough' check='none' migratable='on'>
  <topology sockets='1' dies='1' clusters='1' cores='8' threads='2'/>
  <cache mode='passthrough'/>
  <feature policy='disable' name='hypervisor'/>  <!-- 对外隐藏虚拟机标志 -->
  <feature policy='require' name='svm'/>
  <feature policy='require' name='invtsc'/>       <!-- 不变时间戳,游戏计时不乱跳 -->
  <feature policy='require' name='topoext'/>      <!-- AMD SMT 拓扑扩展 -->
</cpu>

Secure Boot + EFI

Windows 11 强制要求 Secure Boot,用 OVMF 的 secboot 固件就能过:

<os firmware='efi'>
  <type arch='x86_64' machine='pc-q35-8.2'>hvm</type>
  <firmware>
    <feature enabled='yes' name='enrolled-keys'/>
    <feature enabled='yes' name='secure-boot'/>
  </firmware>
  <loader readonly='yes' secure='yes' type='pflash' format='raw'>
    /usr/share/edk2/ovmf/OVMF_CODE.secboot.fd
  </loader>
  <nvram template='/usr/share/edk2/ovmf/OVMF_VARS.secboot.fd'
         format='raw'>/var/lib/libvirt/qemu/nvram/win11_VARS.fd</nvram>
  <smbios mode='sysinfo'/>
</os>

TPM

TPM 2.0 也是 Windows 11 必须的,不过这里有个省事的地方——libvirt 会自己拉起 swtpm 进程,完全不用手动操作:

<tpm model='tpm-crb'>
  <backend type='emulator' version='2.0'/>
</tpm>

GPU 直通

这是整个配置最爽的部分,两行就把 RTX 5070 Ti 完整塞进去了:

<!-- GPU 本体 -->
<hostdev mode='subsystem' type='pci' managed='yes'>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
  </source>
</hostdev>
<!-- HDMI Audio -->
<hostdev mode='subsystem' type='pci' managed='yes'>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x1'/>
  </source>
</hostdev>

VM 里不需要配任何虚拟显示设备,显示输出完全走物理 GPU,连 VNC 都不用开。

网络和 USB

<!-- 桥接网络,VM 拿真实局域网 IP,Moonlight/Sunshine 串流可直连 -->
<interface type='bridge'>
  <source bridge='br0'/>
  <model type='virtio'/>
</interface>

<!-- 罗技设备 -->
<hostdev mode='subsystem' type='usb' managed='yes'>
  <source><vendor id='0x046d'/><product id='0xc548'/></source>
</hostdev>
<!-- Xbox 360 手柄 -->
<hostdev mode='subsystem' type='usb' managed='yes'>
  <source><vendor id='0x045e'/><product id='0x028e'/></source>
</hostdev>

0x02 安装 virtio

Windows 装完之后先别急着装驱动,挂上 virtio-win.iso,跑一下 virtio-win-gt-x64.msi,把所有半虚拟化驱动一口气装完。

驱动 干嘛用的
virtio-net 网卡,装完才能联网激活 Windows
virtio-blk 磁盘驱动,I/O 性能大幅提升
virtio-balloon 内存气球,闲置内存动态回收给宿主机
virtio-serial 主客机通信通道

装完重启,这时候 RTX 5070 Ti 已经出现在设备管理器里了,装上 NVIDIA 驱动就能正常用,和裸机装驱动没有任何区别。


0x03 QEMU 虚拟机伪装

这是整个过程里最需要搞清楚"为什么"的部分。NVIDIA 驱动会主动检测自己是不是跑在虚拟机里,如果检测到了就报 Code 43,拒绝工作。需要从几个层面同时下手把这个检测干掉。

把 KVM 的特征藏起来

最关键的两个开关:

<features>
  <kvm>
    <hidden state='on'/>  <!-- NVIDIA 驱动主要就靠这个特征位判断 -->
  </kvm>
  <vmport state='off'/>   <!-- 关掉 VMware 兼容端口,别让它误以为是 VMware -->
  <smm state='on'/>
</features>

Hyper-V Enlightenments + vendor_id

这一堆 Hyper-V 特性开启之后性能会好一些(Windows 知道自己在虚拟机里可以用更高效的接口),但开了之后 NVIDIA 也会更容易检测到。解决办法是把 vendor_id 设成随机值,让驱动的检测逻辑失效:

<hyperv mode='custom'>
  <relaxed state='on'/>
  <vapic state='on'/>
  <spinlocks state='on' retries='8191'/>
  <vpindex state='on'/>
  <runtime state='on'/>
  <synic state='on'/>
  <stimer state='on'/>
  <reset state='on'/>
  <vendor_id state='on' value='random'/>  <!-- 这个是绕过 Code 43 的关键 -->
  <frequencies state='on'/>
  <reenlightenment state='on'/>
  <tlbflush state='on'/>
  <ipi state='on'/>
</hyperv>

SMBIOS 注入真实主板信息

一些更激进的检测手段会去读 DMI 信息,QEMU 默认填的那些 “System manufacturer” / “Default string” 一眼就是虚拟机。直接把主板的真实信息塞进去:

<sysinfo type='smbios'>
  <bios>
    <entry name='vendor'>American Megatrends Inc.</entry>
    <entry name='version'>5302</entry>
    <entry name='date'>10/20/2023</entry>
  </bios>
  <baseBoard>
    <entry name='manufacturer'>ASUSTeK COMPUTER INC.</entry>
    <entry name='product'>ROG STRIX B450-F GAMING</entry>
  </baseBoard>
</sysinfo>

在 CPU 层面也把 hypervisor 位禁掉

<cpu mode='host-passthrough' ...>
  <feature policy='disable' name='hypervisor'/>
</cpu>

把上面这些组合在一起,NVIDIA 驱动就装得上去了,Code 43 不会再出现。


0x04 性能优化

其实做完前面的部分,大部分性能已经出来了。这里再聊几个可以进一步调的地方。

CPU Pinning 的绑法

前面 XML 里的交错绑定方式看起来有点奇怪——为什么不直接 0→0, 1→1, 2→2 这样顺着绑,而是 0→0, 1→16, 2→1, 3→17?

原因是 9950X3D 的线程编号方式:物理核 0 对应逻辑线程 0 和 16,物理核 1 对应线程 1 和 17,以此类推。交错绑定保证了每对 vCPU 始终在同一个物理核上,共享 L1/L2 缓存,Windows 里看到的 SMT 拓扑也和真实硬件一致。如果顺着绑,vCPU 0 在核 0,vCPU 1 在核 1,SMT 兄弟就对不上了,某些对 NUMA/SMT 敏感的负载会跑得比较难看。

vCPU 0,1  → 物理核 0(线程 0, 16)
vCPU 2,3  → 物理核 1(线程 1, 17)
...以此类推...
emulator  → 线程 8-11(单独的核,不抢 vCPU)

时钟

<clock offset='localtime'>
  <timer name='rtc' tickpolicy='catchup'/>
  <timer name='pit' tickpolicy='delay'/>
  <timer name='hpet' present='no'/>
  <timer name='hypervclock' present='yes'/>
</clock>

hypervclock 配合前面开启的 Hyper-V Enlightenments 使用,Windows 会用更高效的方式同步时钟,游戏帧时间的抖动会小一些。

磁盘

目前跑的是 qcow2 + virtio,日常够用。如果有一块 NVMe 专门给 Windows,直接以裸块设备传进去是最干净的方案,绕过 qcow2 的文件层:

<disk type='block' device='disk'>
  <driver name='qemu' type='raw' cache='none' io='native'/>
  <source dev='/dev/disk/by-id/nvme-xxxxxx'/>
  <target dev='vda' bus='virtio'/>
</disk>

最终效果

项目 表现
CPU 16 vCPU pinned,Cinebench 损耗 <3%
磁盘 virtio qcow2,日常使用无感知
显卡 RTX 5070 Ti 完整直通,3DMark 接近裸机
网络 桥接 br0,VM 独立局域网 IP,Moonlight 可直连
鼠键/手柄 USB 直通,无延迟,即插即用
游戏兼容性 BE/EAC 可运行;Vanguard 因检测 Hypervisor 不支持

整体来说是个相当完整的方案了。宿主 Fedora 用核显跑 Sway,完全感觉不到旁边还有个 Windows VM 在消耗资源;Windows 那边有完整的独显,性能和裸机基本没区别。

Comment