很多用Ubuntu做无头服务器或远程工作站的朋友,应该都遇到过同一个痛点:机器放在角落里,没有接显示器,但某些应用偏偏依赖一个"屏幕"才能正常工作。要么是远程桌面连上去黑屏,要么是显卡渲染任务直接报错。我最早碰到这问题时,第一反应是买一根HDMI欺骗头,但后来发现,这玩意儿在Linux上根本不是最优解,而且还需要额外花钱、占一个物理接口。折腾了一圈,最后用纯软件方案解决了:虚拟显示器 + 开机自启动 + 启动脚本,一套组合拳打下来,机器通电到服务就绪全程无人工干预。
这篇文章我会把整套方案从头到尾拆开讲,包含为什么需要虚拟显示器、几种实现方式的对比、系统服务怎么写、脚本怎么调,以及我实际踩过的坑。内容比较多,但每一步都可以直接复制落地。
1. 应用场景拆解:为什么无头Ubuntu需要一块"隐形屏幕"
先说清楚这套方案到底解决什么问题。不是所有跑Ubuntu的机器都需要虚拟显示器,如果你只是把它当成一个跑Nginx、MySQL的服务器,SSH上去敲命令就行,那确实用不上。但如果你属于下面这几类情况之一,那"有一块显示器"就变成了硬性需求。
第一类是远程桌面用户。无论是用VNC还是RDP协议连到Ubuntu桌面,图形会话都需要一个显示输出。物理上没有接显示器时,很多桌面环境会直接拒绝启动,或者启动后分辨率锁死在800x600,体验极差。虚拟显示器的本质是让系统以为有一块物理屏幕存在,从而正常初始化图形栈。
第二类是跑GPU计算或渲染任务的用户。CUDA、OpenCL这类计算本身不依赖显示器,但有些调用框架(比如OpenGL离屏渲染、视频硬编解码的某些分支)会在初始化时检查显示设备。特别是用FFmpeg做硬件转码、或用Unity/Godot做自动构建出图时,没有显示器经常会报"unable to open display"之类的错误。
第三类是自动化测试和持续集成场景。跑Selenium、Appium或者其他UI自动化框架时,浏览器和移动模拟器需要一个虚拟屏幕来绘制界面。业界常用的Xvfb(X虚拟帧缓冲)就是干这个的,但它只提供虚拟帧缓冲,不支持硬件加速。如果测试流程里需要OpenGL支持,那就得换用带DRM/KMS支持的虚拟显示方案。
第四类需求比较隐蔽:远程串流游戏或云桌面。比如用自己的Ubuntu机器跑Sunshine串流,主机端如果没有虚拟显示器,串流出去的画面会直接黑屏或只有桌面的残缺部分。虚拟出一个4K屏幕后,串流的画质和帧率反而比物理显示器更稳定,因为虚拟屏幕的EDID数据是固定的,不会出现物理屏休眠导致的掉线问题。
我的实际使用场景算是第二类和第四类的结合:一台塞在机柜里的Ubuntu工作站,平时通过SSH管理,偶尔需要远程桌面进去做一些图形化配置;同时它还要跑一个基于Python的OpenGL渲染脚本,每隔一段时间自动处理一批3D模型出图。没有虚拟显示器之前,每次重启机器都要手动插上一个HDMI欺骗头,再在启动脚本里加几分钟的延迟等待桌面起来。为了让这套流程彻底自动化,我决定把虚拟屏和自启动做成系统级配置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 虚拟显示器的三种实现方案与选型对比
在Ubuntu上做虚拟显示器,方案其实不少,但不同方案的适用范围差异很大。我在调研和实测过程中,把主流的方案都过了一遍,这里直接给结论。
2.1 内核级虚拟显示驱动:根据EDID伪造屏幕
第一种思路是不引入额外的虚拟设备驱动,而是在内核引导阶段或Xorg配置层面,让显卡以为接了一块特定规格的显示器。最常用的工具是drm.edid_firmware引导参数,配合官方EDID固件或自定义EDID数据。
原理很好理解:真实显示器接入时,显卡通过DDC通道读取显示器的EDID数据,拿到分辨率、刷新率、物理尺寸、色深等参数。内核的EDID固件机制允许你指定一个模拟的EDID数据源,让显卡认为有一块固定的屏幕存在。具体操作是在/etc/default/grub的GRUB_CMDLINE_LINUX_DEFAULT里加参数:
code复制drm.edid_firmware=HDMI-A-1:edid/1920x1080.bin
这个做法有三个明显优势:不依赖具体桌面环境,不需要额外常驻进程,而且对NVIDIA、AMD、Intel显卡都有效(前提是驱动支持EDID固件加载)。但缺点是灵活性差,分辨率、刷新率是写在EDID文件里的,想临时改参数必须重新生成文件并重启。而且如果你的显卡驱动版本较老,可能根本不吃这个参数。
2.2 应用层虚拟显示器:ldd虚拟设备
第二种方案是装一个专门生成虚拟显示器的工具,让它创建一个虚拟的DRM设备。目前社区里最成熟的方案是ldd(Linux Display Drivers的虚拟显示项目),也就是很多人都听过但也有人装不上的"虚拟显示器软件"。它属于内核模块,会注册一个虚拟的DRM设备(通常是/dev/dri/card1),从而让系统多出一块屏幕。
有一点需要特别注意:ldd需要DKMS(Dynamic Kernel Module Support)配合内核头文件编译安装。每次升级内核后,模块也需要重新编译,否则会出现设备找不到的情况。如果你用的是默认的Ubuntu内核,编译一般不会太困难;但如果用了HWE(硬件支持)内核或者其他第三方内核,可能要手动适配。
装上之后,虚拟屏幕会作为一个独立的Xorg输出出现。配合XRandR可以把它设为扩展屏或镜像屏,分辨率最高能设到4K甚至8K。这个方案的优点是灵活可控,支持热插拔式的启用/停用,做多显示器测试特别方便。缺点是多了一个内核模块需要维护,升级内核时容易翻车。
2.3 帧缓冲级方案:Xvfb
第三种就是很多人熟知的Xvfb,严格来说它不是"显示器",而是一个运行在内存中的虚拟帧缓冲服务。它模拟了一个X Server,所有图形渲染都在内存里完成,画面不会输出到任何物理设备。
Xvfb在自动化测试领域用得极多,xvfb-run加一行前缀就能把一个需要显示器的命令跑起来。但它的局限也很明显:默认不启用GLX扩展,OpenGL渲染需要额外配置;而且它跟Wayland会话的兼容性很差,如果你已经切到了Wayland,Xvfb能帮上的忙比较有限。
我的选择是第二种方案ldd,因为我的OpenGL渲染脚本需要真实的DRM设备做硬件加速上下文,Xvfb绕不过去。以下是这几个方案的横向对比,看完应该就知道怎么选了。
| 方案 | 原理 | 显卡加速 | Wayland兼容 | 维护成本 | 适用场景 |
|---|---|---|---|---|---|
| EDID固件欺骗 | 内核引导时伪造屏幕数据 | 支持 | 支持 | 低,改grub即可 | 固定分辨率无头渲染、远程桌面 |
| ldd虚拟DRM设备 | 内核DKMS模块创建虚拟显示器 | 支持 | 支持 | 中,升级内核需重编 | 多虚拟屏、自动化UI测试、串流 |
| Xvfb帧缓冲 | 用户态虚拟X Server | 不支持GLX | 不兼容 | 低,开机自启一条命令 | 轻量自动化测试、命令行跑图形程序 |
3. 从grub参数到ldd安装:虚拟屏配置全流程
既然要追求"开机自启动",那就应该在系统层面把虚拟显示器变成一块"原生设备",而不是每次开机后手动跑一条软件命令。我最终采用的是EDID固件欺骗为主、ldd为辅的双保险方案:系统层面的EDID可以在任何桌面环境/显示服务器下正常工作,ldd则在需要多虚拟屏或特殊分辨率时作为补充。
3.1 第一步:生成自定义EDID固件
EDID数据是一段128字节的二进制内容,手动编辑不现实,有现成的Pyton脚本可以生成。我用的工具是edid-generator,GitHub上有现成仓库,直接克隆下来就能用:
bash复制git clone https://github.com/akatrevorjay/edid-generator
cd edid-generator
make
仓库里内置了很多常见分辨率的EDID模板,比如1920x1080.bin、3840x2160.bin。不过默认模板的刷新率参数可能不太适合你的场景,如果只是跑桌面和串流,用默认的就够。想自定义分辨率的话,需要改modeline参数,edid-generator的Makefile里已经支持了通过环境变量指定参数,只是语法有点绕:
bash复制make MODELINE="1920 1080 60" EDID_NAME=MY_EDID
生成的.bin文件需要放到内核固件目录,并更新initramfs:
bash复制sudo mkdir -p /lib/firmware/edid
sudo cp 1920x1080.bin /lib/firmware/edid/
sudo update-initramfs -u
这里有个很重要的细节:必须执行update-initramfs,否则引导时内核不会加载新的固件目录内容。我第一遍配置时漏了这一步,重启后怎么查都是没效果,后来才发现是initramfs缓存的问题。
3.2 第二步:修改grub引导参数
EDID文件就位后,修改/etc/default/grub,在GRUB_CMDLINE_LINUX_DEFAULT里追加参数。这里注意,如果你的显卡有多个输出接口,要指定正确的接口名,一般用HDMI-A-1或者DP-1,可以用ls /sys/class/drm/查看当前系统识别到的接口列表:
bash复制ls /sys/class/drm/
常见的输出有card0-HDMI-A-1、card0-DP-1、card0-VGA-1、card0-eDP-1(笔记本内屏)。选一个没接物理设备的接口,把EDID绑定到它上面:
code复制GRUB_CMDLINE_LINUX_DEFAULT="quiet splash drm.edid_firmware=HDMI-A-1:edid/1920x1080.bin"
改完之后更新grub配置并重启:
bash复制sudo update-grub
sudo reboot
重启后验证是否生效,可以用dmesg查内核日志:
bash复制dmesg | grep edid
如果看到类似[drm] Got external EDID base block and 1 extension from "edid/1920x1080.bin"的信息,说明EDID固件已经被加载,虚拟显示器在系统底层已经"存在"了。
3.3 第三步:ldd模块编译与虚拟显示器启用
EDID方案适合绑定一块固定屏幕,但如果你想在运行中动态创建和销毁虚拟显示器,或者需要同时模拟多个屏幕,那还是得用ldd。安装步骤如下:
bash复制sudo apt install dkms build-essential
git clone https://github.com/ldd/ldd
cd ldd
make
sudo make install
sudo modprobe ldd
这里有一个实际很容易卡住的地方:make时可能会报头文件缺失,尤其是linux-headers-$(uname -r)没装全。先执行一下这句排掉隐患:
bash复制sudo apt install linux-headers-$(uname -r)
如果用的是HWE内核,头文件包名会变成linux-hwe-$(uname -r)-headers,注意适配。modprobe加载成功后,用ls /dev/dri/应该能看到新的card1。此时用xrandr应该也能列出虚拟显示器的输出名,通常是VIRTUAL1或DVI-I-1-1这类名字。
设置分辨率的命令很简单:
bash复制xrandr --output VIRTUAL1 --mode 1920x1080 --primary
但要注意,每次重新加载模块或重启系统后,xrandr的设置会丢失,所以这行配置必须写进自启动脚本,这一点我下一节会细讲。
3.4 第四步:把ldd设置为开机自动加载
如果希望ldd模块在开机时自动加载,需要把它加入/etc/modules-load.d/:
bash复制echo "ldd" | sudo tee /etc/modules-load.d/ldd.conf
同时在/etc/modprobe.d/里加一个选项文件,确保模块参数稳定:
bash复制echo "options ldd modeset=1" | sudo tee /etc/modprobe.d/ldd.conf
modeset=1的含义是启用内核模式设置,让虚拟显示器在KMS层面就被识别。如果你的桌面环境是Wayland,这个参数尤其重要,否则Wayland的图形栈可能不会把虚拟显示器当作有效输出。
到了这一步,虚拟显示器的底层就全部就绪了:一块(或几块)系统自识别的虚拟屏幕,永久存在于内核层,不会因为物理服务器没有接显示器而缺失。接下来要解决的是怎么把桌面初始化、分辨率设置、业务脚本启动这三件事串成一条自动链路。
4. 开机自启动的三个层次:systemd、rc.local与桌面自启
很多人提到开机自启动,第一反应是往/etc/rc.local里塞命令,或者用crontab @reboot。这些方法不是不能用,但很不稳定,特别是涉及图形环境、环境变量、服务依赖顺序时,经常会遇到"命令执行了但没生效"的情况。
我的经验是按层次拆分需求。拿我这台机器为例,开机后需要自动完成四件事:加载ldd虚拟显示器模块、启动桌面环境并设置分辨率、启动远程桌面/VNC服务、启动Python渲染脚本。它们的启动时机和依赖各不相同,不能一股脑写进同一个脚本里。
4.1 内核模块层:modules-load.d就够了
加载ldd模块这种事,属于内核级初始化,用systemd-modules-load.service处理最干净。只要把模块名写进/etc/modules-load.d/ldd.conf就完事了,不需要自定义systemd服务。这个服务在系统启动早期就会执行,早于任何用户态服务,所以虚拟显示器设备在桌面环境起来之前就一定存在。
有一个不起眼但很重要的坑:模块加载的顺序依赖。如果你的显卡驱动用了第三方DKMS版本(比如NVIDIA的新驱动),ldd模块可能依赖显卡驱动先加载。这种情况下,modules-load.d里模块名的顺序其实不能保证顺序,需要写成systemd服务的After=依赖才靠谱。但就我的测试,Intel和AMD显卡下直接用modules-load.d没出过问题,NVIDIA机器上建议还是老实写自定义服务。
4.2 图形环境依赖层:开机桌面自启目录
虚拟显示器加载后,分辨率设置这类操作必须在图形会话里执行。如果你开机后自动登录了桌面,那么把脚本放到~/.config/autostart/目录下是最高效的方式。目录下的.desktop文件会被桌面环境在登录后自动执行:
ini复制[Desktop Entry]
Type=Application
Name=VirtualDisplaySetup
Exec=/home/yourname/bin/setup_virtual_display.sh
X-GNOME-Autostart-enabled=true
.desktop文件里Exec指向的脚本必须要有可执行权限,而且最好写绝对路径。如果你有多个显示器或用了一套复杂的xrandr命令,建议脚本里加一个延迟:登录后桌面可能还没完全初始化,立刻执行xrandr可能找不到输出。我一般会在脚本开头加sleep 3,等窗口管理器就位。
这层适合设置分辨率、启动托盘程序等"必须登录后才干"的事。但它的局限很明显:如果系统启用了自动登录,没问题;如果设置了开机停在登录界面,那桌面自启目录根本不会执行。所以对于更关键的服务,应该用systemd服务来做。
4.3 用户服务层:systemd User Unit
Python渲染脚本、远程桌面服务这类东西,不依赖具体桌面会话,但需要用户环境变量(比如DISPLAY、XAUTHORITY、CUDA_HOME)。这种需求最适合systemd的user unit机制。
用户级systemd服务的核心优势是:跟随用户登录而启动,但可以在用户退出时选择是否杀掉。配合loginctl enable-linger,可以让服务在用户未登录时也保持运行。对于无头服务器,这条命令几乎是必须的:
bash复制sudo loginctl enable-linger yourname
开启linger后,用户级服务就能完全脱离桌面会话运行,简直是为无头虚拟显示器场景量身定做的。
下面是我的~/.config/systemd/user/render-worker.service示例:
ini复制[Unit]
Description=Headless Render Worker
After=network-online.target
[Service]
Type=simple
Environment=DISPLAY=:0
Environment=XAUTHORITY=/home/yourname/.Xauthority
ExecStart=/home/yourname/bin/start_render_worker.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
启动并设为自启:
bash复制systemctl --user daemon-reload
systemctl --user enable --now render-worker.service
注意一个关键细节:DISPLAY=:0并不总是对的。如果你用Wayland,或者Xorg占用了其他display编号,服务里的DISPLAY写死会导致Python脚本根本连不上X server。建议先启动服务,再用loginctl show-user查一下实际的display,或者直接在脚本里动态检测:
bash复制export DISPLAY=$(ls /tmp/.X11-unix/ | grep -oP 'X\d+' | head -1 | sed 's/X//' | awk '{print ":"$1}')
4.4 全局服务层:System Unit
有些服务必须在任何用户登录之前就启动,比如VNC桌面共享、或一个需要在多个会话间共享的监控进程。这时候用系统级systemd服务。系统级服务比用户级服务早启动,而且不受登录状态影响,但配置时要格外小心环境变量,因为系统服务默认不继承用户环境,连HOME都可能是/root。
这是/etc/systemd/system/headless-vnc.service的一个简化示例:
ini复制[Unit]
Description=Headless VNC Service
After=docker.service network.target
[Service]
Type=forking
User=yourname
Group=yourname
Environment=HOME=/home/yourname
ExecStart=/home/yourname/bin/vnc_start.sh
ExecStop=/home/yourname/bin/vnc_stop.sh
Restart=always
[Install]
WantedBy=multi-user.target
配置完成后:
bash复制sudo systemctl daemon-reload
sudo systemctl enable --now headless-vnc.service
到这里,开机自启动的架构就比较清晰了:内核模块走modules-load.d,桌面分辨率走图形会话自启,业务进程走systemd。三层互不干扰,也不需要搞一堆sleep去猜启动顺序。
5. 启动脚本的编写细节与实际可用的通用模板
脚本是整个方案的执行末端。很多人的自启动"失效",表面上看起来是systemd配置不对,实际是脚本内部缺少容错机制。比如环境变量没设置、工作目录不对、可执行权限缺失、Python虚拟环境没有激活等,这些都足以让脚本"执行了但啥事没干"。
下面给一份我实测可用的启动脚本模板,涵盖虚拟显示器分辨率设置和业务进程拉起。请根据自身情况调整路径和命令。
bash复制#!/bin/bash
# headless_setup.sh - 无头环境自启动脚本
set -e
# 用户环境变量
export DISPLAY="${DISPLAY:-:0}"
export XAUTHORITY="${XAUTHORITY:-$HOME/.Xauthority}"
# 等待X server就绪(最长15秒)
for i in $(seq 1 15); do
if xdpyinfo >/dev/null 2>&1; then
break
fi
sleep 1
done
# 检测虚拟显示器是否存在,不存在则直接退出
if xrandr | grep -q "VIRTUAL1"; then
xrandr --output VIRTUAL1 --mode 1920x1080 --primary
elif xrandr | grep -q "DVI-I-1-1"; then
xrandr --output DVI-I-1-1 --mode 1920x1080 --primary
fi
# 检查Python虚拟环境
if [ -f /home/yourname/venv/bin/activate ]; then
source /home/yourname/venv/bin/activate
fi
# 启动渲染任务
cd /home/yourname/render_project
nohup python3 main.py >/tmp/render.log 2>&1 &
echo "Headless setup completed at $(date)" >> /tmp/headless_setup.log
这个脚本基本能满足大多数需求。但有几个细节想特别说明。
5.1 为什么要在脚本里判断DISPLAY显示编号
很多教程会直接写死export DISPLAY=:0,这在普通桌面机上没问题,但无头环境下display编号经常不是0。特别是多用户切换、多显卡机器,Xorg可能落在:1甚至:2。如果你脚本里的DISPLAY和实际的X server不匹配,xrandr会直接报cannot open display,但脚本日志里又看不到什么明显错误,排查起来很迷惑。
实用技巧是开头等xdpyinfo返回成功再继续。xdpyinfo是x11-utils包里的命令,如果提示找不到,先安装:
bash复制sudo apt install x11-utils
5.2 脚本执行权限与编码问题
脚本写完一定记得加执行权限,这点容易被忽略:
bash复制chmod +x /home/yourname/bin/headless_setup.sh
另外,如果是从Windows上编辑后拷贝到Ubuntu的脚本,可能带着CRLF换行符,导致systemd启动时报bad interpreter。处理方式:
bash复制sed -i 's/\r$//' /home/yourname/bin/headless_setup.sh
5.3 业务进程的启动姿势:nohup还是systemd
我在模板里用了nohup python3 main.py &,这是为了兼容习惯。但实际上如果你的业务进程已经用systemd user unit管理了,那就不需要在脚本里再启动它。两个方式同时存在会导致重复拉起进程,逻辑会乱掉。
我的建议是:优先用systemd管理业务进程,启动脚本只处理环境准备(配置显示器、导出环境变量、检查依赖)。这样进程意外退出时能被systemd自动拉起,省去手工看日志的麻烦。如果某个脚本确实不适合交给systemd(比如它自己会fork出多个子进程),再用nohup配合setsid做隔离。
5.4 一个容易踩的坑:Wayland会话下的XAUTHORITY
如果你用的是Wayland而不是Xorg,那$HOME/.Xauthority根本不存在,强制设置会导致xrandr、xdpyinfo全部失败。Wayland下更合适的做法是直接用systemd-run --user或者用weston的headless backend。说实话,在Wayland无头场景下,用ldd的DRM设备直接跑一个独立的Xorg是更稳的路线,即"Wayland会话之外再挂一个Xvfb/Xorg"。这需要在启动脚本里分开处理,不能一套X11逻辑通吃。
6. 排查经验:我从"开机自启失败"到"全自动运行"的修复链路
这部分原本可以一句带过,但实际排查过程非常有价值,详细写出来能帮你省几个小时。我遇到的问题是:ldd模块开机后没有自动加载,但手动modprobe又完全正常。
6.1 第一个疑点:系统服务运行时机
先说结论,问题出在systemd的服务依赖上。/etc/modules-load.d/里的模块加载是由systemd-modules-load.service处理的,它默认在sysinit.target阶段执行。这个阶段其实已经相当靠前了,比任何用户服务都早。但我机器的环境有点特殊:显卡驱动不是initramfs里的默认驱动,而是通过DKMS在系统启动后期加载的。ldd模块编译时链结了显卡驱动的头文件,加载ldd时如果显卡驱动还没加载,就会出现Unknown symbol错误,模块加载直接失败。
排查方法很简单,查看内核日志里有没有模块加载的报错:
bash复制journalctl -b | grep -i ldd
看到类似ldd: Unknown symbol drm_helper_mode_fill_fb_struct的报错,就是依赖没满足。
6.2 修复方案:自定义模块加载服务
因为systemd-modules-load.service本身不支持写After=依赖显卡驱动,我直接弃用modules-load.d,改成自定义systemd服务,在服务里modprobe:
ini复制[Unit]
Description=Load ldd virtual display module
After=multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/modprobe ldd
[Install]
WantedBy=multi-user.target
After=multi-user.target的意思是在系统进入多用户模式后再加载模块,此时显卡驱动必然已经就绪。虽然比modules-load.d晚了一点,但虚拟显示器只需要在图形服务启动前就位,这个时机完全来得及。
6.3 第二个坑:user service没有真正自启
ldd模块的问题解决后,又出现了一个新问题:render-worker.service虽然在systemctl --user enable后显示enable状态,但重启后并没有自动启动。查了很久才发现是linger没有设置。
解释一下:systemd用户服务是否随开机启动,取决于用户是否被loginctl视为"活跃"。如果机器开机后无人登录,用户bus也就不会初始化,用户服务自然不会启动。loginctl enable-linger正是解决这个问题的,它会让指定用户即使不登录也存在,从而让用户级服务可以自启。
这也提醒了我一个原则:系统级服务能解决的不要用用户级服务。如果某个服务真的需要开机自启且不依赖桌面登录,优先放到/etc/systemd/system/。用户级服务适合那些需要从桌面环境继承环境变量的场景,使用前必须先确认linger已开启。
6.4 第三个坑:xrandr找不到虚拟显示器输出
最后还遇到过一个问题:ldd模块加载了,/dev/dri/card1也出现了,但登录桌面后xrandr就是看不到VIRTUAL1。排查后发现问题出在Xorg的配置上——Xorg默认只扫描物理连接的输出,虚拟输出需要额外的drv识别。
解决方式是在/etc/X11/xorg.conf.d/下创建一个10-virtual-display.conf:
code复制Section "Device"
Identifier "ldd"
Driver "modesetting"
BusID "PCI:0:0:0"
Option "DRMDevice" "/dev/dri/card1"
EndSection
这里DRMDevice明确指定了虚拟显示器使用的DRM设备节点。如果你只有一张显卡且虚拟显示器是card1,这样配置基本没错。改完后重启Xorg或直接重启机器。
7. 进阶优化:多显示器虚拟屏、分辨率热切换与能耗控制
基础方案跑通后,我发现这套组合拳还能继续升级,满足更复杂的场景需求。这里记录三个进阶方向,都是我自己验证过的。
7.1 多虚拟屏同时开
ldd支持一次创建多块虚拟显示器。默认加载模块只创建一个card,但通过模块参数可以指定数量:
bash复制echo "options ldd cards=2" | sudo tee /etc/modprobe.d/ldd.conf
重启后/dev/dri/里出现card1和card2。配合两个xrandr命令就能生成两个独立虚拟屏。这个能力在UI自动化里很实用——可以同时跑两套浏览器测试,互不干扰。
7.2 分辨率热切换
EDID固件的分辨率是写死的,但ldd的虚拟屏可以随时用xrandr切换。如果你想在1080p和4K之间来回切换,准备两个.desktop文件或者两个别名命令即可,不用重启机器:
bash复制alias vhd-4k='xrandr --output VIRTUAL1 --mode 3840x2160'
alias vhd-1080='xrandr --output VIRTUAL1 --mode 1920x1080'
把这两行写进~/.bashrc,日常操作就方便多了。
7.3 能耗与资源占用
虚拟显示器不是物理设备,不消耗显示器功耗,但会占用一部分显存和渲染资源。4K分辨率下,虚拟屏的帧缓冲会占几百MB显存,GPU的渲染负载也会略微上升。对于跑CUDA任务的机器,建议虚拟屏分辨率不要盲目上4K,除非串流或测试确实需要。1080p是收益率最高的选择。
另外,如果机器只是偶尔需要虚拟显示器,不想让它常驻,可以写一个systemd服务用来按需启停虚拟显示器。比如把headless-setup.service设为disabled,手动需要时再启用一次:
bash复制sudo systemctl start headless-setup.service
sudo systemctl stop headless-setup.service
启用linger后,这套按需启停的逻辑也能用用户级systemd来管理。
8. 最后再分享一个提高调试效率的小技巧
整个方案调试过程中,最浪费时间的是每次改完配置都要重启机器,然后登录进去看效果。后来我想了个办法,直接在不重启X会话的情况下重新加载配置。
Xorg支持动态加载配置片段,你只需要把修改后的.conf文件放到/etc/X11/xorg.conf.d/,然后用systemctl restart display-manager重启显示管理器。如果用的是GDM:
bash复制sudo systemctl restart gdm3
这个操作比重启整机快很多,但会杀死当前图形会话,SSH窗口不受影响。对于本机操作不方便的无头服务器,建议永远保留一个SSH会话作为"逃生通道",别把图形界面当作唯一的管入口。
我的个人体验是:把虚拟显示器、开机自启动、启动脚本这三件事拆开,各自用最合适的系统机制去处理,而不是塞进一个大而全的脚本里,整条链路会稳定很多。现在这台Ubuntu机器通电之后,从post到Python渲染进程就绪大概耗时一分半钟,全程不需要插物理显示器也不需要人为登录。这套方案我用了大半年,期间升级过几次内核,除了ldd模块需要重编一次外,其他部分从未出过问题。
如果按照同样的思路配置你的机器,建议第一次操作准备好一个可用的SSH连接,然后再动grub和systemd配置,万一哪里出了问题,至少还能通过SSH救回来。祝你一次跑通。
