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 会话
自动化脚本的工作流程大概是这样:
- 主用户登录
- 通过 VNC 依次连接到每个子用户
- 选择 “Log in as yourself” 激活子用户 GUI 会话
- 关闭 VNC 窗口,处理下一个用户
听起来很简单对吧?但问题就出在这里。
二、问题是怎么发现的
2.1 两台机器,两种结果
我们有两台配置相似的 Mac mini,运行相同的自动化脚本:
| 机器 | HDMI Dummy | 自动化脚本结果 |
|---|---|---|
| Machine-A | 无 | [√] 全部成功,5 个用户约 2 分钟完成 |
| Machine-B | 有(4K HDMI Dummy) | [X] 第 4 个用户开始卡住 |
2.2 Machine-B 具体怎么挂的
- 前 3 个用户:正常激活
- 第 4 个用户:选择 “Log in as yourself” 后,VNC 窗口变黑
- 脚本卡住:AppleScript 无法操作黑屏的窗口
- 需要人工干预:手动点击鼠标才能恢复
错误日志里出现了这个:
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-A | Machine-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,在单用户场景下确实有帮助,但在多用户场景下却成了问题的根源。
几个核心要点:
-
HDMI Dummy 在多用户场景可能有害:带 EDID 的 HDMI Dummy 会被识别为真实显示器,物理 Framebuffer 在用户切换时会被抢占,导致原用户的 VNC 标准模式黑屏。
-
无头 Mac 不一定需要 HDMI Dummy:macOS 在无显示器时会使用软件虚拟 Framebuffer,虚拟 Framebuffer 不会被单一用户独占,VNC 可以正常工作。
-
配置防休眠很重要:移除 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 &