我手上有一块装在 USB 硬盘盒里的 NVMe SSD,想把它整块交给虚拟机当系统盘,在 Mac 上跑一台 Ubuntu Server。要求不多:不用虚拟磁盘文件,支持 TRIM,最好以后带着这块盘换一台 Mac 也能接着用。
TL;DR
macOS 27 给 Virtualization 框架加了 USB 直通,UTM 5.0.8 已经支持。虚拟机选 Apple Virtualization 后端,在菜单栏的 Virtual Machine Accessories 里把硬盘盒分配给 UTM,就能接进虚拟机,Ubuntu 直接装到这块盘上。
装好之后还要在 Ubuntu 里加一条 udev 规则:
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="xxxx", ATTRS{idProduct}=="xxxx", ATTR{provisioning_mode}="unmap", ATTR{cache_type}="temporary write back"
ACTION=="add|change", SUBSYSTEM=="block", KERNEL=="sd*[!0-9]", ATTRS{idVendor}=="xxxx", ATTRS{idProduct}=="xxxx", ATTR{queue/rotational}="0"
xxxx 换成 lsusb 里你的硬盘盒的 ID。第一行打开 TRIM 和写缓存,第二行告诉内核这是 SSD。
走过的弯路
一开始想的是直接把 /dev/diskN 挂给虚拟机,这条路走了好几个方案:
- UTM 直接挂块设备:不行。UTM 是沙盒应用,打不开
/dev/disk*,UTM 5 的源码里也没有块设备直通。 - 自己写 QEMU 脚本:能挂上物理盘,但 TRIM 传不过去。QEMU 只在 Linux 上对
host_device支持 discard,macOS 上不行。 - Virtualization 框架的块设备接口:
VZDiskBlockDeviceStorageDeviceAttachment,macOS 14 就有了。我写了个小程序试了一下,用稀疏镜像模拟块设备,在虚拟机里写 2 GiB 再fstrim,宿主上镜像的实际占用从 2.1 GiB 降到了 168 MiB,virtio-blk 和 NVMe 两种总线都可以。也就是说 TRIM 是能传过去的,这点 Apple 的文档里没写。问题是打开/dev/diskN需要改设备的 owner 或者用 root 跑,我不太想这么做。Lima、Tart、vfkit 也支持块设备,都卡在同一个权限问题上。
最后用了 macOS 27 新加的 USB 直通。这样虚拟机里的 Linux 直接驱动硬盘盒,TRIM 也是 Linux 自己发给硬盘的,不经过 macOS。
USB 直通
USB 直通用的是 VZUSBPassthroughDevice 和 AccessoryAccess 框架,需要 com.apple.developer.accessory-access.usb 这个 entitlement。从 GitHub 下载的 UTM 5.0.8 正式版已经带上了,自己编的版本就不一定有。
分配设备要用系统自带的一个菜单栏程序 Virtual Machine Accessories,它在 /System/Library/Frameworks/AccessoryAccess.framework/Resources/ 下面。在里面把硬盘盒分配给 UTM,UTM 就会把它接到正在运行的虚拟机上。这一步只能在菜单栏手动点,虚拟机里的系统会看到一个真实的 USB 设备。
UTM 的 utmctl usb list 和 utmctl usb connect 也能用,方便写脚本。
接上以后就和装普通电脑一样,用 Ubuntu Server 26.04.1 的 ISO 启动,把系统装到这块盘上。分区我只分了一个 1 GB 的 ESP 和一个 ext4 根分区,没用 LVM,整块盘只给一台虚拟机用,用不上 LVM 的灵活性。
TRIM 默认是关的
装好以后 lsblk -D 一看,DISC-MAX 是 0,不支持 discard。
先看驱动。内核日志里有这么一句:
USB controller 0000:00:0a.0 does not support streams, which are required by the UAS driver
Virtualization 框架模拟的 xHCI 控制器不支持 USB 3 的 bulk streams,所以硬盘盒没法走 UAS,退回了 usb-storage。这是 Apple 虚拟硬件的限制,在虚拟机里改不了。
usb-storage 为了兼容各种奇怪的硬盘盒,不会去读 SCSI 的 VPD 页,内核拿不到盘的 TRIM 能力,provisioning_mode 就默认成了 full。但其实硬盘盒是支持的,用 sg_vpd -p lbpv 能看到 LBPU=1,也就是支持 UNMAP。
手动改一下试试:
echo unmap | sudo tee /sys/block/sda/device/scsi_disk/*/provisioning_mode
sudo fstrim -v /
DISC-MAX 变成了 4G,第一次 fstrim 释放了 459.4 GiB。确认没问题后写成 udev 规则,每次开机自动设置。Ubuntu 默认开着 fstrim.timer,每周跑一次。
写缓存默认也是关的
usb-storage 下这块盘报告的是「Write cache: disabled」,内核因此认为没必要发刷盘命令。但 SSD 是有 DRAM 缓存的,也没有掉电保护,这样拔线或者断电的时候可能丢数据。
所以我把写缓存强制成了 write back。改完以后 4K 同步写的延迟从 1.1 ms 左右变成了 4.6 ms 左右,说明刷盘命令真的到了 SSD,之前那 1.1 ms 是没刷盘的假数据。
这里有个坑。一开始我写的是:
ATTR{cache_type}="write back"
结果 TRIM 又没了,rotational 也变回了 1。原因是写 write back 时,内核会给硬盘发一个 MODE SELECT 去改真实的缓存设置,然后重新探测整块盘,前面设的 provisioning_mode 和 rotational 都被重置回默认值。要写成 temporary write back,这样只改内核自己的判断,不碰硬盘,也不会触发重新探测。
性能
带 fsync 顺序写 4 GB,大概 480 到 510 MB/s,对一个 10 Gb/s 的 USB 硬盘盒来说还行。读速度没测成,dd iflag=direct 报「Invalid input」,还没查原因。
遗留问题
- 走不了 UAS,速度和队列深度都受影响,只能等 Apple 改虚拟 xHCI。
- 虚拟机运行时不能拔硬盘,要先关机,或者在 UTM 里停掉虚拟机。
- 冷启动有问题:关机后再开机,虚拟机一闪就关了。原因和解决办法见 UTM 直通 USB 系统盘时,冷启动一闪就关机。