Files
cloudhost/windows11-arm64-install-speed.md
T
GuofangandClaude Sonnet 5.5 07a2c6db3e Skip kernel-irqchip=off when installing with --install-from-img
Move the setting from qemu/windows11-darwin-arm64.ini to a -machine option
that run-windows11.py adds on macOS arm64 unless --install-from-img is given.
The workaround is only needed for usb-storage, which the NVMe install disk
does not use, and Setup hangs with the emulated interrupt controller.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 13:33:06 +08:00

14 KiB
Raw Blame History

macOS 上 Windows 11 Arm 安装慢的分析与处理

记录在 Apple Silicon(M5 Max,macOS 26.4,QEMU 11.1.2,HVF)上用 ./run-windows11.py --install-from-iso 安装 Windows 11 Arm 时"安装很慢"的分析过程、验证数据和最终方案。

结论

  • 根因(第五节):HVF 的内核态 GICv3 与 QEMU 的 usb-storage 有竞态,Windows Setup 每读一批数据就卡约 10 秒。 由 run-windows11.py 加 -machine kernel-irqchip=off(--install-from-img 时不加)后, 同样走 USB 光驱的 --install-from-iso 也变快了,和 UTM 的速度一致。
  • 下面第一到五节是更早的分析:当时把慢归因于"模拟 USB 光驱吞吐低"(平均约 4 MB/s), 这个归因不准确,真实原因是上面的 10 秒停顿;那时测到的低吞吐就是停顿拉低的平均值。
  • 把安装介质改成FAT32 分区里的 NVMe 磁盘镜像(install.wim 拆成 .swm,--install-from-img)仍然有效, 因为它绕开了 usb-storage,读速达到 54 到 92 MB/s,镜像由 vm-images/build-windows-install-disk.sh 生成。 但现在不是必须的。

环境

项目 值
主机 Apple M5 Max,arm64,macOS 26.4,18 核,48 GB 内存
QEMU 11.1.2,qemu-system-aarch64,加速器 HVF
客户机配置 qemu/windows11.ini + qemu/windows11-darwin-arm64.ini(virt 机型,8 vCPU,8G 内存,NVMe 系统盘,virtio-net,ramfb,usb-kbd/usb-tablet)
安装 ISO Windows11_Client_arm64_en-us_26300_9457.iso(卷标 CCCOMA_A64FRE_EN-US_DV9,install.wim 7.6 GB,boot.wim 630 MB)
驱动 ISO virtio-win.iso(含 ARM64 驱动目录,根目录只有 x64/x86 的 MSI)

一、基线:为什么慢

用 QEMU monitor 的 info blockstats 每 15 秒采样一次,用相邻两次的差值算读写速度 (采样脚本思路:连接 qemu-monitor.sock,发送 info blockstats,解析 Read/Write 的字节数、次数和总耗时)。

USB 光驱基线(带 Cocoa 窗口,15 个采样):

指标 平均 最大
读安装 ISO 4.1 MB/s(约 50 iops,每次约 64 KB) 13.3 MB/s
写系统盘 8.6 MB/s 30.5 MB/s

无头模式下的结果相近(读 3.5 MB/s,写 5.8 MB/s),所以窗口显示不是瓶颈。

排除的因素:

  • CPU:QEMU 只用 29% 到 123%(主机 18 核),不是算力问题;HVF 硬件加速在工作。
  • 主机磁盘与刷盘:qemu-img bench 往 qcow2 写 64K 块并逐次 flush,2000 次仅 0.31 秒; info blockstats 里主机侧每次读写只有 0.02 到 0.1 毫秒,flush 次数为 0。 所以曾考虑过的 cache.no-flush=on 没有价值,已撤销,未提交。
  • 瓶颈:虚拟机自己发出的请求很少(几十 iops),主机很快、客户机在等, 与"模拟 USB 存储吞吐低"吻合。写系统盘的速度也跟着读 ISO 的速度走。

公开资料:Parallels Desktop 在 Apple Silicon 上装 Windows 11 Arm 约 15 分钟内完成, 说明同一台机器上这个安装本来不该这么慢;Linaro 的 Windows on Arm 指南也用 usb-storage 挂 ISO, 但没有找到专门量化 USB 光驱吞吐的资料,所以"USB 是瓶颈"是本地测量得出的判断。

二、走不通的尝试

  1. 把 ISO 直接挂到 NVMe(nvme,drive=...,media=cdrom):QEMU 报 aio failed: Operation not permitted, Setup 提示缺少介质驱动,读不到安装文件。
  2. 磁盘镜像只读挂载(readonly=on):NVMe 不会向 Windows 报告写保护,Windows 挂载 NTFS 时会尝试写入, 被 QEMU 拒绝(同样是 aio failed,3000 多条),卷被判为 RAW。 改成 snapshot=on(对客户机可写,写入落到临时覆盖层,不改镜像文件)后,错误消失。
  3. NTFS 分区放 install.wim:即使 NTFS 能被识别(WINSRC,盘符 C:),Setup 日志仍显示 Evaluating drive [C] as media location 之后 Found install image at [(null)],页面停在 "Install driver to show hardware"。已试过:补全 mkfs.ntfs 的分区起始扇区/磁头参数(-p 3147776 -H 255 -S 63)、 把 FAT 引导分区类型从 EFI(EF00)改成普通数据(0700)以便分配盘符、把引导文件整份复制进 NTFS,均无效。 根因没有查清。UEFI 固件(EDK2)也不能直接从 NTFS 引导,所以 NTFS 方案放弃。

三、最终方案:FAT32 + 拆分的 install.swm

FAT32 单文件上限 4 GB,而 install.wim 有 7.6 GB,所以用 wimlib 拆成两个分卷 (Windows Setup 认 install.swm + install2.swm)。整个介质放在一个 FAT32 分区里:

  • 镜像:10 GiB 的 raw 文件,GPT,单个 FAT32 分区(类型 0700,卷标 WINPE)。
  • 内容:ISO 里除 sources/install.wim 外的全部文件,加上 sources/install.swm(3.98 GB)和 sources/install2.swm(3.60 GB)。
  • 引导:从磁盘走 efi\boot\bootaa64.efi,不经过光盘的 "Press any key to boot from CD or DVD" 提示,不需要按键。
  • 以 NVMe 磁盘挂载,snapshot=on,bootindex=2(系统盘为 1)。

制作步骤

用脚本 vm-images/build-windows-install-disk.sh,在 macOS 上直接完成,不需要 root,也不需要 Debian 虚拟机;脚本也支持 Debian(见 vm-images/README.md):

brew install wimlib dosfstools gptfdisk mtools     # 一次性
vm-images/build-windows-install-disk.sh [--force] ISO [OUTPUT.img]

OUTPUT 默认是 ISO 路径把 .iso 换成 .img。脚本做的事:

  1. hdiutil attach -readonly 把 ISO 挂载到临时目录。
  2. wimlib-imagex split 把 install.wim 拆成两个不超过 3800 MiB 的分卷。
  3. 按内容大小算镜像大小(内容加 5% 再加 64 MiB),sgdisk 建 GPT 和单个 FAT32 分区(类型 0700,卷标 WINPE), mkfs.fat --offset 直接格式化镜像里的分区。
  4. 用 mmd/mcopy 以字节偏移直接写入镜像(不需要挂载),复制除 install.wim 外的全部文件和两个 .swm。

mkfs.fat 在 Homebrew 里装在 sbin,脚本会自动把 brew 的 sbin 加入 PATH。 *.img 已被 vm-images/.gitignore 忽略,不会被提交。

脚本在 macOS 上生成的镜像(8571 MiB,约 44 秒)已验证:文件和目录清单与 ISO 一致(各 1055 项,差异只是目录名末尾的 /), boot.wim 和 bootaa64.efi 逐字节一致,两个 .swm 大小与 Debian 方案一致, 并用下面的方式启动,Setup 正常运行(约 70 秒到 37%,读安装介质 54 到 84 MB/s)。

早先的验证用的是借 Debian 虚拟机(./run-debian13.py,里面用 losetup、mkfs.vfat、wimlib)手工做的镜像, 步骤相同,结果相同。

启动方式

run-windows11.py 的 --install-from-img 用这个镜像做安装介质,隐含 --install-from-iso。镜像路径是 ISO 路径 (WIN11_VM_INSTALL_ISO 或自动查找到的 ISO)把 .iso 换成 .img,也就是 build-windows-install-disk.sh ISO 默认生成的位置, 此时不再读取 ISO:

./run-windows11.py --install-from-img

install_media 不再作为 usb-storage 光驱,而是:

-drive file=<镜像>,if=none,id=install_media,format=raw,snapshot=on
-device nvme,drive=install_media,serial=installer,bootindex=2

snapshot=on 的原因见上面第二节:NVMe 不报告写保护,Windows 会写它挂载的卷,只读挂载会让卷变成 RAW; snapshot=on 让写入落到临时覆盖层,镜像文件本身不会被改动。 应答介质(CIDATA 的 seed ISO)和 VirtIO ISO 仍然走 usb-storage,它们很小,不影响速度。 镜像文件不存在时会报错并提示用 build-windows-install-disk.sh 生成。

实测

带 Cocoa 窗口,前 7 个采样(复制安装文件阶段):

时间 读安装介质
18 s 54.3 MB/s
35 s 84.7 MB/s
53 s 62.6 MB/s
70 s 66.6 MB/s
88 s 80.6 MB/s
105 s 79.8 MB/s
123 s 92.2 MB/s
  • 平均约 74 MB/s,对比 USB 光驱基线的 4.1 MB/s,约快 18 倍。
  • 写系统盘该阶段约 95 到 143 MB/s,对比基线 8.6 MB/s。
  • 约 80 秒时 Setup 显示 "Installing Windows 11" 40%;约 140 秒后读安装介质基本结束,进入后续阶段(系统盘读写为主)。
  • 同一台机器、同样的 ISO,USB 光驱方式到 49% 用了约 35 分钟。

四、副产品与已知问题

  • Autounattend.xml 的映像序号:文件里写 /IMAGE/INDEX 为 6(适用于 x64 的多版本 ISO), 但这个 Arm ISO 的 install.wim 只有 3 个映像:1 Home、2 Home Single Language、3 Pro。 实际安装能继续(应答文件里的产品密钥是 Pro 的通用密钥,Setup 似乎据此选了版本), 但序号 6 并不存在,应该核对。我没有改这个文件。
  • VirtIO 来宾工具:Configure-Windows.ps1 用 virtio-win-guest-tools.exe(32 位 x86 程序)。 当前 virtio-win.iso 根目录只有 virtio-win-gt-x64.msi 和 virtio-win-gt-x86.msi,没有 arm64 的 MSI。 上游 PR #100 为安装器加了 ARM64 支持 (virtio-win-gt-arm64.msi,含 NetKVM、viostor 等),但我没确认它在哪个发行版里发布。 arm64 的网卡是 virtio-net,驱动装不上就没有网络,后面联网下载的 .bat 会失败。 可选方案:换含 arm64 MSI 的 ISO,或在脚本里用 pnputil 从 NetKVM\w11\ARM64 装驱动。未验证、未修改。
  • 安装脚本的架构:(已修改)40-Install-Rustup.bat 和 60-Install-Qt6.bat 按 PROCESSOR_ARCHITECTURE 选择 aarch64 的 rustup-init 和 win64_msvc2022_arm64 的 Qt。aqt 没有 Arm64 版,aqt_x64.exe 仍靠模拟运行,只负责下载。
  • 按键提示:用 USB 光驱(原方式)时,Arm 版 ISO 会显示 "Press any key to boot from CD or DVD",几秒内不按键会掉进 UEFI Shell。 磁盘镜像方式不经过这个提示。曾经加过自动按键逻辑,按要求已删除。
  • 显卡:arm64 配置用 ramfb。virtio-gpu-pci 会让画面冻结在 "Start boot option"(QEMU 固件的 virtio-gpu 输出在 Windows 引导管理器处冻结)。
  • -daemonize 与 Cocoa:Cocoa 窗口不能和 -daemonize 一起用,要看界面只能前台运行。

五、为什么 UTM 用同样的 USB 光驱却很快

现象:同一台机器、同一个 ISO,UTM 里装 Windows 11 Arm 很快,本仓库的 --install-from-iso 很慢。 UTM 和本仓库都是 QEMU + HVF,ISO 也都是 usb-storage(UTM 加 removable=true,挂在 nec-usb-xhci 上),所以设备类型不是原因。

排查过程

  1. 在 UTM 里建一台 QEMU 后端的 Windows ARM 虚拟机,用同一个 ISO 启动,再用 ps -ww -o args= -p $(pgrep -f QEMULauncher) 取出完整命令行。UTM 的 QEMU 是 Frameworks/qemu-aarch64-softmmu.framework 里的 dylib,由 QEMULauncher 加载(沙盒继承,不能脱离 UTM 单独运行)。
  2. 与本仓库的配置逐项对照,差异有:-machine virt,gic-version=3、-cpu host、 -accel hvf,ipa-granule-size=0x1000、6 核、系统盘 discard=unmap,detect-zeroes=unmap、 file.locking=off、nec-usb-xhci、-nodefaults、-rtc base=localtime、UTM 自己的 UEFI 固件等。 把前面几项全部对齐后速度没有改善。
  3. 查 UTM 仓库的提交,找到 95a7f4a2 (PR #7971,Issue #7970)。

根因

UTM 5.0.7 启用了 Hypervisor.framework 的内核态 GICv3。这暴露了 QEMU usb-storage 的一个竞态: 命令完成时,客户机已经排队的状态包(CSW)被重入处理,请求仍标记为在途,被"挂起"后永远得不到应答。 Windows 对 USB 存储传输的超时是 10 秒,于是每一批读取之后都会停约 10 秒,随后重置设备并重试。 这与第一节测到的现象一致:客户机发出的请求很少、主机很闲、读吞吐只有几 MB/s。

UTM 记录的"复制文件"阶段耗时(Windows 11 24H2 ARM64,USB CD):

构建 耗时
5.0.6 165 秒
5.0.7(有竞态) 约 1230 秒
5.0.7 + -machine kernel-irqchip=off 160 秒
5.0.7 + 修复补丁 约 135 秒

修复补丁在 UTM 的 QEMU fork(utmapp/qemu@ae92cdc41f),在完成数据包之前先释放 SCSI 请求。 UTM 5.0.8 带有这个修复;Homebrew 的 QEMU 11.1.2 没有,所以上游版本会遇到这个问题。

处理

在 qemu/windows11-darwin-arm64.ini 的 [machine] 加 kernel-irqchip = "off",改用模拟的中断控制器。 这一项改完后速度明显提升,已确认和 UTM 一致。

[machine]
  type = "virt"
  kernel-irqchip = "off"

只验证单项(不改配置文件)时,可以用 ./run-windows11.py --install-from-iso -- -machine virt,kernel-irqchip=off。

对齐 UTM 时试过、后来回退的改动

对齐 UTM 时一度同时改了下面这些,但只有 kernel-irqchip=off 确认有效,其余没有单独验证过,已全部回退:

  • -machine 的 gic-version=3、-cpu host、-nodefaults、-rtc base=localtime。
  • 安装介质挂在独立的 nec-usb-xhci 上,usb-storage 加 removable=true,bus=usb-bus.0,ISO 的 -drive 加 file.locking=off。
  • cpus 从 8 改为 6,系统盘加 discard=unmap、detect-zeroes=unmap。

没有对齐的:-accel hvf,ipa-granule-size=0x1000(UTM 的补丁参数)、UTM 的 edk2-aarch64-secure-code.fd、TPM、 virtio-rng-pci、Intel HDA、virtio-ramfb-gl、vmnet-shared。

代价与后续

  • 模拟中断控制器的运行时性能略低于内核态 GIC。若只在安装阶段需要,日常运行可以改回去(去掉 kernel-irqchip)。
  • 等上游 QEMU 合入这个 usb-storage 修复后,可以去掉这一项。

六、后续(需要决定)

  1. (已完成)run-windows11.py --install-from-img 和 build-windows-install-disk.sh。
  2. 核对 Autounattend.xml 的映像序号。
  3. 解决 arm64 下 virtio-net 的驱动安装问题(见上)。
  4. 清理旧的验证产物:vm-images/install-iso/win11-arm64-install.img(10 GiB,Debian 方案做的)和 Debian 虚拟机里的 ~/win11-install.img。