直接渲染管理器(,通常缩写为DRM ) 是Linux 内核的一个子系统,负责与现代显卡的GPU接口。 DRM 向用户空间公开了API ,程序可以使用该 API 向 GPU 发送命令和数据并执行配置显示器模式设置等操作。 DRM 最初是作为X Server直接渲染基础设施的内核空间组件开发的,但从那时起,它已被其他图形堆栈替代方案(例如Wayland)以及独立应用程序和库(例如SDL2和Kodi)使用。
用户空间程序可以使用DRM API来命令GPU进行硬件加速的3D渲染和视频解码,以及GPGPU计算。
概述
Linux 内核已经有一个名为fbdev的API ,用于管理图形适配器的帧缓冲区 ,但它无法满足现代 GPU 的 3D 加速需求。此类设备通常需要在专用的显存中设置和管理命令队列,并将命令分派到 GPU,并且还需要管理该内存中的缓冲区和可用空间。 最初,用户空间程序(例如X Server)直接管理这些资源,但它们通常假定它们是唯一访问这些资源的程序。当两个或多个程序试图同时控制同一硬件并以自己的方式配置其资源时,大多数情况下都会导致灾难性的后果。
DRM 旨在允许多个程序协作使用视频硬件资源。 DRM 获得对 GPU 的独占访问权限,并负责初始化和维护命令队列、内存和任何其他硬件资源。希望使用 GPU 的程序向 DRM 发送请求,DRM 充当仲裁者并避免冲突。
多年来,DRM 的范围不断扩大,涵盖了以前由用户空间程序处理的更多功能,例如帧缓冲区管理和模式设置、内存共享对象和内存同步。 其中一些扩展被赋予了特定的名称,例如图形执行管理器(GEM)或内核模式设置(KMS),并且当它们提供的功能被特别提及时,术语将占主导地位。但它们实际上是整个内核 DRM 子系统的一部分。
在计算机中包含双 GPU(一个独立 GPU 和一个集成 GPU )的趋势导致了GPU 切换等新问题,这些问题也需要在 DRM 层解决。为了匹配Nvidia Optimus技术,DRM提供了GPU卸载能力,称为PRIME。
开发管理
DRM在Linux内核内部开发,其源代码位于 Linux 源码的 /drivers/gpu/drm 目录下。该子系统的维护者是 Dave Airlie,另有其他维护者负责管理特定的驱动程序。
遵循 Linux 内核开发的惯例,DRM 子系统的共同维护者和贡献者会将包含新功能和错误修复的补丁发送给 DRM 主维护者,由主维护者将其集成到自己的 Linux 代码仓库中。每当新版本的 Linux 即将发布时,DRM 主维护者会将所有已准备好被合入主线(mainline)的补丁提交给 Linus Torvalds。作为整个内核的最高维护者,Torvalds 对某个补丁是否适合包含在内核中拥有最终决定权。
由于历史原因,libdrm 库的源代码是在 Mesa 项目的框架下进行维护的。
发展历史
1999 年,在为 XFree86 开发 DRI 的过程中,Precision Insight 公司为 3dfx 显卡创建了第一个版本的 DRM,当时是以 Linux 内核补丁的形式包含在 Mesa 源码中。同年晚些时候,DRM 代码被合入 Linux 内核 2.3.18 的主线,位于 /drivers/char/drm/ 目录下(用于字符设备)。在接下来的几年里,所支持的显卡数量不断增长。到 2001 年 1 月 Linux 2.4.0 发布时,除了 3dfx Voodoo3 显卡之外,已经支持了 Creative Labs GMX 2000、Intel i810、Matrox G200/G400 和 ATI Rage 128,而在 2.4.x 系列期间,这一列表进一步扩大,新增了 ATI Radeon 显卡、部分 SiS 显卡以及 Intel 830M 及后续的集成 GPU。
2004 年下半年,DRM 被拆分为两个组件——DRM 核心和 DRM 驱动,这一拆分被称为 DRM 核心/个性化拆分,并于内核版本 2.6.11 中合入。此次拆分允许多个设备的多个 DRM 驱动同时工作,为多 GPU 支持铺平了道路。
将所有的视频模式设置代码集中放在内核中的一个地方这一想法,多年来已得到广泛认可,但显卡制造商一直主张,进行模式设置的唯一方法是使用他们自己提供的、包含在每块显卡的视频 BIOS 中的例程。这些代码必须使用 x86 实模式执行,这阻止了在保护模式下运行的内核调用它们。当 Luc Verhaegen 和其他开发者找到了一种基于原生实现而非 BIOS 调用来完成模式设置的方法时,情况发生了变化,这证明使用普通内核代码进行模式设置是可行的,并为后来成为内核模式设置(KMS)的技术奠定了基础。2007 年 5 月,Jesse Barnes(英特尔)发布了第一个 drm-modesetting API 提案,以及 i915 DRM 驱动中为英特尔 GPU 实现的原生模式设置的工作实现。2007 年 12 月,Jerome Glisse 开始为 ATI 显卡向 radeon DRM 驱动中添加原生模式设置代码。2008 年期间,API 和驱动的开发工作持续进行,但由于内核空间中也必须有一个内存管理器来处理帧缓冲区,开发工作被推迟了。
2008 年 10 月,Linux 内核 2.6.27 在若干重大变更到来之前,进行了一次重大的源代码重组。DRM 源代码树被移动到了自己的源码目录 /drivers/gpu/drm/,各个驱动被移入了各自的子目录。头文件也被移到了一个新的 /include/drm 目录下。
显存管理日益增长的复杂性导致了解决该问题的若干种方法。第一次尝试是由 Thomas Hellstrom(Tungsten Graphics)与 Emma Anholt(英特尔)和 Dave Airlie(Red Hat)合作开发的转换表映射(TTM)内存管理器。 TTM 于 2007 年 11 月被提议合入主线内核 2.6.25,并于 2008 年 5 月再次提议,但最终被放弃,转而采用一种称为图形执行管理器(GEM)的新方法。GEM 最初由英特尔的 Keith Packard 和 Emma Anholt 开发,作为其 i915 驱动的一种更简单的内存管理解决方案。GEM 受到广泛欢迎,并被合入 2008 年 12 月发布的 Linux 内核版本 2.6.28。与此同时,TTM 不得不等到 2009 年 9 月,才作为新款 Radeon KMS DRM 驱动的需求,最终被合入 Linux 2.6.31。
随着内存管理就位以处理缓冲对象,DRM 开发者终于可以将在内核中已经完成开发的 API 和代码添加进去,以实现模式设置。这个扩展后的 API 被称为内核模式设置(KMS),而实现它的驱动通常被称为 KMS 驱动。2009 年 3 月,KMS 与 i915 驱动的 KMS 支持一同被合入 Linux 内核版本 2.6.29。自 libdrm 2.4.3 版本起,KMS API 已暴露给用户空间程序。针对英特尔显卡的用户空间 X.Org DDX 驱动也是首个使用新 GEM 和 KMS API 的驱动。对 radeon DRM 驱动的 KMS 支持被添加到 2009 年 9 月发布的 Linux 2.6.31 中。新款 radeon KMS 驱动使用了 TTM 内存管理器,但对外暴露的是与 GEM 兼容的接口和 ioctl,而非 TTM 的原生接口。
自 2006 年起,nouveau 项目一直在官方 Linux 内核之外为 NVIDIA GPU 开发自由软件 DRM 驱动。2010 年,nouveau 源码作为实验性驱动被合入 Linux 2.6.33。在合并时,该驱动已经完成了向 KMS 的转换,并且在 GEM API 背后使用 TTM 作为其内存管理器。
新的 KMS API——包括 GEM API——是 DRM 发展过程中的一个重要里程碑,但这并未阻止 API 在后续几年中持续增强。KMS 在 Linux 2.6.33 中增加了对页面翻转配合异步 VBLANK 通知的支持,但当时仅限 i915 驱动;radeon 和 nouveau 后来在 Linux 2.6.38 发布期间添加了该支持。新的页面翻转接口被添加到 libdrm 2.4.17 中。2011 年初,在 Linux 2.6.39 发布周期期间,所谓的“哑缓冲”(dumb buffers)被添加到 KMS API 中。这是一种与硬件无关的非加速方式,用于处理适合用作帧缓冲区的简单缓冲区。其目标是降低诸如 Plymouth 这类应用程序的复杂性,它们不需要使用由驱动特定 ioctl 提供的特殊加速操作。该特性自 libdrm 2.4.25 版本起被暴露出来。同年晚些时候,KMS API 还增加了一种新的主要对象类型,称为平面(plane)。平面是为表示扫描输出引擎支持的硬件叠加层而开发的。对平面的支持被合入 Linux 3.3 和 libdrm 2.4.30。另一个添加到 API 中的概念——在 Linux 3.5 和 libdrm 2.4.36发布期间——是通用对象属性(generic object properties),这是一种向任何 KMS 对象添加通用值的方法。属性对于为 CRTC 和平面等对象设置特殊行为或功能尤为有用。
2010 年,Dave Airlie 开发了一个在 DRM 驱动之间提供 GPU 卸载功能的早期概念验证。由于 Airlie 试图模仿 NVIDIA Optimus 技术,他决定将其命名为“PRIME”。Airlie 于 2011 年底恢复了 PRIME 的开发工作,但这次基于 Linux 内核 3.3 引入的新 DMA-BUF 缓冲区共享机制。基本的 DMA-BUF PRIME 架构于 2012 年 3 月完成,并被合入 Linux 3.4 版本,同时也被合入 libdrm 2.4.34。随后在 Linux 3.5 发布期间,多个 DRM 驱动实现了 PRIME 支持,包括适用于英特尔显卡的 i915、适用于 AMD 显卡的 radeon 以及适用于 NVIDIA 显卡的 nouveau。
近年来,DRM API 通过新增和改进的特性逐步扩展。2013 年,作为 GSoC 的一部分,David Herrmann 开发了多渲染节点特性。他的代码作为实验性特性被添加到 Linux 内核版本 3.12 中,得到了 i915、radeon和nouveau驱动的支持,并自 Linux 3.17 起默认启用。2014 年,Matt Roper(英特尔)开发了通用平面(或称统一平面)概念,根据这一概念,帧缓冲区(主平面)、叠加层(次级平面)和光标(光标平面)都被视为单一类型的对象,使用统一的 API 进行管理。通用平面支持提供了更一致的 DRM API,具有更少、更通用的 ioctl。为了保持 API 的向后兼容性,该特性由 DRM 核心作为一个附加能力暴露出来,供 DRM 驱动选择提供。通用平面支持首次亮相于 Linux 3.15 和 libdrm 2.4.55。已有多个驱动(如 Intel i915)实现了该特性。
最新的 DRM API 增强是原子模式设置 API,它为 DRM 设备上的模式设置和页面翻转操作带来了原子性。原子模式设置 API 的想法最早于 2012 年初提出。Ville Syrjälä(英特尔)接手了设计和实现该原子 API 的任务。基于他的工作,Rob Clark(德州仪器)采用了类似的方法,目标是实现原子页面翻转。后来在 2013 年,这两个提议的特性被合并为一个,使用单个 ioctl 来完成两项任务。由于这是一个需求特性,它不得不等待通用平面的支持在 2014 年年中被合入。2014 年下半年,Daniel Vetter(英特尔)和其他 DRM 开发者对原子代码进行了大幅增强,以方便现有的 KMS 驱动向新的原子框架过渡。所有这些工作最终被合入 Linux 3.19 和 Linux 4.0 版本,并自 Linux 4.2 起默认启用。libdrm 自 2.4.62 版本起暴露了新的原子 API。已有多个驱动完成了向新原子 API 的转换。到 2018 年,已有十个基于这种新原子模型的新 DRM 驱动被添加到 Linux 内核中。
软件架构
DRM运行在内核空间,因此用户空间程序必须使用内核系统调用来请求其服务。然而,DRM并未定义其自身的定制系统调用。相反,它遵循 Unix 的“一切皆文件”原则,通过文件系统命名空间暴露 GPU,在 /dev 层次结构下创建设备文件。DRM 检测到的每个 GPU 称为一个 DRM 设备,并会创建一个设备文件 /dev/dri/cardX (其中 X 是一个顺序编号) 用于与之交互。希望与 GPU 对话的用户空间程序必须打开此文件并使用 ioctl 调用与 DRM 进行通信。不同的 ioctl 调用对应于 DRM API 的不同功能。
为了方便用户空间程序与 DRM 子系统进行交互,创建了一个名为 libdrm 的库。该库仅仅是一个包装器,为 DRM API 的每个 ioctl 调用提供一个编写在 C 中的函数,以及常量、结构体和其他辅助元素。使用 libdrm 不仅避免了直接向应用程序暴露内核接口,还提供了接口复用和共享代码的常见优势。
DRM 由两部分组成:一个通用的“DRM 核心”和一个针对每种受支持硬件的特定部分(“DRM 驱动”)。DRM 核心提供基本框架,不同 DRM 驱动可以在其中注册,并向用户空间提供一组基本的 ioctl 调用,这些调用具有通用且与硬件无关的功能。另一方面,DRM 驱动实现硬件相关的 API 的特定部分,对应于其支持的 GPU 类型;它应提供未被 DRM 核心涵盖的其余 ioctl 调用,但也可以扩展 API,提供仅在该硬件上可用的额外 ioctl 调用。当特定的 DRM 驱动提供增强的 API 时,用户空间 libdrm 也会被扩展为一个额外的库 libdrm-driver,用户空间可以使用它来与额外的 ioctl 调用进行交互。
API
DRM 核心向用户空间应用程序导出多个接口,通常通过相应的 libdrm 包装函数使用。此外,驱动程序还会通过 ioctl 调用和 sysfs 文件导出设备特定的接口,供用户空间驱动程序和设备感知应用程序使用。
外部接口包括:
- 内存映射: 允许用户空间程序直接访问 GPU 内存。
- 上下文管理: 管理 GPU 的状态和配置。
- DMA 操作: 允许直接内存访问,无需 CPU 干预。
- AGP 管理: 管理显存的地址空间。
- VBlank 控制: 控制垂直同步,防止画面撕裂。
- 栅栏管理: 用于同步不同操作之间的执行顺序,确保数据一致性。
- 内存管理: 管理 GPU 内存的分配和释放。
- 输出管理: 管理显示器的输出设置。
DRM-Master 和 DRM-Auth
DRM API 中存在一些操作(ioctl 调用),出于安全或并发问题,必须限制每个 DRM 设备仅允许单个用户空间进程使用。为了实现此限制,DRM 将这些受限的 ioctl 调用限制为仅由 DRM 设备“主”进程调用。通常,所有打开了设备节点 /dev/dri/cardX 的进程中,第一个调用 SET_MASTER ioctl 的进程的文件句柄会被标记为主。任何没有作为 DRM-Master 调用这些受限 ioctl 的尝试都会返回错误。一个进程也可以通过调用 DROP_MASTER ioctl 放弃其主角色,并允许其他进程获取它。
X Server(或任何其他显示服务器)通常是管理每个 DRM 设备时获取 DRM-Master 状态的进程,并在启动时打开相应的设备节点,并在图形会话结束或崩溃之前保留这些权限。
对于剩余的用户空间进程,有一种其他方法可以获得在 DRM 设备上执行某些受限操作的权限,称为 DRM-Auth。它本质上是一种针对 DRM 设备的身份验证方法,用于证明该进程已获得 DRM-Master 的批准,以获取这些权限。该过程包括:
- 客户端通过 GET_MAGIC ioctl 从 DRM 设备获取一个唯一的令牌(一个 32 位整数),并通过某种方式(通常是某种形式的 IPC;例如,在 DRI2 中,存在一个 DRI2Authenticate 请求,任何 X 客户端都可以发送到 X Server)将其传递给 DRM-Master 进程。
- DRM-Master 进程随后通过调用 AUTH_MAGIC ioctl 将令牌发送回 DRM 设备。
- 设备授予与 DRM-Master 收到的令牌匹配的进程文件句柄特殊的权限。
Graphics Execution Manager (GEM)
随着现代GPU显存容量不断增大,以及OpenGL等图形 API 日益复杂,若在每个上下文切换时都重新初始化显卡状态,其性能开销已变得过高。此外,现代 Linux 桌面需要一种优化共享屏幕外缓冲区与合成管理器的方式。这些需求导致了在内核中管理图形缓冲区的新的方法的开发。Graphics Execution Manager (GEM) 就是这些方法之一。
GEM 提供了一个带有显式内存管理原语的 API。通过 GEM,用户空间程序可以创建、处理和销毁存放在显存的内存对象。这些对象,称为“GEM 对象”,从用户空间的观点来看是持久的,并且不必每次程序重新获得 GPU 控制时都重新加载。当用户空间程序需要一块视频内存(用于存储帧缓冲、纹理或 GPU 所需的任何其他数据)时,它会请求通过 GEM API 将其分配给 DRM 驱动程序。DRM 驱动程序跟踪使用的显存,如果可用空间有空闲,则可以满足请求,并将“句柄”返回给用户空间,以便进一步引用分配的内存进行后续操作。GEM API 还提供用于填充缓冲区和不再需要时释放它的操作。在用户空间进程关闭 DRM 设备文件描述符时(有意或由于终止),从未释放的 GEM 句柄的内存将被回收。
GEM 还允许两个或多个使用同一 DRM 设备(因此,相同的 DRM 驱动程序)的用户空间进程共享一个 GEM 对象。GEM 句柄是进程本地的 32 位整数,在其他进程中不具备相同的含义,因此无法直接用于跨进程共享。为此需要全局命名空间,而 GEM 通过引入全局句柄(即 GEM 名称)来满足这一需求。GEM 名称是一个唯一的 32 位整数,用于在相同 DRM 设备内部标识由同一驱动程序创建的单个 GEM 对象。GEM 提供了一个 flink 操作来从 GEM 句柄获取一个 GEM 名称。然后进程可以将此 GEM 名称(32 位整数)传递给另一个进程,可以使用任何可用的 IPC 机制。接收进程可以使用此 GEM 名称来获取指向原始 GEM 对象的本地 GEM 句柄。
不幸的是,使用 GEM 名称共享缓冲区并不安全。恶意第三方进程可以尝试猜测两个其他进程共享的缓冲区的 GEM 名称,只需探测 32 位整数即可。一旦找到 GEM 名称,其内容就可以被访问和修改,从而违反了缓冲区信息的机密性和完整性。这个缺点后来通过在 DRM 中引入 DMA-BUF 支持来克服,因为 DMA-BUF 将用户空间中的缓冲区表示为文件描述符,这些缓冲区可以安全地共享。
除了管理视频内存空间之外,另一个重要的任务是处理 GPU 与 CPU 之间的内存同步。当前的内存架构非常复杂,通常涉及系统内存和有时视频内存的多个级别缓存。因此,视频内存管理器还应处理缓存一致性,以确保 CPU 和 GPU 之间共享的数据的一致性。这意味着通常视频内存管理内部依赖于 GPU 和内存架构的硬件细节,因此是驱动程序特定的。
GEM 最初由英特尔工程师开发,旨在为其 i915 驱动提供显存管理功能。英特尔 GMA 9xx 系列是采用统一内存架构(UMA)的集成 GPU,其 GPU 与 CPU 共享物理内存,没有独立的显存(VRAM)。 GEM 定义了用于内存同步的“内存域”,尽管这些内存域是与 GPU 无关的抽象概念,但它们的设计初衷和最佳适配场景都是 UMA 架构,因此不太适合其他内存架构(例如配备独立显存的方案)。正因如此,其他 DRM 驱动虽然向用户态程序暴露了 GEM API,但在内部却实现了各自更适合其特定硬件和内存架构的内存管理器。
GEM API 也包含用于控制执行流(即命令缓冲区)的 ioctl,但它们属于英特尔私有接口,只能配合 Intel i915 及更新的 GPU 使用。事实上,除了内存管理相关的 ioctl 之外,没有任何其他 DRM 驱动曾尝试实现 GEM API 的其他功能。
TTM(转换表映射)
TTM(Translation Table Maps)是 GEM 之前开发的通用 GPU 内存管理器。它专门设计用于管理 GPU 可能访问的多种内存类型,包括独立的显存(通常安装于显卡上)以及通过名为 GART(图形地址重映射表)的 IOMMU 可访问的系统内存。TTM 还需要处理 CPU 无法直接寻址的那部分显存,并在此基础上实现尽可能高的性能——因为用户态图形应用程序通常处理大量视频数据。另一个重要问题是维护所涉及的多种内存与缓存之间的一致性。
TTM 的核心概念是“缓冲对象”(buffer objects),即 GPU 在某一时刻必须能够寻址的显存区域。 当用户态图形应用程序需要访问某个缓冲对象(通常是向其填充内容)时,TTM 可能要求将其重定位到 CPU 可寻址的内存类型上。当 GPU 需要访问某个尚未进入其地址空间的缓冲对象时,可能还会发生进一步的重定位(即 GART 映射操作)。上述每一次重定位操作都必须处理相关的数据及缓存一致性问题。
TTM 的另一个重要概念是栅栏(fences)。栅栏本质上是一种管理 CPU 与 GPU 之间并发性的机制。栅栏用于追踪缓冲对象是否已不再被 GPU 使用,通常是为了通知所有有权访问该对象的用户态进程。
TTM 试图以一种普适的方式管理所有类型的内存架构(无论是否配备独立显存),并试图提供一个涵盖所有可设想功能的、适用于任意硬件类型的内存管理器,这最终导致其解决方案过于复杂,API 也远大于实际所需。一些 DRM 开发者认为它将难以与任何特定驱动(尤其是其 API)良好适配。当 GEM 作为更简单的内存管理器出现时,它的 API 相比 TTM 的 API 更受青睐。然而,部分驱动开发者认为 TTM 所采用的方法更适合配备独立显存和 IOMMU 的独立显卡,因此他们决定在内部使用 TTM,同时将其缓冲对象作为 GEM 对象暴露出来,从而支持 GEM API。当前使用 TTM 作为内部内存管理器但提供 GEM API 的驱动实例包括:用于 AMD 显卡的 radeon 驱动,以及用于 NVIDIA 显卡的 nouveau 驱动。
DMA 缓冲区共享与 PRIME
DMA 缓冲区共享 API(通常缩写为 DMA-BUF)是 Linux 内核内部的一个 API,旨在提供一种通用机制,用于在多个设备(可能由不同类型的设备驱动管理)之间共享 DMA 缓冲区。例如,一个 Video4Linux 设备和一个图形适配器设备可以通过 DMA-BUF 共享缓冲区,从而实现视频流数据的零拷贝:前者产生数据,后者消费数据。任何 Linux 设备驱动都可以作为导出方(exporter)、使用方(consumer)或同时扮演两种角色来实现该 API。
该功能首次在 DRM 中得到了利用,用于实现 PRIME——一种 GPU 卸载方案,它使用 DMA-BUF 在独立显卡和集成显卡的 DRM 驱动之间共享渲染后的帧缓冲区。DMA-BUF 的一个重要特性是:共享缓冲区以文件描述符的形式呈现给用户空间。为实现 PRIME,DRM API 新增了两个 ioctl,一个用于将本地 GEM 句柄转换为 DMA-BUF 文件描述符,另一个用于执行相反的操作。
后来,这两个新增的 ioctl 被重新用于解决 GEM 缓冲区共享固有的不安全问题。与 GEM 名称不同,文件描述符无法被猜测(它们不属于全局命名空间),并且 Unix 操作系统提供了一种安全的方式来通过 Unix 域套接字传递它们,即使用 SCM_RIGHTS 语义。当某个进程希望与另一个进程共享一个 GEM 对象时,它可以将自己的本地 GEM 句柄转换为 DMA-BUF 文件描述符,然后将其传递给接收方,接收方再根据收到的文件描述符获取自己的 GEM 句柄。DRI3 和 Wayland 都使用此方法在客户端与 X Server(或 Wayland 合成器)之间共享缓冲区。
内核模式设置(Kernel Mode Setting)
用户空间中必须存在一个"DRM 主控"程序,该程序对 KMS 拥有独占访问权限。
为了正常工作,显卡或图形适配器必须设置一种模式——即屏幕分辨率、色彩深度和刷新率的组合——该模式需在自身及所连接显示器支持的数值范围之内。此操作称为模式设置(mode-setting),通常需要原始访问图形硬件的能力——即能够写入显卡显示控制器的特定寄存器。在开始使用帧缓冲区之前,以及当应用程序或用户要求更改模式时,都必须执行模式设置操作。
早期,想要使用图形帧缓冲区的用户空间程序也负责提供模式设置操作。因此,它们需要以特权访问权限运行才能操作视频硬件。在 Unix 类操作系统中,X Server 是最典型的例子。其模式设置实现位于针对每种特定显卡类型的 DDX 驱动中。这种后来被称为用户空间模式设置(User space Mode-Setting,简称 UMS)的方法带来了若干问题。它不仅破坏了操作系统本应在程序与硬件之间提供的隔离性,引发了稳定性和安全性方面的担忧,而且如果两个或更多用户空间程序同时尝试进行模式设置,还可能使图形硬件陷入不一致的状态。为避免这些冲突,X Server 实际上成为了唯一执行模式设置操作的用户空间程序;其余用户空间程序则依赖 X Server 来设置合适的模式并处理任何涉及模式设置的其他操作。最初,模式设置仅在 X Server 启动过程中执行,但后来 X Server 获得了在运行时执行模式设置的能力。XFree86 3.1.2 版本引入了 XFree86-VidModeExtension 扩展,允许任何 X 客户端向 X Server 请求更改显示模式(分辨率)。VidMode 扩展后来被更通用的 XRandR 扩展所取代。
然而,在 Linux 系统中执行模式设置的并非只有这一处代码。在系统启动过程中,Linux 内核必须为虚拟控制台设置一个最小文本模式(基于 VESA BIOS 扩展定义的标准模式)。此外,Linux 内核帧缓冲区驱动也包含用于配置帧缓冲区设备的模式设置代码。为避免模式设置冲突,XFree86 Server——以及后来的 X.Org Server——通过在处理用户从图形环境切换到文本虚拟控制台时保存其模式设置状态,并在用户切换回 X 时恢复该状态,来应对这种情况。这一过程在切换时会产生令人烦恼的闪烁,并且也可能失败,导致输出显示损坏或无法使用。
用户空间模式设置方法还引发了其他问题:
- 挂起/恢复过程必须依赖用户空间工具来恢复先前的模式。这些程序中任何一个发生故障或崩溃,都可能因模式设置错误而导致系统无法正常显示,从而使系统不可用。
- 当屏幕处于图形模式时(例如 X 正在运行时),内核无法显示错误或调试消息,因为内核唯一知道的模式只有 VESA BIOS 标准文本模式。
- 一个更紧迫的问题是,绕过 X Server 的图形应用程序大量涌现,以及 X 之外的其他图形栈替代方案的出现,进一步加剧了模式设置代码在整个系统中的重复。
为了解决这些问题,模式设置代码被移到了内核中的一个单独位置,具体来说是现有的 DRM 模块中。此后,每个进程(包括 X Server)都应该能够命令内核执行模式设置操作,而内核将确保并发操作不会导致不一致的状态。添加到 DRM 模块中以执行这些模式设置操作的新内核 API 和代码被称为内核模式设置(Kernel Mode-Setting,简称 KMS)。
内核模式设置带来了诸多好处。最直接的好处当然是消除了来自内核(Linux 控制台、fbdev)和用户空间(X Server DDX 驱动)两方面的重复模式设置代码。KMS 还使得编写替代图形系统变得更加容易,因为现在这些系统不再需要实现自己的模式设置代码。通过提供集中化的模式管理,KMS 解决了在控制台与 X 之间以及不同 X 实例之间切换(快速用户切换)时的闪烁问题。由于 KMS 在内核中可用,它也可以在启动过程的早期阶段使用,从而避免了这些早期阶段因模式切换而产生的闪烁。
KMS 作为内核的一部分,使其能够使用仅在内核空间可用的资源,例如中断。例如,挂起/恢复过程后的模式恢复由内核自身管理,大大简化了流程,并且顺便提高了安全性(不再需要需要 root 权限的用户空间工具)。内核还允许轻松热插拔新的显示设备,解决了一个长期存在的问题。模式设置也与内存管理密切相关——因为帧缓冲区本质上就是内存缓冲区——因此强烈建议与图形内存管理器紧密集成。这正是内核模式设置代码被纳入 DRM 而不是作为一个独立子系统的核心理由。
为了避免破坏 DRM API 的向后兼容性,内核模式设置作为某些 DRM 驱动的附加驱动特性提供。任何 DRM 驱动在向 DRM 核心注册时都可以选择提供 DRIVER_MODESET 标志,以表明其支持 KMS API。那些实现了内核模式设置的驱动通常被称为 KMS 驱动,以区别于传统的(无 KMS)DRM 驱动。
KMS 的普及程度如此之高,以至于某些缺乏 3D 加速能力(或者硬件厂商不愿暴露或实现 3D 加速)的驱动,也实现了 KMS API(即使没有实现 DRM API 的其他部分),从而使得显示服务器(如 Wayland)能够轻松运行。
KMS 设备模型
KMS 将输出设备建模并管理为一系列抽象硬件模块,这些模块通常存在于显示控制器的显示输出流水线中。这些模块包括:
- CRTC:每个 CRTC(CRT Controller 的缩写)代表显示控制器的一个扫描输出引擎,指向一个扫描输出缓冲区(帧缓冲区)。CRTC 的作用是读取扫描输出缓冲区中的当前像素数据,并借助 PLL 电路从中生成视频模式时序信号。可用的 CRTC 数量决定了硬件能够同时处理的独立输出设备数量,因此要使用多头(multi-head)配置,每个显示设备至少需要一个 CRTC。两个(或更多)CRTC 也可以工作在克隆模式下,此时它们从同一个帧缓冲区扫描输出,将相同的图像发送到多个输出设备。
- 连接器(Connector):连接器代表显示控制器将扫描输出操作产生的视频信号发送到何处进行显示。通常,KMS 中的连接器概念对应于硬件中的物理连接器(VGA、DVI、FPD-Link、HDMI、DisplayPort、S-Video 等),输出设备(显示器、笔记本面板等)永久或临时地连接到该连接器上。与当前物理连接的输出设备相关的信息——例如连接状态、EDID 数据、DPMS 状态或支持的视频模式——也存储在连接器内部。
- 编码器(Encoder):显示控制器必须使用适合目标连接器的格式,对来自 CRTC 的视频模式时序信号进行编码。编码器代表能够执行其中一种编码的硬件模块。数字输出的编码示例包括 TMDS 和 LVDS;对于 VGA 和 TV out 等模拟输出,通常使用特定的 DAC 模块。一个连接器一次只能接收来自一个编码器的信号,且每种类型的连接器仅支持某些编码方式。此外,还可能存在额外的物理限制,使得并非每个 CRTC 都连接到每个可用的编码器,从而限制了 CRTC-编码器-连接器的可能组合。
- 平面(Plane):平面并非硬件模块,而是一个内存对象,包含一个缓冲区,供扫描输出引擎(即 CRTC)从中读取数据。存放帧缓冲区的平面称为主平面(primary plane),每个 CRTC 都必须有一个关联的主平面,因为它是 CRTC 确定视频模式——显示分辨率(宽度和高度)、像素大小、像素格式、刷新率等——的数据源。如果显示控制器支持硬件光标叠加层,CRTC 还可能关联光标平面(cursor plane);如果能够从额外的硬件叠加层扫描输出并“实时”合成或混合最终发送到输出设备的图像,则还可能关联次级平面(secondary plane)。
原子显示
近年来,人们一直在努力为 KMS API 的某些常规操作(特别是模式设置和页面翻转操作)引入原子性。这一增强后的 KMS API 被称为原子显示(Atomic Display),曾用名包括原子模式设置(atomic mode-setting)和原子页面翻转(atomic或nuclear pageflip)。
原子模式设置的目的是:在存在多种限制的复杂配置中,通过避免那些可能导致视频状态不一致或无效的中间步骤,来确保模式的正确变更;同时,当某个失败的模式设置过程需要回滚(rollback)时,它也能避免系统进入危险的视频状态。原子模式设置通过提供模式测试能力,允许用户提前判断某特定模式配置是否合适。当一个原子模式经过测试并确认有效后,就可以通过一个不可分割的(原子)提交操作来应用它。测试和提交操作均由同一个新增的 ioctl 通过不同的标志位提供。
另一方面,原子页面翻转允许在同一个输出上更新多个平面(例如主平面、光标平面,以及可能的某些叠加层或次级平面),并使所有这些更新在同一个 VBLANK 间隔内保持同步,从而确保显示正常、无撕裂。这一需求对于倾向于使用多个平面/叠加层来节省电量的移动和嵌入式显示控制器尤为重要。
新的原子 API 构建在旧的 KMS API 之上。它使用相同的模型和对象(CRTC、编码器、连接器、平面等),但可修改的对象属性数量不断增加。原子操作流程基于修改相关属性,以构建我们想要测试或提交的状态。需要修改哪些属性取决于我们是要进行模式设置(主要涉及 CRTC、编码器和连接器的属性)还是页面翻转(通常涉及平面的属性)。两种场景使用同一个 ioctl,区别在于传递给它的属性列表不同。
渲染节点
在原始的 DRM API 中,DRM 设备 /dev/dri/cardX 同时用于特权操作(模式设置、其他显示控制)和非特权操作(渲染、GPGPU 计算)。出于安全原因,打开关联的 DRM 设备文件需要“等同于 root 权限”的特殊特权。这导致了一种架构:只有某些可靠的用户空间程序(如 X Server、图形合成器等)拥有 DRM API 的完整访问权限,包括模式设置 API 等特权部分。其他想要进行渲染或执行 GPGPU 计算的用户空间应用程序,必须通过使用特殊的认证接口,由 DRM 设备的拥有者(即“DRM 主控”)授予权限。随后,被认证的应用程序可以使用受限版本的 DRM API 进行渲染或计算,但无权执行特权操作。这种设计带来了一个严重的限制:系统中必须始终有一个正在运行的图形服务器(X Server、Wayland 合成器等)作为 DRM 设备的 DRM 主控,以便其他用户空间程序能够被授予使用该设备的权限——即使在完全不涉及图形显示的场景(如纯 GPGPU 计算)中也是如此。
“渲染节点”的概念试图通过将 DRM 用户空间 API 拆分为两个接口——一个特权接口和一个非特权接口——并为每个接口使用独立的设备文件(或称“节点”)来解决上述场景。对于找到的每一个 GPU,其对应的 DRM 驱动——如果支持渲染节点特性——除了创建主节点 /dev/dri/cardX 之外,还会创建一个设备文件 /dev/dri/renderDX,称为渲染节点。使用直接渲染模型的客户端,以及希望利用 GPU 计算能力的应用程序,只需打开任意现有的渲染节点,并使用这些节点所支持的 DRM API 受限子集来调度 GPU 操作,即可无需额外特权完成工作——前提是它们拥有打开该设备文件的文件系统权限。显示服务器、合成器以及任何其他需要模式设置 API 或任何特权操作的程序,则必须打开标准的、授予完整 DRM API 访问权限的主节点,并照常使用它。渲染节点明确禁止使用 GEM flink 操作,以防止通过不安全的 GEM 全局名称进行缓冲区共享;只能使用 PRIME(DMA-BUF)文件描述符与另一个客户端(包括图形服务器)共享缓冲区。
另见
*
*
参考
外部链接
- [http://dri.freedesktop.org/wiki/DRM DRM 主页]
- [https://dri.freedesktop.org/docs/drm/gpu/ Linux GPU 驱动程序开发人员指南] (以前称为Linux DRM 开发人员指南)
- 嵌入式 Linux 会议 2013 -
评论 (0)