WWDC Quick Look 💓 By SwiftGGTeam
Expand the capabilities of your Virtualization app

Expand the capabilities of your Virtualization app

观看原视频

Highlight

macOS 27 的 Virtualization 框架新增了四项核心能力:自动配置 macOS 客户机用户账户、通过 Accessory Access 框架安全直通 USB 设备、DiskImageKit 分层稀疏磁盘镜像、以及自定义 Virtio 设备,让第三方虚拟机 App 在自动化、存储效率和硬件扩展性上达到生产级水准。

核心内容

自动配置 macOS 客户机:告别手动点 Setup Assistant

以前装完 macOS 虚拟机,必须手动走一遍 Setup Assistant(设置助理)——创建用户、设密码、点同意条款。对于 CI/CD 流水线或自动化测试场景,这一步是最大瓶颈。每次新建 VM 都要人工介入,无法实现真正的无人值守。

macOS 27 引入了 VZMacGuestProvisioningOptions。启动 VM 时传入用户名、密码、全名,还可以开启自动登录和 SSH 远程登录。客户机首次启动时,这些参数直接传给 Setup Assistant,自动完成配置。

01:56

有个限制:这些选项只在客户机尚未配置过时生效。如果已经创建过用户,后续启动传入的 provisioning 选项会被忽略。密码处理上,Apple 建议从 Keychain 或环境变量读取,不要硬编码。

USB 设备直通:Accessory Access 框架的安全模型

虚拟机里用 U 盘、加密狗、调试器,以前是个老大难问题。要么靠底层 IOKit hack,要么体验不稳定,拔插时 VM 容易卡死。

macOS 27 引入了 Accessory Access 框架。它的核心设计是:用户始终拥有控制权。App 注册感兴趣的设备类型后,macOS 菜单栏会出现一个配件图标。用户手动选择”连接到 App”,设备才会被授权给虚拟机使用。用户也可以随时断开。

04:39

这种设计牺牲了全自动化的便利性,换取了安全性和稳定性。设备可以在 VM 运行时热插拔,不需要重启或修改 VM 的静态配置。对 C 端用户友好,也杜绝了恶意软件偷偷读取 USB 设备的风险。

分层磁盘镜像:DiskImageKit 解决存储空间问题

传统 raw 磁盘镜像是一比一映射的。100GB 的逻辑磁盘就是 100GB 的物理文件。做快照需要复制整个文件,克隆 VM 的成本极高。

macOS 27 的 DiskImageKit 框架引入了 Apple Sparse Image Format(ASIF),支持分层镜像栈。底层是 base layer(基础层),只读,存放干净的系统镜像。上层可以是 cache layer(缓存层)或 overlay layer(覆盖层),都是 ASIF 格式。

11:40

Cache layer 用于加速:底层数据在慢速存储(如网络文件系统)上时,读取过的数据会被缓存到 cache layer,后续读取直接从缓存拿。Overlay layer 实现写时复制(copy-on-write):写入操作只发生在 overlay 层,base 层保持只读不变。多个 VM 可以共享同一个 base layer,各自拥有独立的 overlay,大幅节省磁盘空间。

ASIF 是稀疏格式。逻辑大小可以远大于实际存储的数据量,未使用的块不占物理空间。

自定义 Virtio 设备:打通 Host-Guest 高性能通道

Virtualization 框架已经内置了常见的设备类型,但有些场景需要更 specialized 的通信通道。比如实现自定义协议做 Host-Guest 高性能通信,或者把机器学习加速器暴露给 Linux 客户机。

macOS 27 开放了 VZCustomVirtioDevice。Virtio 是半虚拟化设备的行业标准,通过共享内存缓冲区和 Virtio queue 实现 Host-Guest 通信,设计上最小化上下文切换次数。

15:57

App 配置设备的 Virtio device ID、PCI class/subclass、queue 数量,然后设置 delegate。客户机通过 Virtio queue 发送数据时,delegate 的 didReceiveNotificationFor 被调用,App 从 queue 中取出元素处理,处理完再归还到 queue。App 也可以通过触发中断来通知客户机驱动。

详细内容

自动配置 macOS 客户机

01:56

import Virtualization

let provisioningOptions = VZMacGuestProvisioningOptions()
provisioningOptions.fullName = fullName
provisioningOptions.username = username
provisioningOptions.password = password
provisioningOptions.logsInAutomatically = true
provisioningOptions.enablesRemoteLogin = true

let startOptions = VZMacOSVirtualMachineStartOptions()
try startOptions.setGuestProvisioning(provisioningOptions)

try await virtualMachine.start(options: startOptions)

关键点:

  • VZMacGuestProvisioningOptions 封装了首次启动时需要传递给 Setup Assistant 的所有参数
  • fullNameusernamepassword 用于创建用户账户
  • logsInAutomatically = true 让系统首次启动后自动登录到该账户
  • enablesRemoteLogin = true 开启 SSH 服务,方便远程管理
  • setGuestProvisioning 把配置附加到启动选项上
  • 这些参数只在客户机首次启动、尚未配置用户时生效

注册 USB 配件监听器

07:12

import AccessoryAccess

let criteria: [AAUSBAccessoryMatchingCriteria] = []
let accessories = try await AAUSBAccessoryManager.shared.registerListener(self, matchingCriteria: criteria)

for accessory in accessories {
    // Handle previously attached accessories.
}

关键点:

  • AAUSBAccessoryMatchingCriteria 用于过滤感兴趣的设备类型,可以按 device class、vendor ID、product ID 等条件筛选
  • 传入空数组表示对所有 USB 设备感兴趣
  • registerListener 会返回已经授权给 App 的设备列表,需要遍历处理
  • 监听器对象需要实现 AAUSBAccessoryListener 协议
  • 需要在 Xcode Target 的 Capabilities 中添加 “Claim USB Accessory”

响应 USB 配件连接

07:39

import AccessoryAccess
import Virtualization

class AccessoryListener: NSObject, AAUSBAccessoryListener {
    func usbAccessoryDidConnect(_ usbAccessory: AAUSBAccessory) {
        virtualMachine.queue.async {
            do {
                let configuration = VZUSBPassthroughDeviceConfiguration(device: usbAccessory)
                let device = try VZUSBPassthroughDevice(configuration: configuration)
                self.virtualMachine.usbControllers.first?.attach(device: device) { error in
                    // Handle error if necessary...
                }
            } catch {
                // Handle error...
            }
        }
    }
}

关键点:

  • usbAccessoryDidConnect 在用户通过菜单栏授权设备后触发
  • 所有 VM 硬件修改必须在 virtualMachine.queue 上执行,这是线程安全要求
  • VZUSBPassthroughDeviceConfigurationAAUSBAccessory 包装成 VM 可识别的配置
  • VZUSBPassthroughDevice 是实际的直通设备对象
  • attach(device:) 把设备动态挂载到 VM 的 USB 控制器上
  • 用户随时可以通过菜单栏断开设备,App 需要实现 usbAccessoryDidDisconnect 做清理

创建自定义 vmnet 网络

10:04

import Virtualization
import vmnet

var status: vmnet_return_t = .VMNET_FAILURE
guard let networkConfiguration =
    vmnet_network_configuration_create(.VMNET_SHARED_MODE, &status) else { ... }

guard let network =
    vmnet_network_create(networkConfiguration, &status) else { ... }

let attachment = VZVmnetNetworkDeviceAttachment(network: network)

let networkDeviceConfiguration = VZVirtioNetworkDeviceConfiguration()
networkDeviceConfiguration.attachment = attachment

virtualMachineConfiguration.networkDevices = [networkDeviceConfiguration]

let virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)

关键点:

  • vmnet_network_configuration_create 创建网络配置对象,.VMNET_SHARED_MODE 是共享模式
  • 可以通过 vmnet API 进一步配置 DHCP 参数、端口转发规则等
  • vmnet_network_create 从配置创建网络对象
  • VZVmnetNetworkDeviceAttachment 桥接 vmnet 网络和 Virtualization 框架
  • 多个 VM 使用同一个 network 对象就能互相通信
  • vmnet 网络是引用计数的 Objective-C 对象,App 退出后网络消失,需要自行持久化配置
  • 通过 vmnet_network_copy_serializationvmnet_network_create_with_serialization 可以跨 XPC 传递网络对象

DiskImageKit 分层镜像

14:54

import DiskImageKit
import Virtualization

let baseImage = try DiskImage(opening: .open(url: baseLayerURL, mode: .readOnly))
let cacheImage = try baseImage.appending(.asifLayer(url: cacheLayerURL, type: .cache))
let overlayImage = try DiskImage(opening: .open(url: overlayLayerURL))
let stackedImage = try cacheImage.appending(overlayImage)

let storageDeviceAttachment = try VZDiskImageStorageDeviceAttachment(diskImage: stackedImage)

let storageDeviceConfiguration =
    VZVirtioBlockDeviceConfiguration(attachment: storageDeviceAttachment)

virtualMachineConfiguration.storageDevices = [storageDeviceConfiguration]

let virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)

关键点:

  • DiskImage(opening: .open(url:mode:)) 打开基础镜像,.readOnly 确保 base layer 不会被修改
  • .appending(.asifLayer(url:type:)) 追加 ASIF 格式的层,.cache 类型用于性能缓存
  • 也可以追加另一个 DiskImage 对象作为 overlay layer
  • 读取时 DiskImageKit 从上到下遍历层栈,找到第一个包含目标块的层
  • 如果有 cache 层,读取底层数据时会自动缓存到 cache 层
  • 遇到 overlay 层时,写入操作会存储在 overlay 中,实现写时复制
  • VZDiskImageStorageDeviceAttachment 把 DiskImageKit 镜像包装成 VM 存储设备附件
  • 层栈越浅性能越好,建议保持最少层数
  • 多个 VM 共享 base layer 时,需要各自复制辅助存储文件(auxiliary storage)和 EFI 变量存储文件

配置自定义 Virtio 设备

17:41

import Virtualization

let deviceConfiguration = VZCustomVirtioDeviceConfiguration()

deviceConfiguration.deviceID = 4
deviceConfiguration.pciClassID = 0x10
deviceConfiguration.pciSubclassID = 0x00
deviceConfiguration.virtioQueueCount = 1

deviceConfiguration.provider =
    VZCustomVirtioDeviceDelegateProvider(deviceQueue: deviceQueue, delegate: provider)

virtualMachineConfiguration.customVirtioDevices = [deviceConfiguration]

let virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)

关键点:

  • deviceID = 4 对应 Virtio entropy 设备,自定义设备需要选择未被占用的 ID
  • pciClassID = 0x10 是 PCI crypto 设备类别
  • pciSubclassID = 0x00 是网络与计算加密控制器子类
  • virtioQueueCount 设置 Virtio queue 数量,根据设备需求配置
  • VZCustomVirtioDeviceDelegateProvider 把 delegate 和 queue 绑定到设备配置
  • 自定义设备需要客户机侧有对应的 Virtio 驱动才能工作

Virtio 设备 Delegate

18:20

import Virtualization

class DeviceConfigurationDelegate: NSObject, VZCustomVirtioDeviceConfigurationDelegate {
    func customVirtioConfiguration(_ deviceConfiguration: VZCustomVirtioDeviceConfiguration,
                                   didCreateDevice device: VZCustomVirtioDevice) {
        device.delegate = deviceDelegate
        self.device = device
    }
}

关键点:

  • VZCustomVirtioDeviceConfigurationDelegate 在 VM 启动、设备创建完成后被调用
  • 在这里设置设备的 delegate,处理后续的 Virtio queue 事件
  • 保留 device 引用,以便后续主动触发客户机中断

处理 Virtio Queue 元素

18:42

import Virtualization

class DeviceDelegate: NSObject, VZCustomVirtioDeviceDelegate {
    func customVirtioDevice(_ device: VZCustomVirtioDevice,
                            didReceiveNotificationFor queue: VZVirtioQueue) {
        while let element = queue.nextElement() {
            // Process element...
            element.returnToQueue()
        }
    }
}

关键点:

  • didReceiveNotificationFor 在客户机驱动向 queue 写入数据时触发
  • queue.nextElement() 取出 queue 中的元素,返回 nil 时 queue 为空
  • 处理完元素后必须调用 element.returnToQueue() 归还到 queue
  • 自定义设备需要客户机侧编写对应的 Virtio 驱动
  • 设计驱动时应遵循 Virtio 最佳实践,充分利用 queue 的批处理能力

核心启发

1. CI/CD 无人值守 macOS 测试集群

VZMacGuestProvisioningOptions 实现 VM 的 zero-touch 初始化。结合 DiskImageKit 的分层镜像,一个 base 镜像供所有测试节点共享,每个节点只需几 MB 的 overlay。测试结束后删除 overlay 即可恢复干净状态。

入口:VZMacGuestProvisioningOptions + VZDiskImageStorageDeviceAttachment

2. 带 USB 设备直通的开发环境

做嵌入式开发或硬件测试时,经常需要在虚拟机里用 USB 调试器、加密狗、固件烧录器。以前只能装双系统或买物理机。现在用 Accessory Access 框架,用户手动授权后设备直通到 VM,热插拔稳定可靠。

入口:AAUSBAccessoryManager.shared.registerListener + VZUSBPassthroughDevice

3. 微服务本地联调网络

用 vmnet 框架创建自定义网络拓扑,把宿主机的特定端口转发到 VM 内部。比如宿主机 8080 映射到 VM 的 80,本地浏览器直接访问宿主机的 localhost:8080 就能调试 VM 里的 Web 服务。

入口:vmnet_network_configuration_create + DHCP 配置 + 端口转发规则

4. 基于分层镜像的”快照即恢复”系统

用 DiskImageKit 的 overlay layer 做快照。每次测试前基于 clean base 创建新 overlay,测试后可以选择保留(快照)或丢弃(恢复)。overlay 是稀疏文件,创建成本极低。

入口:DiskImage + .appending(.asifLayer) + overlay layer 管理

5. Linux VM 里的自定义硬件加速

通过 VZCustomVirtioDevice 把宿主机的专用硬件(如 NPU、FPGA、自定义协处理器)暴露给 Linux 客户机。编写对应的 Linux Virtio 驱动,实现 Host-Guest 低延迟通信。

入口:VZCustomVirtioDeviceConfiguration + VZCustomVirtioDeviceDelegate

关联 Session

评论

GitHub Issues · utterances