macOS VNC 黑屏排查记:HDMI Dummy 在多用户场景下的坑

macOS VNC 黑屏排查记:HDMI Dummy 在多用户场景下的坑

前言

管理无头 Mac 服务器时,有一条经常被提及的建议:

“用 HDMI Dummy 可以获得更好的远程桌面体验,避免分辨率问题。”

这个建议在单用户场景下确实是对的。但我们最近发现,在多用户场景下,HDMI Dummy 反而会成为 VNC 黑屏的罪魁祸首。

一、背景介绍

1.1 什么是 HDMI Dummy?

HDMI Dummy(也叫 Headless Ghost、虚拟显示器适配器)是一种插入 HDMI 端口的小型设备,用途是:

  • 模拟物理显示器连接:让 macOS 认为有真实显示器
  • 提供 EDID 信息:告诉系统显示器的分辨率、刷新率等参数
  • 启用 GPU 加速:某些 GPU 功能需要检测到显示器才能工作

1.2 我们的使用场景

我们在 Mac mini 上配置了多用户环境:

  • 1 个主用户(管理员):用于系统管理
  • 多个子用户:各自运行独立服务,需要 GUI 环境
  • 自动化脚本:开机时自动激活所有子用户的 GUI 会话

自动化脚本的工作流程大概是这样:

  1. 主用户登录
  2. 通过 VNC 依次连接到每个子用户
  3. 选择 “Log in as yourself” 激活子用户 GUI 会话
  4. 关闭 VNC 窗口,处理下一个用户

听起来很简单对吧?但问题就出在这里。

二、问题是怎么发现的

2.1 两台机器,两种结果

我们有两台配置相似的 Mac mini,运行相同的自动化脚本:

机器HDMI Dummy自动化脚本结果
Machine-A[√] 全部成功,5 个用户约 2 分钟完成
Machine-B(4K HDMI Dummy)[X] 第 4 个用户开始卡住

2.2 Machine-B 具体怎么挂的

  1. 前 3 个用户:正常激活
  2. 第 4 个用户:选择 “Log in as yourself” 后,VNC 窗口变黑
  3. 脚本卡住:AppleScript 无法操作黑屏的窗口
  4. 需要人工干预:手动点击鼠标才能恢复

错误日志里出现了这个:

execution error: System Events got an error: Can't set process "Screen Sharing" to true. (-10006)

错误码 -10006 表示无法将进程设置为前台——因为显示环境已经丢失了。

三、排查过程

3.1 排除系统版本差异

# Machine-A(正常)
sw_vers
# ProductVersion: 26.2, BuildVersion: 25C56

# Machine-B(异常)
sw_vers
# ProductVersion: 26.2, BuildVersion: 25C56

版本完全相同,排除系统版本问题。

3.2 检查 HDMI 热插拔状态

这时候我们开始怀疑 HDMI Dummy 了。

Machine-A(无 HDMI Dummy)

ioreg -lw0 | grep -i "HDMI_HPD"
# "HDMI_HPD" = No
# "HDMI_HPD-Debounced" = 0

Machine-B(有 HDMI Dummy)

ioreg -lw0 | grep -i "HDMI_HPD"
# "HDMI_HPD" = Yes
# "HDMI_HPD-Debounced" = 0
# "HPD_StateDescription" = "High"
# "HPD_State" = 2

Machine-B 检测到了 HDMI 热插拔信号(HPD = Yes)。

3.3 看看检测到了什么显示器

在 Machine-B 上进一步检查:

ioreg -lw0 | grep -iE "SinkActive|MaxW|MaxH|ProductName"
# "SinkActive" = 1
# "MaxW" = 4096
# "MaxH" = 2160
# ProductName = "AOC28E850.HDR"

HDMI Dummy 被识别为一个 4K 显示器(品牌 AOC,型号 28E850)!

3.4 对比脚本日志

Machine-A(成功)的日志

[03:46:55] Processing user: sub_user_1 on port 5901
  -> Connecting to VNC...
  -> Authenticating as sub_user_1...
  -> Handling connection dialog - selecting 'Log in as yourself'...
button Connect of window 1 of application process Screen Sharing
  -> Waiting for session to activate...
  -> Session activated after 2s
  -> Closing Screen Sharing window...
button 1 of window Machine's Mac mini – Locked    # ← 正常显示 Locked 状态
  -> Done. Moving to next user.

每个用户处理约 30 秒,全程流畅。

Machine-B(失败)的日志

[23:34:52] Processing user: sub_user_4 on port 5904
  -> Connecting to VNC...
  -> Authenticating as sub_user_4...
  -> Handling connection dialog - selecting 'Log in as yourself'...
  -> Waiting for session to activate...           # ← 注意:没有 "button Connect" 输出!
  -> Closing Screen Sharing window...
button Sign In of window 1                        # ← 还停留在登录界面!
  -> Done. Moving to next user.

从 user_3 到 user_4 耗时 63 秒(应该是 30 秒),而且最终状态异常。

3.5 确认问题模式

用户Machine-AMachine-B
user_1[√] 29s[√] 29s
user_2[√] 30s[√] 29s
user_3[√] 30s[√] 31s
user_4[√] 31s[X] 63s + 异常
user_5[√] 32s[!] 依赖 user_4

问题从第 4 个用户开始出现,越往后越严重。

四、根因分析

4.1 macOS 显示器所有权模型

要理解这个问题,得先了解 macOS 是怎么管理显示器的。

物理显示器 / HDMI Dummy(被识别为物理显示器)

  • Framebuffer(显示内存区域)有一个所有者
  • 这个所有者就是当前控制台用户(Console User)
  • 同一时间只能有一个所有者

软件虚拟 Framebuffer(无物理显示器时)

  • 没有特定所有者
  • 可以被多用户共享访问

这就是问题的关键。

4.2 有 HDMI Dummy 时的问题流程

让我用时间线来描述一下:

T0:初始状态

  • HDMI Dummy (AOC28E850 4K) 被系统识别
  • Framebuffer 所有者:主用户
  • 主用户 VNC 窗口:正常显示

T1:主用户通过 VNC 连接子用户,点击 “Log in as yourself”

T2:macOS 执行 Fast User Switch

  • Framebuffer 所有者切换到子用户
  • 主用户不再是控制台用户
  • 主用户失去 Framebuffer 访问权
  • 主用户的 VNC 标准模式窗口 → 黑屏

T3:AppleScript 尝试操作 Screen Sharing 窗口

  • 主用户的 GUI 环境已无活跃显示
  • 无法将 Screen Sharing 设为前台
  • 报错:-10006

T4:脚本卡住,等待超时或人工干预

4.3 无 HDMI Dummy 时的正常流程

T0:初始状态

  • Virtual Framebuffer,无特定所有者
  • 主用户 VNC 窗口:正常显示

T1:主用户通过 VNC 连接子用户,点击 “Log in as yourself”

T2:macOS 执行 Fast User Switch

  • Virtual Framebuffer 所有者不变(因为没有特定所有者)
  • 主用户虽然不是控制台用户,但仍可访问虚拟 Framebuffer
  • VNC 窗口显示 “Locked” 状态(正常)

T3:AppleScript 成功操作 Screen Sharing 窗口

  • 窗口显示子用户的锁屏界面
  • 可以正常关闭窗口

T4:继续处理下一个用户

4.4 关键差异总结

场景Framebuffer 类型用户切换时VNC 标准模式
有 HDMI Dummy”物理”被新用户抢占[X] 黑屏
无 HDMI Dummy虚拟不被抢占[√] 正常(显示 Locked)

五、深入理解 macOS 显示架构

5.1 控制台用户 vs 后台用户

macOS 的 Fast User Switching 机制区分两类用户:

控制台用户 (Console User)

  • 拥有物理显示器的 Framebuffer
  • 接收键盘、鼠标输入
  • 同一时间只能有一个
  • 通过 stat -f '%Su' /dev/console 查看

后台用户 (Background User)

  • 拥有独立的 GUI 会话 (loginwindow 进程)
  • 可以运行图形应用程序
  • 无法直接访问物理 Framebuffer
  • 可以有多个同时存在
  • VNC 高性能模式可以访问其虚拟显示器

5.2 “Log in as yourself” 的作用

当通过 VNC 连接并选择 “Log in as yourself” 时:

情况 1:目标用户 = 当前控制台用户

  • 共享现有显示器
  • 不发生用户切换
  • VNC 正常显示

情况 2:目标用户 ≠ 当前控制台用户

  • 执行 Fast User Switch
  • 目标用户成为新的控制台用户
  • 原用户失去物理 Framebuffer 访问
  • 如果有物理显示器/HDMI Dummy → 原用户 VNC 黑屏
  • 如果无物理显示器 → 原用户 VNC 显示 “Locked” 状态

5.3 HDMI Dummy 的双刃剑效应

HDMI Dummy 特性优势潜在问题
提供 EDID 信息获得正确分辨率被识别为”真实显示器”
发送 HPD 信号稳定的显示器检测Framebuffer 被独占
模拟物理连接某些 GPU 功能可用用户切换时发生抢占

六、解决方案

6.1 移除 HDMI Dummy(推荐)

对于多用户场景,移除 HDMI Dummy 是最简单有效的解决方案。

# 1. 物理移除 HDMI Dummy

# 2. 验证 HDMI 状态
ioreg -lw0 | grep "HDMI_HPD"
# 应该显示: "HDMI_HPD" = No

# 3. 配置防休眠(见下文)

6.2 配置防休眠

移除 HDMI Dummy 后,需要配置系统防止休眠:

#!/bin/bash
# configure-headless-mac.sh

echo "=== 配置无头 Mac 服务器 ==="

# 禁用所有休眠模式
sudo pmset -a sleep 0           # 禁用系统休眠
sudo pmset -a displaysleep 0    # 禁用显示器休眠
sudo pmset -a disksleep 0       # 禁用硬盘休眠
sudo pmset -a standby 0         # 禁用待机模式
sudo pmset -a autopoweroff 0    # 禁用自动关机

# 启用网络相关功能
sudo pmset -a womp 1            # Wake on LAN
sudo pmset -a tcpkeepalive 1    # TCP 连接保活
sudo pmset -a ttyskeepawake 1   # SSH/TTY 会话保活

# 电源恢复后自动启动
sudo pmset -a autorestart 1

# 验证设置
echo ""
echo "=== 当前电源设置 ==="
pmset -g

6.3 使用 caffeinate 保活

创建一个 LaunchDaemon 来持续运行 caffeinate:

# 创建 LaunchDaemon 配置
sudo tee /Library/LaunchDaemons/com.custom.caffeinate.plist > /dev/null << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.custom.caffeinate</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/caffeinate</string>
        <string>-d</string>
        <string>-i</string>
        <string>-s</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>
EOF

# 加载服务
sudo launchctl load /Library/LaunchDaemons/com.custom.caffeinate.plist

# 验证
ps aux | grep caffeinate

caffeinate 参数说明:

  • -d: 防止显示器休眠
  • -i: 防止系统空闲休眠
  • -s: 防止系统休眠(接电源时)

6.4 使用 VNC 高性能模式

如果必须保留 HDMI Dummy,可以修改自动化脚本使用高性能 VNC 模式。高性能模式创建独立的虚拟显示器,不依赖物理 Framebuffer,不受 Fast User Switch 影响。

不过这需要修改连接参数和脚本逻辑,改动会比较大。

七、最佳实践

7.1 无头 Mac 多用户服务器推荐配置

硬件配置

  • 不使用 HDMI Dummy
  • 确保稳定的网络连接
  • 考虑使用 UPS 电源保护

系统配置

  • 禁用所有休眠模式 (pmset)
  • 启用 Wake on LAN
  • 启用 TCP Keepalive
  • 配置自动重启
  • 运行 caffeinate 守护进程

远程访问

  • 启用屏幕共享 (Screen Sharing)
  • 启用 SSH
  • 考虑使用 VNC 高性能模式

自动化脚本

  • 添加适当的延迟和重试逻辑
  • 记录详细日志便于排查
  • 处理 VNC 窗口状态变化

7.2 诊断脚本

#!/bin/bash
# diagnose-hdmi-vnc.sh

echo "=== HDMI Dummy & VNC 诊断 ==="
echo ""

# 1. 检查 macOS 版本
echo "1. 系统信息:"
sw_vers
echo ""

# 2. 检查 HDMI 状态
echo "2. HDMI 连接状态:"
hdmi_hpd=$(ioreg -lw0 | grep '"HDMI_HPD"' | head -1)
if [[ "$hdmi_hpd" == *"Yes"* ]]; then
    echo "   [!] 检测到 HDMI 连接"
    echo "   $hdmi_hpd"
    
    echo ""
    echo "   检测到的显示器信息:"
    ioreg -lw0 | grep -E "MaxW|MaxH" | head -2 | while read line; do
        echo "   $line"
    done
else
    echo "   [√] 无 HDMI 连接(推荐状态)"
fi
echo ""

# 3. 检查当前控制台用户
echo "3. 控制台用户:"
console_user=$(stat -f '%Su' /dev/console)
current_user=$(whoami)
echo "   控制台用户: $console_user"
echo "   当前用户: $current_user"
if [[ "$console_user" != "$current_user" ]]; then
    echo "   [!] 当前用户不是控制台用户"
fi
echo ""

# 4. 检查所有 GUI 会话
echo "4. 活跃的 GUI 会话:"
ps aux | grep loginwindow | grep -v grep | while read line; do
    user=$(echo "$line" | awk '{print $1}')
    echo "   - $user"
done
echo ""

# 5. 检查电源设置
echo "5. 电源管理设置:"
sleep_val=$(pmset -g | grep "^ sleep" | awk '{print $2}')
display_val=$(pmset -g | grep "^ displaysleep" | awk '{print $2}')

if [[ "$sleep_val" == "0" ]]; then
    echo "   [√] 系统休眠: 已禁用"
else
    echo "   [!] 系统休眠: ${sleep_val} 分钟"
fi

if [[ "$display_val" == "0" ]]; then
    echo "   [√] 显示器休眠: 已禁用"
else
    echo "   [!] 显示器休眠: ${display_val} 分钟"
fi
echo ""

# 6. 检查 caffeinate
echo "6. Caffeinate 状态:"
if pgrep caffeinate > /dev/null; then
    echo "   [√] caffeinate 正在运行"
else
    echo "   [!] caffeinate 未运行"
fi
echo ""

echo "=== 诊断完成 ==="

八、小结

这次排查让我对 macOS 的显示架构有了更深的理解。一个看似无害的 HDMI Dummy,在单用户场景下确实有帮助,但在多用户场景下却成了问题的根源。

几个核心要点:

  1. HDMI Dummy 在多用户场景可能有害:带 EDID 的 HDMI Dummy 会被识别为真实显示器,物理 Framebuffer 在用户切换时会被抢占,导致原用户的 VNC 标准模式黑屏。

  2. 无头 Mac 不一定需要 HDMI Dummy:macOS 在无显示器时会使用软件虚拟 Framebuffer,虚拟 Framebuffer 不会被单一用户独占,VNC 可以正常工作。

  3. 配置防休眠很重要:移除 HDMI Dummy 后需要配置 pmset,使用 caffeinate 确保系统不休眠,启用 Wake on LAN 以便远程唤醒。

“有时候,添加的东西反而会成为障碍。”

关键命令速查:

# 检查 HDMI 状态
ioreg -lw0 | grep "HDMI_HPD"

# 检查控制台用户
stat -f '%Su' /dev/console

# 查看所有 GUI 会话
ps aux | grep loginwindow

# 配置防休眠
sudo pmset -a sleep 0 displaysleep 0 disksleep 0

# 检查电源设置
pmset -g

# 启动 caffeinate
caffeinate -d -i -s &
#macOS#技术教程#VNC#远程桌面#踩坑记录