# 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 秒。 在 `[machine]` 里加 `kernel-irqchip = "off"`(已写入 `qemu/windows11-darwin-arm64.ini`)后, **同样走 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`): ```bash 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: ```bash ./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](https://github.com/virtio-win/virtio-win-guest-tools-installer/pull/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](https://github.com/utmapp/UTM/commit/95a7f4a2) (PR [#7971](https://github.com/utmapp/UTM/pull/7971),Issue [#7970](https://github.com/utmapp/UTM/issues/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 一致。 ```ini [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`。