Linux主机物理屏与虚拟屏切换指南

书接上文,虽然我们已经实现了Linux主机的虚拟屏设置,可以很好的使用该“无头”服务器,但是如果我们希望给他外接物理显示屏则必须删除虚拟驱动设置,然后再重启。主包觉得这样太过麻烦了,是不是可以让系统自动检测当前是否有显示屏接入,如果接入显示屏那我们采用真实显卡设置,否则采用DUMMY虚拟驱动?

如何启动虚拟屏

参见“无头”Linux主机远程桌面使用指南中“使用X11桌面协议”与“配置X.org服务器”

启动虚拟屏进阶

对于上篇博文中提到的开机服务,可以设置为用户层面的服务,此时无需指定DISPLAY,这意味着鲁棒性更强

vim ~/.config/systemd/user/x11vnc.service

[Unit]
Description=x11vnc server for the current graphical session
After=graphical-session.target

[Service]
ExecStart=/usr/bin/x11vnc \
-remap /home/chi/.x11vnc_remap \
-xkb \
-forever \
-noxdamage \
-passwd 200510060050 \
-rfbport 5901 \
-o /home/chi/.x11vnc.log

Restart=on-failure
RestartSec=5s

[Install]
WantedBy=graphical-session.target
sudo systemctl --user daemon-reload
sudo systemctl --user enable x11vnc.service

此时

  • 使用物理屏时,同时可以使用显示屏或被VNC转发
  • 使用虚拟屏时,只被VNC转发

如何切换物理屏与虚拟屏

我们通过X.org服务器建构屏幕,物理屏与虚拟屏也只是在驱动方面有所不同。物理屏采用真实的显卡作为驱动例如/usr/share/X11/xorg.conf.d/*-amdgpu.conf;而在上一篇博客中我们采用DUMMY虚拟驱动构建虚拟屏幕即/usr/share/X11/xorg.conf.d/*-dummy.conf

X.org服务器读入配置规则如下

  • 首先读取/usr/share/X11/xorg.conf.d/下所有*-*.conf,并按照字典序依次构建整体的配置(这意味对于同名Section,后加载的配置会覆盖先加载的配置)
  • 然后读取/etc/X11/xorg.conf.d/下所有的*-*.conf,规则如上

为了规范配置的存放方式,我们不再修改/usr/share/X11文件夹,而是修改/etc/X11文件夹

首先我们将物理屏设置与虚拟屏设置单独存在在同一个空闲文件夹下例如xorg.conf.d.available

sudo mkdir -p /etc/X11/xorg.conf.d.available
sudo cp /usr/share/X11/xorg.conf.d/*-amdgpu.conf /etc/X11/xorg.conf.d.available/amdgpu.conf
sudo mv /usr/share/X11/xorg.conf.d/*-dummy.conf /etc/X11/xorg.conf.d.available/dummy.conf

接下来我们只要依据是否外接显示屏某个配置链接置xorg.conf.d
AVAILABLE_DIR="/etc/X11/xorg.conf.d.available"
ACTIVE_CONF_LINK="/etc/X11/xorg.conf.d/20-active-gpu.conf"
desired_config=$([[ "$monitor_connected" == "true" ]] && echo "amdgpu.conf" || echo "dummy.conf")

current_config_target="none"
if [ -L "$ACTIVE_CONF_LINK" ]; then
current_config_target=$(readlink "$ACTIVE_CONF_LINK" | xargs basename)
fi

if [ "$current_config_target" != "$desired_config" ]; then
log "配置需要改变!从 '$current_config_target' 切换到 '$desired_config'。"
ln -sf "$AVAILABLE_DIR/$desired_config" "$ACTIVE_CONF_LINK"
log "符号链接已更新。"
exit 0
else
exit 1
fi

如何检测是否外接显示屏

DRM(略有迟钝,通用性强)

Linux的/sys目录下存放着大量和硬件相关的文件,而主机提供的各种接口同样以文件的形式存放于此。我们找到Linux系统的DRM子系统,该子系统负责图形硬件的控制,一般的桌面Linux系统与显示屏都通过该子系统进行交互。我们最后在/sys/class/drm目录下找到DRM子系统的各个接口

ls /sys/class/drm

card1 card1-DP-2 card1-DP-4 card1-DP-6 card1-DP-8 card1-Writeback-1 version
card1-DP-1 card1-DP-3 card1-DP-5 card1-DP-7 card1-HDMI-A-1 renderD128

  • card*-*目录下的status文件显示该接口是否处于连接状态,我们只需要探查是否有接口处于connected状态即可
monitor_connected=false
for connector in /sys/class/drm/card*-* ; do
if [[ -f "$connector/status" && $(cat "$connector/status") == "connected" ]]; then
monitor_connected=true
break
fi
done

DDC/CI(反应迅速,需要特定硬件支持)

如果你的显示屏可以使用DDC/CI协议通信,那么使用ddcutil就可以直接探查是否有显示屏

sudo apt update
sudo apt upgrade
sudo apt install ddcutil

ddcutil detect

# 无外接显示屏
No displays found.
Run "ddcutil environment" to check for system configuration problems.

# 存在外接显示屏
Invalid display
I2C bus: /dev/i2c-4
DRM connector: card1-HDMI-A-1
EDID synopsis:
Mfg id: KOS - UNK
Model: KOIOS K2418U
Product code: 9240 (0x2418)
Serial number: 0000000000000
Binary serial number: 0 (0x00000000)
Manufacture year: 2018, Week: 45
DDC communication failed

  • 虽然不知道为什么DDC交互中断了,但是如果外接显示屏的话其实还是可以获取一些显示屏的信息的,我们可以通过该特点区分是否外接显示屏
if ddcutil detect 2>&1 | grep -q "No displays found."; then
# grep 找到了 "No displays found.",退出码为 0,说明没有显示器。
monitor_connected=false
else
# grep 没找到 "No displays found.",退出码为 1,说明有显示器。
monitor_connected=true
fi

非热插拔

select-conf

接下来只要将上面两步结合在一起,就可以实现按需切换虚拟屏与物理屏了,给一个基于DRM的例子,使用DDC/CI只需要更换判断是否外接显示屏部分逻辑

sudo vim /usr/local/bin/select-xorg-conf.sh

#!/bin/bash

# --- 配置 ---
AVAILABLE_DIR="/etc/X11/xorg.conf.d.available"
ACTIVE_CONF_LINK="/etc/X11/xorg.conf.d/20-active-gpu.conf"
LOG_FILE="/var/log/xorg-selector.log"

log() { echo "$(date '+%F %T'): [Checker] $1" >> "$LOG_FILE"; }

monitor_connected=false
for connector in /sys/class/drm/card*-* ; do
if [[ -f "$connector/status" && $(cat "$connector/status") == "connected" ]]; then
monitor_connected=true; break
fi
done
desired_config=$([[ "$monitor_connected" == "true" ]] && echo "amdgpu.conf" || echo "dummy.conf")

current_config_target="none"
if [ -L "$ACTIVE_CONF_LINK" ]; then
current_config_target=$(readlink "$ACTIVE_CONF_LINK" | xargs basename)
fi

if [ "$current_config_target" != "$desired_config" ]; then
log "配置需要改变!从 '$current_config_target' 切换到 '$desired_config'。"
ln -sf "$AVAILABLE_DIR/$desired_config" "$ACTIVE_CONF_LINK"
log "符号链接已更新。"
# 配置改动
exit 0
else
# 一切正常
exit 1
fi

systemd

同时,我们只要在display-manager.service启动进行一次探查即可

sudo vim /etc/systemd/system/xorg-select.service

[Unit]
Description=Select Xorg configuration based on monitor presence
Before=display-manager.service
Wants=systemd-udev-settle.service
After=systemd-udev-settle.service

[Service]
Type=oneshot

ExecStartPre=/bin/bash -c '\
COUNTER=0; \
until ls -d /sys/class/drm/card* >/dev/null 2>&1; do \
sleep 0.2; \
COUNTER=$((COUNTER+1)); \
if [ $COUNTER -gt 50 ]; then \
echo "Timeout waiting for any /sys/class/drm/card* device." >&2; \
exit 1; \
fi; \
done; \
echo "Found a DRM card device. Waiting for connectors to settle..." >&2; \
sleep 2'

ExecStart=/usr/local/bin/select-xorg-conf.sh

[Install]
WantedBy=display-manager.service
sudo systemctl daemon-reload
sudo systemctl enable xorg-select.service

注意

  • 注意使用sudo chmod +x给执行脚本权限,否则开机服务将执行失败
  • 由于DRM子系统探查硬件的时机不确定,故使用DRM方案需要先判断DRM子系统硬件是否探查完毕,以card*目录出现为标志
  • DDC/CI方案不依赖于DRM子系统,不需要等待card*目录出现

热插拔

非热插拔方案只在每次开机时进行探查,这意味着在主机运行期间进行插拔HDMI线等是不会产生任何影响的(当然拔了显示屏,物理屏方案自然就不能用了,这里是说Xorg服务器运行的配置)

如何优雅地重启X.org服务器

当然你可以采用kill手段直接清楚已有的X.org服务器,但是由于图形化页面繁杂,建议直接将图形化页面重启,即重启display-manager.service,所以我们写一个脚本或者服务,在检测到配置不符时重启display-manager.service

sudo vim /etc/systemd/system/xorg-conf-check.service

[Unit]
Description=Check X.Org configuration after a potential monitor change

[Service]
Type=oneshot
# 我们直接运行检查脚本
ExecStart=/usr/local/bin/select-xorg-conf.sh
# 0 -> 配置应当改变 -> ExecStartPost
# 1 -> 配置无需改变 -> ExecStartPost
ExecStartPost=/bin/bash -c 'echo "Configuration changed, restarting GDM in 3s..." | systemd-cat -p info && sleep 3 && systemctl restart display-manager.service

sudo systemctl daemon-reload

如何实时检测

使用udev实时检测即可,DRM探测发现有热插拔现象时会将环境变量的”HOTPLUG”置1,所以我们可以为udev编写一条新的rule,在HOTPLUG发生变化时执行我们的脚本

sudo vim /etc/udev/rules.d/num-name.conf

  • num代表加载顺序
  • name代表具有一定语义的名称
  • 例如95-monitor.conf
# 当DRM子系统发生变化时,重启我们的延时检查定时器
ACTION=="change", SUBSYSTEM=="drm", KERNEL=="card*", ENV{HOTPLUG}=="1", RUN+="/bin/systemctl restart /etc/systemd/system/xorg-conf-check.service"

问题

硬件更新有延迟

热插拔后,硬件有一小段时间的延迟,随后/sys/class/drm/card*/status才会发生改变,所以我们可以设置一个计时器,udev触发后一段时间在执行xorg-conf-check服务

新建同名timer

sudo vim /etc/systemd/system/xorg-conf-check.timer

[Unit]
Description=Run X.Org configuration check 10s after a monitor hotplug event

[Timer]
# 当定时器被启动后,等待3s再执行服务(或者更长时间)
OnActiveSec=3s
Unit=xorg-conf-check.service

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload

udev改为触发计时器,而不是触发服务

# 当DRM子系统发生变化时,重启我们的延时检查定时器
ACTION=="change", SUBSYSTEM=="drm", KERNEL=="card*", ENV{HOTPLUG}=="1", RUN+="/bin/systemctl restart /etc/systemd/system/xorg-conf-check.timer"

热插拔一两次后失效

该问题主要针对使用DRM子系统进行判断的方案,开机后进行一两次热插拔后,虽然会触发”HOTPLUG”,但是/sys/class/drm/card*/status不再改变,导致所有基于DRM方案完全失效,目前暂无好的解决方案,推荐更换DDC/CI或者设置为非热插拔。

不过对于热插拔情况较少时仍可以考虑DRM,否则还是推荐DDC/CI

图形化页面崩溃

注销当前用户即可,目前尚不清楚是由什么引起的