返回文章列表

开发实践 v1.0 - 引导启动 Ubuntu

linuxvirtualization

概述

本文档基于 Apple Virtualization 框架的实践经验,涵盖在 macOS 上创建和管理 Linux 虚拟机的完整技术方案。内容包括两种主流引导方式的对比、核心 API 使用、设备配置、常见问题排查及最佳实践。


1. 框架概述

Apple Virtualization 框架 (Virtualization.framework) 是 macOS 原生的虚拟化 API,支持在 Apple Silicon 和 Intel Mac 上运行 Linux 虚拟机。

1.1 核心能力

能力描述
CPU/内存配置灵活分配虚拟 CPU 核心数和内存大小
存储设备支持 virtio 块设备、USB 存储
网络NAT 网络、桥接网络
图形显示VirtIO GPU,支持 GUI 桌面
输入设备USB 键盘、鼠标/触控板
串口控制台用于无 GUI 环境的命令行交互

1.2 必要权限

Entitlements 文件中添加:

xml
<key>com.apple.security.virtualization</key>
<true/>

2. 引导方式详解

2.1 方式对比

特性EFI 引导 (VZEFIBootLoader)直接内核引导 (VZLinuxBootLoader)
入口UEFI 固件 → 引导加载器 → 内核直接加载 Linux 内核
典型用途完整发行版安装、桌面系统云镜像、开发测试、CI/CD
ISO 安装✅ 支持❌ 不支持
启动速度较慢(完整引导链)快速
状态持久化需要 EFI 变量存储 + MachineIdentifier不需要
用户门槛低(标准安装流程)高(需提供内核/initrd/cmdline)

2.2 EFI 引导实现

适用于需要从 ISO 安装完整 Linux 发行版的场景。

swift
// 1. 创建平台配置
let platform = VZGenericPlatformConfiguration()
platform.machineIdentifier = loadOrCreateMachineIdentifier()

// 2. 创建 EFI 引导器
let bootLoader = VZEFIBootLoader()
bootLoader.variableStore = loadOrCreateEFIVariableStore()

// 3. 应用到配置
config.platform = platform
config.bootLoader = bootLoader

// 4. 挂载 ISO 作为 USB 存储(首次安装)
if let isoURL = installISOURL {
    let isoAttachment = try VZDiskImageStorageDeviceAttachment(
        url: isoURL, 
        readOnly: true
    )
    let usbStorage = VZUSBMassStorageDeviceConfiguration(attachment: isoAttachment)
    config.storageDevices.append(usbStorage)
}

关键持久化文件:

  • MachineIdentifier.bin - 机器标识,确保 VM 身份一致
  • NVRAM.bin - EFI 变量存储,保存引导配置

2.3 直接内核引导实现

适用于云镜像或需要快速迭代的开发场景。

swift
// 1. 创建 Linux 引导器
let bootLoader = VZLinuxBootLoader(kernelURL: kernelURL)

// 2. 配置 initrd(可选)
bootLoader.initialRamdiskURL = initrdURL

// 3. 设置内核命令行
bootLoader.commandLine = "console=hvc0 root=/dev/vda1 rw"

// 4. 应用到配置
config.bootLoader = bootLoader

内核命令行常用参数:

参数说明
console=hvc0输出到 virtio 控制台
root=/dev/vda1指定根文件系统分区
rw以读写模式挂载根文件系统
cloud-init=disabled禁用 cloud-init(避免网络等待)
init=/bin/bash进入单用户模式(调试用)

3. 核心 API 参考

3.1 配置类层次

VZVirtualMachineConfiguration
├── platform: VZPlatformConfiguration
   └── VZGenericPlatformConfiguration
├── bootLoader: VZBootLoader
   ├── VZEFIBootLoader
   └── VZLinuxBootLoader
├── storageDevices: [VZStorageDeviceConfiguration]
├── networkDevices: [VZNetworkDeviceConfiguration]
├── graphicsDevices: [VZGraphicsDeviceConfiguration]
├── keyboards: [VZKeyboardConfiguration]
├── pointingDevices: [VZPointingDeviceConfiguration]
└── serialPorts: [VZSerialPortConfiguration]

3.2 虚拟机生命周期

swift
class VMInstance: NSObject, VZVirtualMachineDelegate {
    private var virtualMachine: VZVirtualMachine?
    
    enum State {
        case stopped, starting, running, paused, error(String)
    }
    
    // 启动
    func start() async throws {
        let config = try createConfiguration()
        try config.validate()
        
        virtualMachine = VZVirtualMachine(configuration: config)
        virtualMachine?.delegate = self
        
        try await virtualMachine?.start()
    }
    
    // 暂停
    func pause() async throws {
        try await virtualMachine?.pause()
    }
    
    // 恢复
    func resume() async throws {
        try await virtualMachine?.resume()
    }
    
    // 停止
    func stop() async throws {
        try await virtualMachine?.stop()
    }
    
    // 代理方法
    func virtualMachine(_ vm: VZVirtualMachine, 
                        didStopWithError error: Error?) {
        // 处理停止或错误
    }
    
    func guestDidStop(_ virtualMachine: VZVirtualMachine) {
        // 客户机主动关机
    }
}

3.3 显示视图集成 (SwiftUI)

swift
import SwiftUI
import Virtualization

struct VMDisplayView: NSViewRepresentable {
    let virtualMachine: VZVirtualMachine
    
    func makeNSView(context: Context) -> VZVirtualMachineView {
        let view = VZVirtualMachineView()
        view.virtualMachine = virtualMachine
        view.capturesSystemKeys = true
        return view
    }
    
    func updateNSView(_ nsView: VZVirtualMachineView, context: Context) {
        nsView.virtualMachine = virtualMachine
    }
}

4. 项目架构设计

4.1 单 VM 演示架构(Apple 示例风格)

┌─────────────────────────────────────────────┐
              AppDelegate                     
  ┌─────────────────────────────────────┐    
    VZVirtualMachineConfiguration          
    - EFI Boot + ISO Install               
    - Graphics/Audio/Network/SPICE         
  └─────────────────────────────────────┘    
                                             
                                             
  ┌─────────────────────────────────────┐    
         VZVirtualMachine                  
  └─────────────────────────────────────┘    
                                             
                                             
  ┌─────────────────────────────────────┐    
       VZVirtualMachineView (主窗口)        
  └─────────────────────────────────────┘    
└─────────────────────────────────────────────┘

特点:
- 应用启动即创建/启动 VM
- VM 停止时退出进程
- 适合演示和单一用途

4.2 多 VM 管理架构

┌──────────────────────────────────────────────────────────┐
                     VMManager                             
  ┌────────────────────────────────────────────────────┐  
    instances: [VMInstance]                             
    - 多实例列表管理                                     
    - 配置持久化 (vms.json)                             
    - CRUD 操作                                         
  └────────────────────────────────────────────────────┘  
└──────────────────────────────────────────────────────────┘
                           
           ┌───────────────┼───────────────┐
                                         
    ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
     VMInstance    VMInstance    VMInstance  
     - 状态机      - 状态机      - 状态机    
     - 生命周期    - 生命周期    - 生命周期  
    └─────────────┘ └─────────────┘ └─────────────┘
           
           
    ┌─────────────────────────────────────────────┐
                  UI Layer                        
      ┌──────────┐ ┌──────────┐ ┌──────────────┐ 
       VMList     Detail    │Display/Console│ 
       View       View         Windows     
      └──────────┘ └──────────┘ └──────────────┘ 
    └─────────────────────────────────────────────┘

特点:
- 支持多 VM 同时管理
- 显式状态控制 (start/stop/pause/resume)
- 独立的显示和控制台窗口
- 配置持久化到 Application Support

4.3 推荐目录结构

LinuxVMManager/
├── Models/
   ├── VMConfiguration.swift    # VM 配置模型
   ├── VMInstance.swift         # 单个 VM 实例管理
   └── VMManager.swift          #  VM 管理器
├── Views/
   ├── VMListView.swift         # VM 列表
   ├── VMDetailView.swift       # VM 详情与控制
   ├── VMDisplayView.swift      # 图形显示窗口
   └── ConsoleView.swift        # 串口控制台窗口
├── Utilities/
   ├── DiskImageHelper.swift    # 磁盘镜像处理
   └── CloudInitHelper.swift    # Cloud-init ISO 生成
└── Resources/
    └── LinuxVMManager.entitlements

5. 设备配置清单

5.1 存储设备

swift
// 主磁盘(virtio 块设备)
let diskAttachment = try VZDiskImageStorageDeviceAttachment(
    url: diskURL,
    readOnly: false
)
let disk = VZVirtioBlockDeviceConfiguration(attachment: diskAttachment)
config.storageDevices = [disk]

// USB 存储(用于 ISO 安装)
let usbAttachment = try VZDiskImageStorageDeviceAttachment(
    url: isoURL,
    readOnly: true
)
let usb = VZUSBMassStorageDeviceConfiguration(attachment: usbAttachment)
config.storageDevices.append(usb)

5.2 网络设备

swift
// NAT 网络(最简单,VM 可访问外网)
let network = VZVirtioNetworkDeviceConfiguration()
network.attachment = VZNATNetworkDeviceAttachment()
config.networkDevices = [network]

NAT 网络特点:

  • VM 可访问外部网络
  • 外部无法主动连接 VM
  • DHCP 由 macOS 提供

5.3 图形设备

swift
let graphics = VZVirtioGraphicsDeviceConfiguration()
graphics.scanouts = [
    VZVirtioGraphicsScanoutConfiguration(
        widthInPixels: 1280,
        heightInPixels: 800
    )
]
config.graphicsDevices = [graphics]

5.4 输入设备

swift
// USB 键盘
config.keyboards = [VZUSBKeyboardConfiguration()]

// USB 屏幕坐标指针(推荐用于 GUI)
config.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]

5.5 串口控制台

swift
let consolePort = VZVirtioConsoleDeviceSerialPortConfiguration()

let inputPipe = Pipe()
let outputPipe = Pipe()

consolePort.attachment = VZFileHandleSerialPortAttachment(
    fileHandleForReading: inputPipe.fileHandleForReading,
    fileHandleForWriting: outputPipe.fileHandleForWriting
)

config.serialPorts = [consolePort]

// 读取输出
outputPipe.fileHandleForReading.readabilityHandler = { handle in
    let data = handle.availableData
    if let text = String(data: data, encoding: .utf8) {
        print(stripAnsiCodes(text))
    }
}

// 发送输入
func sendCommand(_ command: String) {
    if let data = (command + "\n").data(using: .utf8) {
        inputPipe.fileHandleForWriting.write(data)
    }
}

5.6 其他可选设备

swift
// 音频(输入+输出)
let audioInput = VZVirtioSoundDeviceConfiguration()
audioInput.streams = [VZVirtioSoundDeviceInputStreamConfiguration()]

let audioOutput = VZVirtioSoundDeviceConfiguration()
audioOutput.streams = [VZVirtioSoundDeviceOutputStreamConfiguration()]

config.audioDevices = [audioInput, audioOutput]

// 内存气球(动态内存管理)
config.memoryBalloonDevices = [VZVirtioTraditionalMemoryBalloonDeviceConfiguration()]

// 熵设备(提供随机数)
config.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]

// SPICE Agent(剪贴板共享等)
let spiceAgent = VZVirtioConsoleDeviceConfiguration()
let spicePort = VZVirtioConsolePortConfiguration()
spicePort.name = VZSpiceAgentPortAttachment.spiceAgentPortName
spicePort.attachment = VZSpiceAgentPortAttachment()
spiceAgent.ports = [spicePort]
config.consoleDevices.append(spiceAgent)

6. 常见问题与解决方案

6.1 VM 启动失败:Invalid kernel format

原因: vmlinuz 文件是 gzip 压缩的,需要解压。

解决方案:

bash
# 检查文件类型
file vmlinuz
# 输出: vmlinuz: gzip compressed data...

# 解压
gzip -dc vmlinuz > vmlinux

6.2 VM 启动失败:Invalid disk format

原因: Virtualization 框架只支持 raw 格式磁盘,不支持 QCOW2。

解决方案:

bash
# 转换 QCOW2  raw
qemu-img convert -O raw ubuntu.qcow2 ubuntu.raw

# 可选:扩展磁盘大小
qemu-img resize -f raw ubuntu.raw 20G

6.3 启动卡在 "Waiting for network"

原因: Cloud-init 等待网络配置超时。

解决方案: 在内核命令行添加 cloud-init=disabled

swift
bootLoader.commandLine = "console=hvc0 root=/dev/vda1 rw cloud-init=disabled"

6.4 文件系统只读

可能原因:

  1. 内核命令行缺少 rw 参数
  2. 磁盘镜像文件系统损坏(非正常关机导致)
  3. 磁盘空间不足

解决方案:

方案 A - 确保命令行包含 rw

swift
bootLoader.commandLine = "console=hvc0 root=/dev/vda1 rw"

方案 B - 进入单用户模式修复:

swift
// 临时修改命令行
bootLoader.commandLine = "console=hvc0 root=/dev/vda1 rw init=/bin/bash"

启动后执行:

bash
mount -o remount,rw /
# 执行修复操作
sync

方案 C - 重新下载干净的镜像:

bash
curl -L -o ubuntu.qcow2 "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-arm64.img"
qemu-img convert -O raw ubuntu.qcow2 ubuntu.raw
qemu-img resize -f raw ubuntu.raw 20G

6.5 登录失败 (ubuntu/ubuntu 不工作)

原因: Ubuntu Cloud Image 默认禁用密码登录,需要 cloud-init 配置。

解决方案 A - 创建 Cloud-init seed.iso:

  1. 创建 user-data 文件:
yaml
#cloud-config
users:
  - name: ubuntu
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    lock_passwd: false
    plain_text_passwd: ubuntu
  1. 创建 meta-data 文件:
yaml
instance-id: linux-vm-001
local-hostname: linux-vm
  1. 生成 ISO:
bash
hdiutil makehybrid -o seed.iso -hfs -joliet -iso -default-volume-name cidata seed/
  1. 在配置中挂载 seed.iso

解决方案 B - 单用户模式手动设置密码:

bash
# 使用 init=/bin/bash 启动后
mount -o remount,rw /
passwd ubuntu
# 输入新密码
sync
# 重启,恢复正常命令行

6.6 控制台输出乱码(ANSI 转义序列)

解决方案: 添加过滤函数

swift
func stripAnsiCodes(_ text: String) -> String {
    let pattern = "\\x1B\\[[0-9;]<em>[A-Za-z]|\\x1B\\][^\\x07]</em>\\x07|\\x1B[()][AB012]"
    guard let regex = try? NSRegularExpression(pattern: pattern) else {
        return text
    }
    let range = NSRange(text.startIndex..., in: text)
    return regex.stringByReplacingMatches(in: text, range: range, withTemplate: "")
}

6.7 显示窗口文字太小

解决方案: 调整虚拟显示器分辨率

swift
// 使用较低分辨率
graphics.scanouts = [
    VZVirtioGraphicsScanoutConfiguration(
        widthInPixels: 800,
        heightInPixels: 600
    )
]

7. 最佳实践

7.1 选择合适的引导方式

场景推荐方式
安装完整 Linux 桌面发行版EFI 引导 + ISO
云镜像快速启动直接内核引导
CI/CD 测试环境直接内核引导
长期使用的开发环境EFI 引导

7.2 正确关闭 VM

swift
// 优先使用正常关机(保护文件系统)
func gracefulShutdown() async {
    // 发送关机命令到 VM
    sendCommand("sudo shutdown -h now")
    
    // 等待 VM 自行停止
    // 或设置超时后强制停止
}

// 强制停止前同步数据
func forceStop() async throws {
    sendCommand("sync")
    try await Task.sleep(nanoseconds: 1_000_000_000) // 等待 1 秒
    try await virtualMachine?.stop()
}

7.3 磁盘镜像管理

bash
# 创建基础镜像后,使用写时复制创建实例
# macOS 使用 APFS clone
cp -c base.raw instance1.raw

# 定期检查磁盘空间
df -h  #  VM 内执行

7.4 配置持久化

swift
struct VMConfiguration: Codable {
    var id: UUID
    var name: String
    var cpuCount: Int
    var memorySize: UInt64
    var diskPath: String
    var kernelPath: String?
    var initrdPath: String?
    var kernelCommandLine: String
    var useEFIBoot: Bool
}

// 保存到 Application Support
func saveConfigurations(_ configs: [VMConfiguration]) throws {
    let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
        .appendingPathComponent("LinuxVMManager/vms.json")
    let data = try JSONEncoder().encode(configs)
    try data.write(to: url)
}

8. 附录:引导方式全景

8.1 虚拟化环境常见引导方式

引导方式描述典型场景
EFI/UEFI现代标准固件引导桌面系统、服务器
直接内核引导Hypervisor 直接加载内核云平台、轻量 VM
Legacy BIOS传统 PC 兼容模式旧系统兼容
UEFI + Secure Boot带签名校验的安全启动企业环境
PXE/iPXE网络引导数据中心批量部署
从 ISO 引导光驱/USB 介质启动系统安装

8.2 容器环境引导说明

平台引导方式说明
Docker (容器)无引导共享宿主内核的进程隔离
Docker Desktop (macOS/Windows)直接内核引导LinuxKit 轻量 VM
WSL2 (Windows)直接内核引导微软定制 Linux 内核
OrbStack (macOS)直接内核引导优化的轻量 Linux VM

关键结论: 容器本身无需引导;容器运行时底层的 Linux VM 通常采用直接内核引导以优化启动速度。


参考资源


文档版本: 1.0 | 最后更新: 2026-02-04

相关文章