DRM 和 KMS 内核模块实现

首次发布于:2012 年 10 月 16 日

类别:Linux

介绍

“直接渲染管理器”(DRM)和“内核模式设置”(KMS)API 是 Linux 图形系统的重要组成部分。然而,很难找到关于它们到底是什么的文档——而且谷歌出现的大部分内容已经完全过时了。看来在这个领域工作的人太忙了,无法记录它。

该站点上的一篇文章概述了 linux 图形堆栈本文提供了有关 DRM 内核模块内部实现细节的更多详细信息。

您可能还希望阅读我关于显卡的文章,以大致了解现代显卡的工作原理。

更新:我找到了一篇2012 年的DRM Developers Guide文章,其中对内部 DRM 细节进行了非常详细的讨论,并且比这篇文章更透彻!

本文中的代码讨论基于通过 PCIe 连接的 ATI Radeon 显卡的“radeon.ko”驱动程序(因为这是我笔记本电脑中的内容)。然而,一般原则应该适用于许多卡,至少部分适用于嵌入式图形设备以及 PCIe 设备。

请注意,我不是这方面的专家,现有的文档很糟糕。下面会有错误和误解——可能是重大的欢迎指正。这也是一篇正在进行中的文章,希望随着时间的推移会有所改进。

DRI、DRM 和 KMS 的目的

一开始,所有的图形都是通过X server 1完成的;每个卡都有一个不同的“DDX”用户空间驱动程序,它通过对 /dev/mem “文件”进行 MMAP 系统调用,然后在映射范围内读取/写入地址来映射视频卡的共享缓冲区。驱动程序负责支持 X11 绘图 API(传统的 2D API 或 X Render 扩展),以及设置图形模式和一般配置图形卡 - 所有这些都没有任何内核端支持。

这种方法的问题在于:

  1. 依赖于 X对显卡的独占访问权;
  2. 效率不高(内核模式代码可以更有效地处理内存映射等);
  3. 将许多关键逻辑放在其他图形系统无法使用的 X 特定驱动程序中。

第 (1) 项是一个主要问题 - 强制希望使用现代卡片的 3D 渲染功能的应用程序通过 X 服务器传递所有数据简直是疯了。X Render 扩展允许客户端应用程序至少传递镶嵌的 2D 形状和字符字形,但这并没有多大帮助。还有一种将 OpenGL 命令传递给 X 的机制,并让 X 代表调用者运行它们(GLX 间接渲染)——但这也相当低效。最后,性能关键型应用程序最好直接访问显卡。然而,需要有一种机制来协调来自多个应用程序的访问。

alt-f{n}即使在旧系统中,假设 (1) 也不完全正确:基于帧缓冲驱动程序的虚拟控制台在按下时也需要访问显卡。如果没有 KMS,X 中有一些丑陋的 hack 来支持这一点,它们仍然不能完全有效地工作,这意味着切换到控制台很慢并且会导致屏幕闪烁。在引导期间从帧缓冲区切换到 X 是另一种情况。

第(2)项也很重要。显卡广泛使用 DMA,但从用户空间驱动它效率不高(特别是处理中断)。做现代图形还需要分配/释放大量内存缓冲区——这在内核中要容易得多。

第 (3) 项阻止了 X 替代方案的研究和实施。虽然 X 是一个经过验证的系统,但它已有数十年的历史;最好允许至少探索替代方案,而不必移植和维护许多 DDX(设备相关 X)图形驱动程序的分支。

DRM(Direct Rendering Manager)内核模块就是为了解决上述问题而开发的。

LibDRM

DRM 内核模块向用户空间公开了一个“不稳定”的 api,不应由应用程序直接使用。相反,可以从 freedesktop.org 获得一个 C 库(并与所有发行版捆绑在一起),它公开了一个稳定的 api。然后从 X、Mesa (opengl) 驱动程序、libva 等中使用这个库。当特性被添加到 drm 驱动程序(例如新的 ioctls)时,会生成相应的 libdrm 版本。

该库支持 Linux 和 BSD,以及可能的其他操作系统。它包含通用代码和特定于卡的代码。然后它编译成一组可动态加载的库,每个支持的卡类型(内核驱动程序)都有一个库,通常可以在 /usr/lib 下找到。例如,在我的系统上,AMD radeon 卡的 libdrm 版本可以在/usr/lib/i386-linux-gnu/libdrm_radeon.so.1.

阅读 libdrm 源代码——尤其是头文件xf86drm.h和——可能是开始理解 DRM 和 KMS 内核实现的最佳方式xf86drmMode.hinclude/drm/libdrm.h

直接渲染管理器 (DRM)

“drm core”模块是适用于所有卡的通用代码;它提供了一组 IOCTL 操作,它可以自行处理或委托给特定于卡的内核模块。

加载时,特定于卡的 drm 帮助模块调用 drm 模块以将自己注册为“drm 驱动程序”,并提供一组 drm 核心模块可以调用的函数指针。然后,“drm 核心”创建一个文件 /dev/dri/card{n},IOCTL 可以在该文件上与驱动程序通信;读取文件还会返回“事件”结构,指示“vblank”或“翻转完成”事件何时发生。

作为注册过程的一部分,“drm 帮助程序模块”提供了一组标志,指示它支持哪些功能,例如 DRIVER_MODESET 指示它支持内核模式设置 (KMS)。当用户空间尝试调用特定于卡的“帮助模块”不支持的功能时,drm 核心将返回错误。

drm “api”(即 libdrm 公开的 api)定义了支持在多个用户空间应用程序之间共享图形卡所需的最小扩展数。仍然需要具有卡特定知识的 X DDX 驱动程序才能访问完整的卡功能。例如,DDX 驱动程序知道卡上的哪些地址范围对于用户空间映射是安全的;驱动程序将此信息传递给 drm 模块,因此以后可以验证来自 X 客户端应用程序的映射请求。这降低了卡特定驱动程序所需的复杂性(这在从完全用户空间 X 驱动程序迁移到 DRM 时特别有用)。

因为“drm”(即核心代码)本身就是一个内核模块,所以有相应的sysfs入口。您会在和/sys/virtual/devices/drm找到它们。DRM 驱动程序声明对 drm 模块的依赖。/sys/class/drm/sys/module/drm

因为 DRM 的内核 API 不直接暴露给用户(而是包装在 libdrm 中),不幸的是,很少有关于 drm 模块本身暴露的 IOCTL 的文档;有必要阅读 libdrm 的源代码以了解这些 ioctl 的实际使用方式。如上所述,了解 libdrm 代码确实是了解 drm 内核端的先决条件。

知识管理系统

DRM 的第一个版本仅支持用户空间图形应用程序拥有的内存与显卡可访问的内存之间的“DMA 样式缓冲区传输”。DRM 功能后来得到显着增强,以支持额外的 ioctl 操作,包括模式设置(KMS) - 只要卡特定的“帮助模块”支持它。

KMS 功能通过 libdrm 访问,与“drm core”内核模块提供的所有其他功能一样。用于通过 libdrm 访问 KMS 功能的函数和常量通常带有drm_前缀。

较新的 DRM 驱动程序还:

  • 提供“缓冲区管理”操作(通常通过 GEM 或 TTM 库),这些操作比 DRM 提供的 DMA 配置 API 的抽象级别更高;

  • 实现一个“modesetting” api,它是通过 framebuffer modesetting ioctl 提供的功能的超集;

  • 实现“帧缓冲”API;

  • 提供一些以前在 X DDX 驱动程序中实现的特定于卡的逻辑(例如 DDX 驱动程序告诉drm 模块用户可以映射哪些内存范围)。

用于设置图形模式的用户 api 在 libdrm 文件 xf86drmMode.h 中定义(特别是函数 drmModeAttachMode 或 drmModeSetCrtc)。这个 api 的内核端是通过名为 DRM_IOCTL_MODE_* 的 ioctl 完成的。本文的部分链接到了一些有用的模式设置示例References

一些特定于卡的逻辑已经从 X 转移到内核这一事实意味着更容易实现 X 的替代方案(例如 Wayland)。缓冲区管理代码在内核中的事实也使得实现 X 的替代方案变得更加容易。

KMS 驱动程序是一个集成的 DRM帧缓冲驱动程序这一事实允许从引导到 X 的平滑图形切换,以及 X 和基于帧缓冲的虚拟控制台之间的平滑/快速切换。

因为一些逻辑从 X 移动到内核(尤其是那些用户空间驱动程序配置 drm 内核模块的那些),相应的 IOCTL 不再有用。因此,KMS 驱动程序提供了这些 API 的实现,这些 API 只返回一个错误代码。

理论上,KMS 也可以在非 Linux 系统上实现。然而,它确实需要移植或重新实现 GEM/TTM 模块,这是一个非常重要的过程。目前(2012 年末)正在努力让 KMS 在 OpenBSD 上运行。

迅达和创业板

GEM 是一个内核模块,它提供了一个用于管理 GPU 内存缓冲区的函数库。目前,它不直接处理卡上内存。

TTM 与 GEM 的目的相同;它更复杂,但更好地处理卡上内存。TTM 还实现了 GEM API,因此用户空间不需要关心当前驱动程序正在使用哪个实现。

DRM 驱动程序通过 IOCTL 操作公开 GEM 接口,目的是操作与驱动程序处理的图形卡关联的缓冲区。

DRM 模块实现

drm 核心的源代码位于drivers/gpu/drm.

可以在drivers/gpu/drm/tdfx(支持 3dfx Voodoo 卡)中找到纯 DRM 驱动程序的示例。另一个在drivers/gpu/drm/sis.

初始化和驱动程序注册

Filedrm_drv.c是 drm 模块的“入口点”:它包含 module_init 声明,指向 drm_core_init()。

drm_drv.c : on init, drm_core_init():

* initialises static var drm_minors_idr to hold an empty mapping of (minor->drm_device)
* creates & registers major-device-number DRM_MAJOR (226) with a very simple file_operations structure
* creates & registers a "class" object with sysfs (drm_class)

特定于卡的 DRM 驱动程序有自己的模块,这些模块定义了它可以处理的设备的 PCI Id 列表,然后调用drm_pci.c:drm_pci_init.

对于“旧”驱动程序,drm_pci_init 手动扫描所有已知的 PCI 设备,并调用drm_get_pci_dev每个匹配的设备。

对于“新”驱动程序,drm_pci_init期望调用者的pci_driver表包含指向“探测”函数的指针;PCI 子系统将为每个匹配的设备调用它,然后探测函数应该调用drm_get_pci_dev.

PCI 子系统从调用者“扫描”到调用者提供“探测”回调的变化似乎与 KMS 的开发几乎同时发生,因此 KMS 驱动程序提供了一种探测方法,而较旧的驱动程序则没有。

drm_pci.c:drm_get_pci_dev:

* allocates a device minor number in range 0..63 (LEGACY), and creates device node `/dev/dri/card{n}`
* optionally allocates a device minor number in range 64..127 (CONTROL), and creates device node `/dev/dri/controlD{n}` which is connected to the modesetting functions only
  (ie userspace code which can open this file can perform mode-setting but not generate graphics; can be helpful for unusual use-cases).
* allocates a drm_device structure, and stores it in the drm_minors_idr map keyed by each of the allocated minor#s.
* calls the "load" function provided by the card-specific driver

两个设备节点都从它们的主要设备(之前创建的)继承file_operations回调drm_core_init;这有一个用于“打开”操作的钩子,它指向drm_stub_open.

当用户打开/dev/dri/card{n}文件(用于所有操作)或/dev/dri/controlD{n}文件(仅用于模式设置)时,函数drm_stub.c:drm_stub_open会传递打开文件的次要#,并将其用作drm_minors_idr表中的索引以获得对适当 drm_device 结构的引用。然后它将这个指针存储到打开的文件结构中,以便将来的文件操作可以立即找到设备特定的信息。

存根打开函数还做了一个神奇的切换:它将file_operations打开文件句柄从DRM_MAJOR设备继承的结构替换为卡特定驱动程序提供的结构。实际上,这绕过了正常的 Unix 行为,其中 major# 引用驱动程序,minor-number 表示同一设备的多个实例,而是允许每个设备 minor# 指向不同的驱动程序。在文件操作切换之后,用户空间执行的所有未来与文件相关的系统调用都直接转到特定于卡的驱动程序。

文件操作

如前一节所述,特定于卡的驱动程序可以提供一个file_operations回调表,当用户对打开的文件句柄执行各种操作时,将调用这些回调表。对于大多数驱动程序,此表中的大多数条目都指向 drm 核心代码提供的常用库函数。

IOCTL

DRM 驱动程序提供的 IOCTL 操作的标准列表可以在表中看到drm_drv.c:drm_ioctls

大多数驱动程序都有一个文件操作结构,它将 ioctl 回调映射到标准的 drm_ioctl 库函数。然后,他们定义了一个额外的自定义 ioctl 操作表并将其存储在 drm_driver 结构中。drm_ioctl 函数使用特定于设备的表转发 COMMAND_BASE..COMMAND_END (0x40-0xA0) 范围内的所有 ioctl 操作,并使用其 ioctls 表自行处理所有其他操作。这确保了所有“标准”DRM ioctl 都在 drm 核心中实现,但特定于卡的驱动程序可以实现其相应的 X DDX 驱动程序可能知道如何使用的附加 ioctl 操作。

除非调用者是 root(即 X 服务器本身),否则主drm_ioctl表中标记为返回错误的条目。DRM_ROOTONLY这允许drm 驱动程序将调用分离为那些仅用于直接绘图的图形客户端应用程序的安全调用,以及那些可用于接管机器的调用(例如DMA 配置)。

除非用于 ioctl 的文件句柄被标记为设备的“主”,否则标记为 DRM_MASTER 的那些会返回错误。预计 X 服务器将是第一个为任何图形设备打开设备节点的应用程序,并将立即调用 SET_MASTER ioctl。这允许它为自己“保留”对某些危险操作的访问。有一个对应的 DROP_MASTER ioctl。使用 DRM_MASTER 标记的操作可能通过将竞争性操作限制为单个“主”进程来防止安全问题和竞争条件。

如前所述,用户空间 API 是在 libdrm 中定义的,而不是在内核级别 - 因此没有正式定义各种 IOCTL 操作的作用(至少我找不到)。找出任何特定 IOCTL 用途的最佳方法是查看 libdrm 源代码(记录得更好一些)。

根据一般 DRI 文档:

  • 有一个“每卡锁同步访问”。这是 LOCK/UNLOCK 操作吗?
  • X 服务器可以指定客户端应用程序允许映射的内存范围 - 这是 ADD_MAP/RM_MAP 操作吗?

DRI 体系结构通常期望客户端要求 X 分配数据缓冲区以与卡共享,并让 X 返回缓冲区的地址。也许 ADD_BUFS/MARK_BUFS 操作就是为了这个?特别是 ADD_BUFS 被标记为 DRM_ROOTONLY(即 X),但 FREE_BUFS 不是(即客户端可以自己释放缓冲区)。

我似乎还记得听说过验证命令流指令以防止系统接管甚至损坏的必要性。这些是??操作?

也许 CTX 操作用于支持多个应用程序同时访问环形缓冲区?

DRM 驱动程序源代码

这些驱动程序的源代码位于drivers/gpu/drm. 这些将自己注册到 drm 核心模块,并且当用户空间打开适当的文件时发生文件操作切换。

对于 AMD 卡,KMS 驱动程序源在drivers/gpu/drm/radeon,编译模块在/lib/modules/{version}/kernel/drivers/gpu/drm/radeon/radeon.ko. 此驱动程序支持所有不同型号的 AMD/ATI 卡,即使它们完全不同。

X 服务器为每个具有内核 DRM 驱动程序的显卡提供不同的启用 KMS 的用户空间驱动程序;与 X Native DDX 驱动程序一样,这些驱动程序负责自己实现所有加速(即性能在很大程度上取决于此驱动程序的质量)。然而,与 X Native DDX 驱动程序不同的是,它们可以将模式设置和缓冲区管理留给内核层(这可以更有效地完成)。此外,在内核中分配缓冲区这一事实为应用程序提供了更多直接访问显卡的选项(绕过 X 以提高性能)。用于卡的 X 原生 DDX 驱动程序和支持 X KMS 的 DDX 驱动程序通常共享大量与生成绘图操作“命令”相关的代码。

请注意,虽然 DRM 驱动程序实现了帧缓冲区 API,但也可能有一个单独的(更简单的)帧缓冲区驱动程序可用于同一张卡。例如,对于某些 intel 和 nvidia 卡来说就是这样——而 AMD 只有一个组合驱动程序。当 KMS 驱动程序处于活动状态时,将不会使用替代帧缓冲区驱动程序。然而,可以选择使用更简单的帧缓冲区是一件好事(例如,不需要高性能图形或服务器的嵌入式系统);当然,这确实意味着要维护重复的代码,因此某些显卡不再具有单独的帧缓冲区驱动程序。

初始化和驱动程序注册

radeon_drv.c on init:
* registers itself with the PCI bus as handler for specific PCI IDs, using
   + driver=kms_driver
   + pci_driver = radeon_kms_pci_driver

The PCI bus then calls back into radeon_pci_probe when a radeon card is found.
This calls drm_get_pci_dev which:
 + calls drm_get_minor(...,DRM_MINOR_CONTROL) which registers `/dev/dri/controlD{n}`
 + calls drm_get_minor(...,DRM_MINOR_LEGACY) which registers `/dev/dri/card{n}`
 + calls back into dev->driver->load, ie radeon_driver_load_kms.

drm/drm_stub.c: drm_get_minor 
+ calls drm_minor_get_id to allocate an unused minor-device-number:
   + type=DRM_MINOR_LEGACY: range = 0..63
   + type=DRM_MINOR_CONTROL: range = 64..127
   + type=DRM_MINOR_RENDER: range=128..255 (TODO: is this used anymore?)
   + this also ensures that drm_minors_idr has enough space for the new node [NOTE: UGLY SIDE-EFFECT]
+ allocates space for a "drm_minor", ie a simple wrapper around card-specific drm_device
+ sets the drm_minor structure to point to the drm_device for the radeon card
+ stores the new drm_minor structure into the drm_minors_idr
+ calls drm_sysfs_device_add

drm_sysfs_device_add:
+ initialises some kdev-related fields
+ sets device_name to either `/dev/dri/controlD{n}`, `/dev/dri/renderD{n}` or `/dev/dri/card{n}` depending on whether
  the type param was DRM_MINOR_CONTROL/DRM_MINOR_RENDER/DRM_MINOR_LEGACY
+ calls device_register

radeon_driver_load_kms:
* calls radeon_device_init (inits non-display parts of card, eg command-processor)
* calls radeon_modeset_init (inits display parts, eg CRTCs, encoders)
* calls radeon_acpi_init (configures ACPI)

通过适当地初始化各种回调表来处理不同的卡模型,例如,如果卡是“南岛”卡,则表在文件中定义si.c

打开设备文件

用户空间最终打开/dev/dri/card{N}。函数drm_stub_open()被执行,其中:

  • 用于drm_minors_idr将次要设备号映射到由 radeon 驱动程序在模块初始化时注册的结构 drm_device
  • 然后对file_operations来自该 drm_device 的结构进行切换,以便 radeon 提供的直接用于用户空间执行的未来文件操作。

IOCTL 操作

用户空间然后在文件句柄上执行 IOCTL,这些文件句柄被转发到radeon_ioctls_kms.

文件radeon_kms.c定义结构radeon_ioctls_kms以定义用户空间可用的所有 IOCTL 操作。这些 IOCTLS 不是“标准化的”;只有匹配的 libdrm 文件(例如/usr/lib/i386-linux-gnu/libdrm_radeon.so.1)才能理解它们。

第一个块(直到“KMS”注释)是过时/禁用的 IOCTL,只返回 -EINVAL。?? 谁用这些??

以下 IOCTL 是启用 kms 的图形驱动程序使用的 API:

  • INFO
  • CS– 提交一批命令流指令
  • GEM_INFO– ??
  • GEM_CREATE – 创建一个缓冲区
  • GEM_MMAP– 将 GEM 缓冲区映射到调用者的地址空间
  • GEM_SET_DOMAIN
  • GEM_PREAD
  • GEM_PWRITE
  • GEM_WAIT_IDLE
  • GEM_SET_TILING
  • GEM_GET_TILING
  • GEM_BUSY
  • GEM_VA

环形缓冲区处理

用户空间应用程序需要做的一项重要任务是将“命令流”指令直接发送到显卡(而不是通过 X)。Radeon 显卡使用“环形缓冲区”结构为这些指令提供高性能数据通道。

文件radeon_ring.c实现了必要的逻辑。尤其:

+ `radeon_ib_get` --> returns one of the "indirect" buffers - in r5xx there are two.
  * `radeon_sa_bo_new` allocates memory for the buffer
+ `radeon_ib_schedule` --> emits a command on the circular buffer that tells the GPU to use an IB buffer.

命令流解析

用户空间应用程序需要做的一项重要任务是将“命令流”指令直接发送到显卡(而不是通过 X)。然而,其中一些命令可用于接管主机,甚至损坏显卡或显示器。因此,发送的命令在传递之前首先由 KMS 驱动程序“验证”。

文件radeon_cs_parse.c.实现了必要的逻辑。尤其:

radeon_cs_ib_chunk():
  + obtains an intermediate buffer
  + calls `radeon_sc_parse`
  + calls `radeon_cs_finish_pages`
  + calls `radeon_cs_sync_rings`
  + calls `radeon_ib_schedule`
    ==> writes commands to the main ring to load the intermediate buffer now

radeon_cs.c:radeon_cs_ioctl 
+ validates commands then passes them on to ring buffer
+ referenced from radeon_kms.c as
     DRM_IOCTL_DEF_DRV(

问题

  • DRM_MINOR_RENDER 完全过时了吗?——看起来像。

  • 评论中引用的“/dev/drm”文件是否已经存在——看起来不像。?? 如果内核使用 nomodeset 启动,会出现 /dev/drm 吗?

参考

本文中的信息来自许多来源。这里有几个:

以下是一些其他有用的参考资料:

脚注

  1. 控制台(如引导控制台)是使用 BIOS VGA 文本操作输出的,不支持图形

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值