Skip to content

macOS: 合盖后虚拟机继续运行导致发热 #5124

Description

@DoChaoing

macOS: 合盖后虚拟机继续运行导致发热

问题描述

Summary: MacBook 合盖(关闭盖子)后,Multipass 虚拟机继续运行,导致设备发热和电池快速耗尽。

Severity: High - 影响用户体验和设备安全

Platform: macOS (affecting both Intel and Apple Silicon)

Affected Versions: All versions up to and including 1.16.3

复现步骤

  1. 在 MacBook 上安装 Multipass 1.16.3
  2. 启动一个或多个实例:multipass launch
  3. 合上 MacBook 盖子(clamshell mode)
  4. 等待几分钟到几小时
  5. 观察设备底部温度、风扇噪音、电池电量消耗

预期行为

合盖后,Multipass 应该自动挂起所有运行中的虚拟机,以节省电量并避免发热。

实际行为

虚拟机继续全速运行,导致:

  • 设备持续发热,底部温度可达 40-50°C
  • 风扇高速运转(如有)
  • 电池在睡眠状态下快速耗尽
  • 用户携带设备时感觉明显发热

根因分析

经过代码排查,发现以下问题:

1. 守护进程未订阅 macOS 系统睡眠通知

src/daemon/daemon_main.cpp 中的 UnixSignalHandler 仅处理 SIGTERMSIGINTSIGUSR1 三种 Unix 信号,未订阅 NSWorkspaceWillSleepNotificationIORegisterForSystemPower 等系统睡眠事件。

相关代码:

// daemon_main.cpp:47-77
class UnixSignalHandler
{
    void monitor_signals(sigset_t sigset)
    {
        int sig = -1;
        sigwait(&sigset, &sig);
        // 仅处理 SIGTERM/SIGINT/SIGUSR1
        if (sig != SIGUSR1)
            mpl::info("daemon", "Received signal {} ({})", sig, strsignal(sig));
        app_ready_signal.wait();
        QCoreApplication::quit();
    }
};

2. QEMU/AppleVZ 虚拟 CPU 线程不受睡眠约束

src/platform/backends/qemu/macos/qemu_platform_macos.cpp 使用 -accel hvf(Hypervisor Framework)启动虚拟机。HVF 创建的虚拟 CPU 线程在宿主机上以实时调度优先级运行,不受 macOS clamshell sleep 约束

相关代码:

// qemu_platform_macos.cpp:83
qemu_args << "-accel" << "hvf";

3. 无自动挂起机制

src/daemon/daemon.h 中没有"系统睡眠时自动挂起实例"的机制,用户必须手动执行 multipass suspendmultipass stop

建议的解决方案

核心功能

实现 "合盖自动挂起实例" 功能,包括:

  1. 新增 SleepWakeHandler 组件

    • 订阅 NSWorkspaceWillSleepNotificationNSWorkspaceDidWakeNotification
    • 收到睡眠通知时调用 Daemon::suspend_all_instances()
    • 收到唤醒通知时调用 Daemon::resume_suspended_instances()(可配置)
  2. 新增两个设置项

    • local.auto-suspend-on-sleep(bool,默认 true):控制是否在系统睡眠时自动挂起实例
    • local.auto-resume-on-wake(bool,默认 false):控制是否在系统唤醒时自动恢复实例
  3. 扩展 Daemon 类

    • 新增 suspend_all_instances() 方法:挂起所有 running 状态的实例
    • 新增 resume_suspended_instances() 方法:恢复之前自动挂起的实例
    • 记录自动挂起的实例名称,避免唤醒后重复启动用户手动挂起的实例

文件变更

文件 变更类型
src/platform/backends/macos/sleep_wake_handler.h 新建
src/platform/backends/macos/sleep_wake_handler.mm 新建
include/multipass/constants.h 修改(新增设置常量)
src/daemon/daemon.h 修改(新增方法声明)
src/daemon/daemon.cpp 修改(实现新方法)
src/daemon/daemon_main.cpp 修改(初始化 handler)
src/daemon/daemon_init_settings.cpp 修改(注册设置项)
src/platform/backends/macos/CMakeLists.txt 修改(添加源文件)

用户接口

# 启用/禁用自动挂起
multipass set local.auto-suspend-on-sleep=true  # 默认值

# 启用/禁用自动恢复
multipass set local.auto-resume-on-wake=false   # 默认值

替代方案

用户端临时缓解措施

在官方实现前,用户可以使用以下方法缓解:

  1. 手动挂起: 合盖前执行 multipass suspend --all
  2. 使用 sleepwatcher: 安装 sleepwatcher 并配置脚本:
    # ~/.sleep
    /usr/local/bin/multipass suspend --all
    # ~/.wakeup
    /usr/local/bin/multipass start --all
  3. 使用 launchd: 创建 LaunchAgent 监听系统睡眠事件并执行脚本

macOS caffeinate(不推荐)

使用 caffeinate 可以防止系统睡眠,但这会加剧发热问题,不适用于此场景。

详细设计文档

完整的架构设计和实施计划请参考:

相关 Issue

  • 暂未找到相关 Issue

测试计划

单元测试

  • SleepWakeHandler 的回调触发
  • Daemon::suspend_all_instances() 的正确性
  • 设置项读取/写入

集成测试

  • 合盖后验证实例状态变为 suspended
  • 开盖后验证实例状态恢复为 running(如果启用)
  • 日志中确认收到睡眠/唤醒通知

手动测试

  • 在真实 MacBook 上合盖,观察发热情况
  • 使用 pmset -g log 查看系统睡眠事件
  • 使用 multipass info 确认实例状态

环境信息

Multipass version: 1.16.3
macOS version: macOS Sequoia 15.x / macOS Sonoma 14.x
Architecture: Apple Silicon (M1/M2/M3/M4) and Intel
Driver: QEMU (default) / AppleVZ

Labels: bug, macOS, enhancement, good first issue, help wanted

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions