年初写过一篇文章《从 X11 到 Wayland,迈出这一步为何如此艰难?》,分析了从 X11 演进到 Wayland 所面临的困难。直到今天,Wayland 替代 X11 仍不容乐观。虽然 Ubuntu、Debian 等发行版本都将默认的桌面环境设置到 Wayland,但很多用户在遇到诸多兼容问题之后,仍会切换回 X11。比如我前段时间使用 Ubuntu 24.04,实在受不了里面的输入法,为了使用搜狗输入法,不得不切换回 X11。
理论上,X Window System (X11) 服役已逾三十年,设计理念已显陈旧,又有性能、安全和维护性上的一系列问题,作为 X11 的继任者 Wayland,替换 X11 是一件水到渠成的事,为什么阻力这么大?
本文将从 X11 的核心痛点出发,深入剖析 Wayland 为何被设计成如今的模样。我们将系统性地讲解 Wayland 的核心架构、协议机制,并通过分析 libwayland 的客户端与服务端代码,揭示其通信的底层逻辑。无论你是桌面环境开发者、Linux爱好者还是对图形技术感兴趣的工程师,本文都将为你提供一份从理论到实践的详尽指南。
读完本文之后,你就会明白 Wayland 取代 X11,并非技术上做不到,更多的是生态问题,甚至是,有人的地方就有江湖,技术之争在开源世界并不少见。就在本月初,曾深度参与 Xorg 开发的维护者 Enrico Weigelt 又搞了一个 X11 的分支: XLibre Xserver,试图复兴逐渐被边缘化的图形服务器 X11。
本文借助了 AI 工具 DeepWiki 进行了 libwayland 和 Weston 的源码分析,如果有什么错误,那一定是我的问题,还请指正。
先看一张 X11 和 Wayland 架构对比图,来自 freedesktop.org:
从上图可以看出,Wayland 相比 X11 架构,就是拿掉了 X Server。因为这个 X Server 曾经扮演着重要的角色,如今却处境尴尬。
现代人可能难以理解,但我们要知道,X 系统诞生于 1980 年代,那时的计算环境是一台强大的中央服务器(比如 VAX 或 Sun SPARCstation)和许多连接到它的“哑终端”(Dumb Terminals)。这些终端几乎没有计算能力,只有一个屏幕、键盘和鼠标。网络速度也相对较慢。
X11 的设计完美地契合了这个场景。当一个应用程序(Client)想要在屏幕上画些什么时,它不会自己去计算每个像素的颜色。相反,它向 X Server 发送一系列绘图指令(Drawing Primitives)。这些指令非常基础,就像是在指挥一个远程的绘图机器人:
这种模式的优点在当时非常明显:
然而,随着计算机技术的发展,到了今天,计算环境发生了翻天覆地的变化:
这些复杂的视觉效果很难用 X11 的基础绘图指令来描述。比如,你该如何用指令告诉 X Server 去画一个带有高斯模糊的半透明阴影?这几乎是不可能的。
因此,现代 GUI 工具包(如 GTK、Qt)和应用程序(如 Chrome、Blender)采取了一种完全不同的策略:客户端渲染(Client-Side Rendering)。
也就是说,客户端负责全部的渲染工作,生成一个完整的位图。它不再指挥服务器去画画,而是直接把画好的成品交给服务器去展示。
在这种工作模式下,一个尴尬而低效的工作流就出现了。我们以一个带有桌面特效(如窗口阴影、透明)的现代 Linux 桌面为例:
很明显,这个过程引入了至少一到两次不必要的进程间通信(IPC)和数据拷贝,导致了延迟增加、性能下降和系统资源浪费。
有朋友可能回说,这么理解 X Server,是否太浅薄了,要知道 X Server 不仅是显示服务器,还包含了窗口管理逻辑、输入设备驱动、字体渲染、2D图形加速(通过 XRender 等)等众多功能。
是的,曾经 X Server 是一个巨大而复杂的单体,而且为了适应新功能(如 Compositing、RandR),X11 引入了大量扩展,这使得修复 bug 和添加新功能变得异常困难,所以很多功能已经从 X Server 剥离出来了。
通过上面的逐步剥离,X Server 就像一个逐渐被“掏空”的历史建筑。
如果仅仅是上述问题,那么我们也没有必要推倒重建,就像 X 系统的改良派所坚持的,又不是不能用,修修补补还可再战。但是 X11 的体系架构存在严重的安全漏洞:
为了甩掉 X Server 这个包袱,Wayland 应运而生,让那些已经实际承担工作的组件(合成器、客户端库)直接对话,不再需要中间这个日渐臃肿、低效且不安全的“传话筒”。
当我们说“我的桌面在用 Wayland”,这其实是一种简化说法。准确来说,我们的桌面环境(如 GNOME、KDE)正在使用遵循 Wayland 协议的组件。
什么是协议? 想象一下 HTTP。HTTP 本身不是浏览器(如 Chrome)也不是 Web 服务器(如 Nginx),它只是一套规则,规定了浏览器和服务器之间应该如何请求和响应数据(GET /index.html HTTP/1.1)。
同样,Wayland 协议定义了一套客户端(应用程序)和服务器(合成器)之间进行通信的语言和规则。它精确地描述了:
Wayland 本身没有一行代码是关于如何画窗口、如何处理鼠标点击的。它只负责 “传话”。这意味着,不存在一个叫做“Wayland 显示服务器”的单一、标准的程序。任何软件,只要它能正确地“说”和“听” Wayland 协议定义的这门语言,它就可以成为一个 Wayland 服务器(Compositor)。这就是为什么 GNOME 的 Mutter 和 KDE 的 KWin 都是 Wayland Compositor,但它们是完全不同的软件。
这是 Wayland 相对于 X11 最具颠覆性的变革,也是其简洁高效的根源。在 X11 的世界里,显示服务器(X Server)、窗口管理器(Window Manager)和合成器(Compositor)是三个独立的角色,它们之间通过复杂的协议和扩展进行通信。
而在 Wayland 的世界里,这三个角色被合并成了一个单一实体:Compositor。
这个 Compositor 现在是唯一的权力中心,它直接负责:
再回顾一下这张架构对比图,Wayland 通过将所有图形和输入相关的职责集中到 Compositor,极大地简化了图形栈。数据流从“应用 -> X Server -> Compositor -> X Server -> 硬件”这条曲折的路径,变成了“应用 -> Compositor -> 硬件”这条直线高速公路。
在了解 Wayland 总体蓝图后,现在我们来认识一下构成这个生态系统的具体“砖瓦”。
Wayland 通过一系列 XML 文件,用一种结构化的方式定义了通信的细节。下面是一个摘录的片段:
<interface name="wl_display" version="1">
<description summary="core global object">
The core global object. This is a special singleton object. It
is used for internal Wayland protocol features.
</description>
<request name="sync">
<description summary="asynchronous roundtrip">
The sync request asks the server to emit the 'done' event
on the returned wl_callback object. Since requests are
handled in-order and events are delivered in-order, this can
be used as a barrier to ensure all previous requests and the
resulting events have been handled.
The object returned by this request will be destroyed by the
compositor after the callback is fired and as such the client must not
attempt to use it after that point.
The callback_data passed in the callback is undefined and should be ignored.
</description>
<arg name="callback" type="new_id" interface="wl_callback"
summary="callback object for the sync request"/>
</request>
<request name="get_registry">
<description summary="get global registry object">
This request creates a registry object that allows the client
to list and bind the global objects available from the
compositor.
It should be noted that the server side resources consumed in
response to a get_registry request can only be released when the
client disconnects, not when the client side proxy is destroyed.
Therefore, clients should invoke get_registry as infrequently as
possible to avoid wasting memory.
</description>
<arg name="registry" type="new_id" interface="wl_registry"
summary="global registry object"/>
</request>
<event name="error">
<description summary="fatal error event">
The error event is sent out when a fatal (non-recoverable)
error has occurred. The object_id argument is the object
where the error occurred, most often in response to a request
to that object. The code identifies the error and is defined
by the object interface. As such, each interface defines its
own set of error codes. The message is a brief description
of the error, for (debugging) convenience.
</description>
<arg name="object_id" type="object" summary="object where the error occurred"/>
<arg name="code" type="uint" summary="error code"/>
<arg name="message" type="string" summary="error description"/>
</event>
<enum name="error">
<description summary="global error values">
These errors are global and can be emitted in response to any
server request.
</description>
<entry name="invalid_object" value="0"
summary="server couldn't find object"/>
<entry name="invalid_method" value="1"
summary="method doesn't exist on the specified interface or malformed request"/>
<entry name="no_memory" value="2"
summary="server is out of memory"/>
<entry name="implementation" value="3"
summary="implementation error in compositor"/>
</enum>
<event name="delete_id">
<description summary="acknowledge object ID deletion">
This event is used internally by the object ID management
logic. When a client deletes an object that it had created,
the server will send this event to acknowledge that it has
seen the delete request. When the client receives this event,
it will know that it can safely reuse the object ID.
</description>
<arg name="id" type="uint" summary="deleted object ID"/>
</event>
</interface>
这些 XML 文件是给人读的,也是给机器读的。一个名为 wayland-scanner 的工具会读取这些 XML,并自动生成 C 语言的头文件和胶水代码,供 libwayland 和应用程序使用。
libwayland 又包含两个部分:
需要注意的是,libwayland 自身不包含任何图形逻辑。它不知道什么是像素,什么是窗口,什么是 OpenGL。它只是一个纯粹的、轻量级的 IPC(进程间通信)库,严格按照协议规范来打包和解包数据。
协议的实现者才是是真正干活的那个,也就是 Compositor 的实现。不同的桌面环境有不同的 Compositor 实现,它们共享同一个协议,但内部实现和提供的用户体验千差万别。
开发一个完美的 Compositor 并不容易,Compositor 作为系统唯一的图形和输入管理者,需要直接与操作系统内核提供的底层接口打交道。
协议的使用者,是我们日常运行的各种应用程序。极少数应用会直接链接 libwayland-client 并手动处理 Wayland 对象,绝大多数应用都是通过高级的 GUI 工具包来与 Wayland 交互的。例如:
一般情况下,应用程序不需要修改就能运行在 X11 或 Wayland 环境下,但凡是都有例外,比如在代码中直接使用了 X11 的 API。为了获得底层的消息和事件,很多应用程序都这么干。这也导致应用程序在迁移到 Wayland 时会出现各种水土不服的问题。
libwayland 的源码可以访问:https://gitlab.freedesktop.org/wayland/wayland。由于 wayland 代码在不断的进化中,为了和本文的分析能对应上,也为了 DeepWiki 能够分析,我在 github 上 fork 了一份代码:https://github.com/mogoweb/wayland。
Wayland 的架构本质上是协议驱动的,这意味着整个系统围绕 XML 协议定义构建,从而驱动自动代码生成。这种方法确保了客户端和合成器之间的类型安全通信,同时保持了协议版本控制和可扩展性。
Wayland 系统通过定义明确的流水线将 XML 协议规范转换为可执行的 C 代码:
在 protocol 文件夹下,有两个比较重要的文件,也就是上图中的 wayland.xml 和 wayland.dtd。
在上图中,还有扩展协议,这一部分的内容并不在这个 git 库中,而是在 https://gitlab.freedesktop.org/wayland/wayland-protocols 仓库中。
由于核心协议 wayland.xml 非常精简,几乎所有我们认为理所当然的桌面功能都是通过扩展协议来实现的。扩展协议为 Wayland 添加额外的、非核心的功能。这使得 Wayland 保持了高度的模块化和灵活性。合成器可以只实现它所需要的功能。每一个扩展协议都是一个独立的 XML 文件,其语法同样必须遵循 wayland.dtd。在实现 Compoistor 时需要实现哪个扩展特性,可以查看其对应的 xml 定义文件,这里就不展开。
Wayland 使用一套完善的构建系统,可根据 XML 规范生成协议实现代码。wayland-scanner 工具是此过程的核心,它将协议定义转换为类型安全的 C API。
该系统支持多种输出模式,适用于不同的用例:
此架构确保协议变更自动反映在实现中,从而保持规范与代码之间的一致性,同时为协议交互提供编译时类型安全。
合成器(服务器)端以 wl_display 对象及其相关子系统为中心,wl_display 结构定义如下:
struct wl_display {
struct wl_event_loop *loop;
bool run;
uint32_t next_global_name;
uint32_t serial;
struct wl_list registry_resource_list;
struct wl_list global_list;
struct wl_list socket_list;
struct wl_list client_list;
struct wl_list protocol_loggers;
struct wl_priv_signal destroy_signal;
struct wl_priv_signal create_client_signal;
struct wl_array additional_shm_formats;
wl_display_global_filter_func_t global_filter;
void *global_filter_data;
int terminate_efd;
struct wl_event_source *term_source;
size_t max_buffer_size;
};
其中有一个重要的结构 wl_event_loop 和 wl_connection:
struct wl_event_loop {
int epoll_fd;
struct wl_list check_list;
struct wl_list idle_list;
struct wl_list destroy_list;
struct wl_priv_signal destroy_signal;
struct wl_timer_heap timers;
};
struct wl_connection {
struct wl_ring_buffer in, out;
struct wl_ring_buffer fds_in, fds_out;
int fd;
int want_flush;
};
这三者之间的关系,可以用一个比喻来概括:
wl_connection 是这三者中最低级别的抽象。
简单来说,wl_connection 只关心字节流的发送、接收和正确分包。
wl_display 是客户端应用程序与之交互的主要入口点,也是最高级别的对象。
简而言之,wl_display 赋予了 wl_connection 传来的原始消息以“意义”,并将它们路由到正确的处理程序。
wl_event_loop 解决了“何时”去检查新事件的问题。如果没有它,你的程序就得在一个死循环里不停地调用 wl_display_dispatch(),这会消耗 100% 的 CPU。
简单来说,wl_event_loop 让你的程序可以“睡眠”,直到有事情发生时才被唤醒,从而高效地处理事件。
这三者协同工作的完整流程:
+---------------------------------+
| 你的应用程序代码 |
| (注册监听器, 发送请求) |
+---------------------------------+
| wl_display_roundtrip()
| wl_surface_attach()
V
+---------------------------------+ <-- 主要交互层
| wl_display |
| (对象管理, 事件分发) |
| |
| 内部持有: | ----> (通过 wl_display_get_fd() 关联) ----+
| +---------------------------+ | |
| | wl_connection | | |
| | (序列化/反序列化, 读/写) | | |
| +---------------------------+ | |
| | (拥有 Socket FD) | V
| V | +------------------------+
+---------------------------------+ | wl_event_loop |
| | (等待 FD 事件, 调用回调) |
V +------------------------+
+---------------------------------+
| Unix Socket (OS 内核) |
+---------------------------------+
| <---- 网络/IPC ----> |
+---------------------------------+
| Wayland Compositor |
+---------------------------------+
一个典型的 Wayland 客户端(应用程序)遵循一个清晰的、事件驱动的生命周期。可以将其分为以下几个核心步骤:
1. 连接 (Connection)
2. 创建对象 (Object Creation)
3. 渲染与更新 (Rendering & Updating) - "Attach & Commit" 模型
4. 事件循环与处理 (Event Loop & Handling)
5. 清理与断开 (Cleanup & Disconnection)
Wayland 使用二进制线路协议实现客户端和合成器之间的高效通信:
Wayland 的消息编组(Marshalling)和传输是其高性能和强类型安全设计的核心。
Wayland 的设计哲学是:协议规范(XML 文件)应该能被机器读取并自动生成代码。它摒弃了像 X11 那样复杂的、可扩展的文本协议,转而使用一种紧凑、高效的二进制协议。
这个过程可以分为两大部分:
假设客户端要创建一个新窗口表面:
1. 开发者调用: struct wl_surface *surface = wl_compositor_create_surface(compositor);
2. 编组 (Marshalling): libwayland-client 库的内部实现执行以下操作:
3. 传输 (Transport):
4. 完成: 合成器在内部创建一个代表新窗口表面的资源,并将其与 ID 12 关联起来,等待客户端的后续操作(如附加缓冲区和提交)。
通过这个编组 -> 二进制传输 -> 解组的流程,Wayland 实现了高效、类型安全且低延迟的通信。
通过前面的分析,我们会发现,Wayland 核心协议(wayland.xml)本身对窗口管理、输入/输出策略等高级功能几乎没有定义。
它完全没有规定:
Wayland 将所有这些“策略”性的决策权都下放给了 Wayland Compositor(合成器)。这意味着:
这种设计有些优点:
这种灵活性也带来了一个显而易见的问题:如果每个合成器都各自为政,应用程序如何知道怎样与它们交互以实现基本功能(如创建一个正常的窗口)?
为了解决这个问题,社区(主要通过 freedesktop.org)制定了一系列被广泛接受的扩展协议,以提供跨桌面环境的通用功能。其中最重要的就是:
前面说过,扩展协议定义在 wayland-protocols 仓库中,查看一下 wayland-protocols 代码结构,就可以看出,扩展协议可太多了。
目前稳定的协议只有 5 个,大部分都处在 staging 和 unstable 阶段。这会带来一个新的问题,客户端或服务器端对扩展协议支持不完善,甚至支持的版本不同,两者之间如何和谐相处?
为此,wayland 设计了一套机制来处理客户端和服务器端不匹配的情况,确保它们能够“和谐相处”。这个机制的核心就是 wl_registry 和明确的版本协商规则。
1. 核心机制:wl_registry 服务发现
当一个 Wayland 客户端连接到合成器时,它做的第一件事就是获取 wl_registry 对象并监听它的事件。wl_registry 就像一个“服务黄页”,合成器通过它来广播自己支持的所有功能(全局接口)。
2. 客户端的响应与协商
客户端会监听这些 global 事件。当它看到自己需要或认识的接口时,它会决定是否要使用它。
3. 和谐相处的关键规则:版本协商
现在,最关键的一步发生了。当 wl_registry_bind() 被调用时,客户端和服务器会根据以下简单而明确的规则,就最终使用的协议版本达成一致:
最终使用的版本 = MIN (服务器支持的最高版本, 客户端请求的版本)
这个规则完美地解决了版本不匹配的问题:
场景一:服务器版本更高
场景二:客户端版本更高
4. 客户端如何处理协商结果?
协商完成后,客户端必须根据实际得到的版本来调整自己的行为。这通常有三种策略:
a) 功能优雅降级 (Graceful Degradation)
这是最常见和最理想的情况。
b) 硬性要求与优雅退出 (Hard Requirement & Graceful Exit)
有时候,一个扩展协议或它的某个最低版本对于应用程序是绝对必要的。
c) 完全不支持 (No Support)
虽然这种协商机制允许服务器和客户端以不同的速度演进,但很明显,最终实现效果非常依赖客户端和服务器端的实现,导致不同实现(不同桌面环境)在视觉效果和交互体验上差异很大,甚至无法工作(比如协商结果是 No Support)。
在 Linux 社区,由于缺乏一个强有力的领导者,各自为战,导致了严重的碎片化。GNOME 的 Mutter 和 KDE 的 KWin 在功能实现上差异很大,很多应用在一个桌面环境下能用,在另一个环境下就有问题。
虽然 Wayland 面临着挑战,总的来说,Wayland 的前景非常光明,它已经越过了最艰难的“引爆点”,正在成为 Linux 桌面的既定未来标准。但这个过程确实比许多人最初预期的要漫长和复杂。
市场已经围绕 GNOME 和 KDE 这两大实现 形成了强大的向心力,它们为整个生态系统设定了基准。
虽然路途坎坷,但方向已定。Wayland 不是会不会成功的问题,而是还需要多长时间来彻底完善并取代 X11 在所有角落的存在。目前看来,这一天已经不远了。
如果你坚持看到这里了,那一定是这个领域的专家。我个人接触这块工作的时间并不长,很多资料都是查询 AI 得到的,希望 AI 没有胡说八道,如果有什么疏漏之处,还请指点一二。