Files
cloudhost/windows11-arm64-install-speed.md
GuofangandClaude Sonnet 5.5 78f6fd424b Read port forwards from per-VM forwards.conf files
A QEMU config file section cannot hold more than one hostfwd entry, so
the forwards move out of the ini files into qemu/<vm>/forwards.conf, one
"LABEL HOST_PORT GUEST_PORT" rule per line. The new vmforwards.py parses
them, skips rules whose host port is taken and builds the -netdev
argument, which every launcher now passes instead of relying on a
netdev section in the ini or on -nic.

The ini files move to qemu/<vm>/config.ini and qemu/<vm>/<os>-<arch>.ini.
The *_SSH_PORT / *_RDP_PORT variables and --rdp-port / --ssh-port still
replace the host port of the SSH and RDP rules.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 14:55:30 +08:00

236 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/config.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`。