尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux DRM显示驱动探秘:从KMS到跨DRM录制实践

Linux DRM显示驱动探秘:从KMS到跨DRM录制实践 做了几年 Linux 图形栈的活儿最常被同事问的问题就是天天听你说 DRM DRM它到底是啥为什么新的显示驱动改了一版又一版最后都得落在那几个/dev/dri节点上这个问题确实值得掰开揉碎聊一聊。DRM 是 Linux 内核里的 Direct Rendering Manager从 2.4 内核时代一路孵化到现在已经成为几乎所有 GPU 显示驱动的主通道。不管你做的是 i915、amdgpu、nouveau还是嵌入式里那堆 msm、vc4、imx-drm只要想让自己的设备跑起一个能用 X/Wayland 的驱动就绕不开 DRM 这几个字母。这篇文章我按实际调驱动项目的方式把它拆开讲清楚顺手聊聊大家讨论比较多的跨 DRM 录制这类玩法适合第一次接触内核显示驱动、或者正在为无头机器录不了屏、画面黑屏发愁的朋友。1. DRM 的出身从“显存直刷”到“显示资源管理”1.1 老 fbdev 只能当画布管不了显卡的复杂度很多人第一次接触驱动看的是fbdev它简单一个/dev/fb0节点把内存里的像素数据往里一写屏幕就出画面。如果你做的是单片机、低端嵌入式这个模型到今天都够用。但放到 PC 和现代 SoC 上fbdev 就捉襟见肘了。原因很简单现代 GPU 输出不是“一块显存对应一块屏”这么直白。一块显卡往往带多个物理输出口每个口可能接不同的显示器分辨率、刷新率、色彩空间全都不一样显示链路里还夹着编码器、桥接芯片、DP MST 分流器甚至一个 CRTC 可以驱动多个 Connector。这些资源的管理、热插拔检测、EDID 解析、切换模式时的重建都远远超出“画布”的能力边界。用 fbdev 去做结果就是每家驱动各写各的私有接口用户态根本没法统一配合。1.2 从 DRI 到内核主线的那次整合DRM 真正被推到内核主线是 2.6 前后的事。更早的时候XFree86/X.Org 想走 Direct Rendering 让 OpenGL 直接访问 GPU 2D/3D 硬件搞了 DRIDirect Rendering Infrastructure项目。后来大家发现光靠用户态和驱动私聊不行内核里必须有一个统一的显存对象模型和权限控制层否则多进程同时访问 GPU显存怎么分配、怎么同步、怎么回收全是灾难。于是 DRM 被设计成这样一个中间层它住进内核接管 GPU 的设备文件、显存对象、中断、fence 同步、显示模式管理。再往后radeon 和 i915 先后把原来的私有 fb 驱动迁到 DRM 框架里fbdev 逐渐退化成控制台兜底方案。到 2010 年代后期DRM 事实上已经成为 Linux 图形栈的中枢。你打开/dev/dri目录看到card0、renderD128就是这套体系的对外窗口。1.3 DRM 到底提供哪三类能力把 DRM 拆到不能再简单的程度它提供三类基础能力显示输出管理KMS控制扫描输出、显示模式、颜色管理对应用户态能感知的“屏幕上显示什么”。显存对象管理GEM/GBM分配、映射、导出 GPU buffer让渲染结果能被合成、扫描、共享给其他设备。同步机制fence/syncobj给 CPU、GPU、显示扫描之间加同步点避免画到一半就被扫出去的情况。这三类能力每一块都能单独写几篇文章。但有个关键点需要先建立认知DRM 不是一个“画图的库”它是一个硬件资源管理框架。它不负责生成像素只负责把生成像素所需的 GPU 能力以标准接口开放给用户态。2. DRM 架构两条腿走路哪条都不能瘸2.1 KMS像舞台调度一样去管显示器KMSKernel Mode Setting是 DRM 最常被感知到的一半。它管的是显示路径上的四个核心对象CRTC扫描控制器。它按固定时钟从内存读帧合成多个平面后送出信号。Plane显示平面。主平面、光标平面、视频覆盖平面都在这一类每个平面挂一个 framebuffer。Encoder编码器。把 CRTC 出来的信号转换成具体接口形式比如 TMDS、DisplayPort、MIPI DSI。Connector物理输出口。代表一个 HDMI 口、DP 口或者面板接口通过探测拿到 EDID确定显示器支持哪些模式。这四个对象的关系我一般喜欢用舞台来类比Connector 是观众席Encoder 是扩音器CRTC 是舞台控制台Plane 是台上正在播放的屏幕。你要让观众看到画面得先确认观众席还存在Connector detect调好扩音器格式Encoder从舞台控制台指定从哪块屏幕取内容CRTC Plane最后按下演出开始键SetCrtc/Atomic Commit。用户态通过/dev/dri/card0发 ioctl 操作这些对象。drmModeGetResources枚举对象drmModeGetConnector拿显示器和模式列表drmModeSetCrtc做最基础的显示切换。复杂一点的场景要用drmModeAtomicCommit一次把多个属性统一提交避免中间状态导致花屏。2.2 GEM显存对象与跨设备共享的底座另一半是 GEM。早期 GEM 这个名字确实有点“图形执行管理器”的意思但今天看它更像一个显存对象模型内核里一切 buffer 都用一个 GEM object 表示。用户态拿到 handle通过mmap可以读写通过PRIME机制可以导出成dma-buf传给其他设备比如给编码器做零拷贝。实操里最常用的是 dumb buffer不需要 GPU 华丽渲染、只想把 CPU 数据刷到屏幕时用drmIoctl(DRM_IOCTL_MODE_CREATE_DUMB)创建一块普通显存DRM_IOCTL_MODE_MAP_DUMB拿到 CPU 映射地址往里填像素再drmModeSetCrtc让它显示出来。这也是很多 framebuffer 移植到 DRM 下的典型思路。但一旦涉及 GPU 渲染就不是 dumb buffer 能扛的了。渲染场景需要 GBM 分配缓冲区Mesa 或 Vulkan 层把渲染图像写进 buffer再通过drmPrimeHandleToFD导出给显示、编码、另一个 GPU。这就是跨 DRM 录制里最关键的一环显存对象一旦能被工厂化地导出导入录屏工具就根本不需要关心底层是 AMD 还是 Intel。2.3 用户态怎么和 DRM 打交道libdrm 与 master 权限用户态一般不是直接放 ioctl而是走 libdrm 这套封装。modetest这种命令行工具也是基于 libdrm 写的。打开一个 KMS 节点后要先drmSetMaster拿到主控权才能改显示模式权限控制这一层很关键否则随便一个进程都能把分辨率改掉桌面早就乱套了。这里有个初学者特别容易懵的概念card0和renderD128有什么区别。简单说card0是主节点既能渲染也能做 KMSrenderD128是渲染节点只负责分配缓冲和提交渲染任务不能碰模式设置。做录屏和显示合成通常需要主节点做纯离屏渲染可以只用渲染节点两个别混。3. 为什么显示驱动绕不开 DRM三个层面的现实3.1 用户态生态全部长在 DRM 之上你先想一个问题如果我现在新写一个显示驱动不走 DRM而是自己发明一套 ioctl用户态能跑起来吗答案是否定的。X.Org 的 modesetting 驱动、Wayland 合成器里的 drm-backend、Mesa 的 DRI3/GBM 后端全是从 libdrm 的接口拿帧、做 Present。你不接 DRM等于桌面系统根本没有公共语言和你沟通。这个“被迫接入”是良性的。DRM 把显示驱动最复杂的部分——模式扫描、热插拔、缓冲翻页、同步——抽象成稳定接口上游开发者围绕它做工具、做测试、做安全加固。作为下游驱动作者你反而省掉了维护私有 ABI 的负担。这正是“绕不开”的第一层含义生态和用户都在 DRM 另一边你没有第二个入口。3.2 内核层面的基础设施也是围绕 DRM 转的很多人只看到显示驱动要接 DRM其实内核里其他相关子系统也在往 DRM 靠。曾经的 fbdev 如今默认退居控制台V4L2 的视频采集设备与 DRM 的dma-buf配合做零拷贝DRM 的writeback连接器甚至允许驱动把 CRTC 输出“回写”成内存帧这在录屏、远程桌面、硬件回放上特别有用。另一个现实因素是新驱动必须跟主线走。Linux 内核对新驱动有个隐要求尽量往通用框架上迁移复用别人的atomic_check、atomic_commit逻辑。你去看include/drm/drm_atomic_helper.h那一堆 helper就是在帮你减少造轮子。相比几十年前各写各的 radeon fb 驱动时代现在写一个新平台显示驱动工作量大头反而变成了实现好mode_valid、atomic_check、atomic_flush这几个钩子然后把硬件怪癖填进去。3.3 安全和维护成本的倒逼显示驱动不接 DRM还有一个更深层的风险你自己管 CRTC、管显存映射、管权限很容易漏掉安全边界。内核社区反复强调 IOMMU、显存隔离、禁止用户态直接踩任意物理地址DRM 框架把这些边界都固化了buffer 必须先被 GEM object 引用映射必须走 VMA 权限校验多设备的 buffer 共享必须过dma-buf的 attach/export 流程。你想绕过它等于把攻击面重新打开。维护角度也一样。DRM 有大量现成的 debugfs、tracepoint、drm_client框架你嵌入进去之后出了黑屏、闪屏问题可以直接拿trace-cmd抓 atomic state 事件拿modetest摆现状。这些基础设施是我的日常排障利器。绕开 DRM等于放弃这些现成工具。4. 跨 DRM 录制没有屏幕的地方也能把画面抓下来4.1 什么是跨 DRM 录制解决什么问题“跨 DRM 录制”这个名字听起来玄拆开就是两件事一是跨厂商一套录制逻辑在 i915、amdgpu、vc4 上都能跑因为大家走的是同一套 DRM/KMS 接口二是跨环境服务器无头、虚拟机里没有物理显示器但只要模出一套 KMS 设备画面照样能抓能录。实际业务场景非常常见远程桌面网关想把虚拟机的桌面录成流云游戏平台想把 GPU 渲染出来的帧转给编码器测试机房想抓启动阶段的生命周期画面这时候没有 X11、没有 Wayland甚至 HDMI 口都没插显示器。唯一的公共路径就是 DRM。录制工具通过 libdrm 枚举 Connector拿到 preferred mode建立 framebuffer之后要么回读到 CPU要么直接导出成dma-buf送给硬件编码器。这个过程做到了位就叫跨 DRM 录制。4.2 方法一用 vkms 造一个虚拟显示器最省事的入门方案是用内核的vkms驱动。modprobe vkms之后系统里会多出一个虚拟 KMS 设备没有物理显示器也能枚举出 Connector 和 Mode。我一般先跑modetest -M vkms看看节点状态modprobe vkms modetest -M vkms -c输出里能看到一个虚拟的Virtual-1连接器以及它支持的 1920x1080、1280x720 等模式。之后用 libdrm 打开card0拿虚拟 Connector 的 preferred mode创建 dumb buffer 填帧drmModeSetCrtc提交vkms就会把 framebuffer 的 CRC 算出来。此时想录屏就得在 CPU 侧把你填进去的像素一并存下来——因为vkms本身不做真正的像素扫描它只是模拟机制验证流程、测试音频无关的显示管线非常顺手。这套玩法特别适合测试跨 DRM 录制的应用逻辑你不需要一块真实显卡就能验证枚举、模式选择、frame 提交、颜色格式换算这些核心代码。4.3 方法二EGL GBM 渲染后做 CPU 回读如果你手里有真实 GPU但机器没有物理显示器可以用渲染节点配合 GBM 做离屏渲染再把结果回读到内存。流程是创建 GBM 设备gbm_surface_create申请一块渲染 buffer配好 EGL 上下文和 surface渲染完成后eglSwapBuffers再gbm_bo_map拿地址用glReadPixels或者直接memcpy把像素取到 CPU 侧。这里有个容易踩的坑gbm_bo_map不是所有驱动都支持零拷贝。有些嵌入式驱动 map 回来之后像素格式是 tiled 的不能直接当 NV12 或者 BGRA 用。稳妥做法是先查gbm_bo_get_format必要时在 GPU 侧做一次 blit把 tiled buffer 转成线性布局再回读。回读拿到的是连续内存帧喂给 FFmpeg 转封装就行。比如用一个rawvideo输入指定好宽高和像素格式就能编出 H.264 文件。这种方案 CPU 开销大但胜在通用驱动兼容问题少。4.4 方法三dma-buf 零拷贝接力硬件编码器生产环境对性能有要求这个方法才算正路。思路是不把渲染结果回读到 CPU而是保持帧数据在 GPU 显存里通过dma-buf把同一个 buffer 的所有权分享给编码器。链路大概是GBM 分配 buffer - GPU 渲染 -drmPrimeHandleToFD导出 FD - 编码器侧把 FD 通过 VAAPI/NVENC/V4L2 M2M 设备导入。整个过程始终是 GPU 到 GPU没有 PCIe 回读带宽和延迟都能压下来。FFmpeg 的hwupload、hwmapfilter 就是干这个的一张显存帧在这条管道里从 DRM 流到编码器。我印象比较深的是在一台无头 AMD 服务器上做桌面录制。环境里没有物理输出我用vkms提供 KMS 节点渲染后端是 amdgpu编码走 VAAPI最终录出来的是流畅的 4K 流。整条链路看起来是“两个不同 DRM 设备在协作”实际底层全靠dma-buf的 import/export 机制打通。跨 DRM 录制能做到这个程度才算是真正吃透了框架。5. 实操踩坑实录与排查清单5.1 没有权限打不开 card0最常见的报错是drmOpen失败或者Permission denied。如果你用的是普通桌面环境X/Wayland 那边的 compositor 已经占着 master你的工具直接drmSetMaster会被拒绝。解决要分情况临时调试就在 root 或video组下跑要长期跑录制服务用udev规则把/dev/dri/*的权限拨到指定用户组。还有一个细节多 GPU 机器上card0不一定是你想操作的那张卡用drmGetDeviceNameFromFd或者去/sys/class/drm/card*/device/vendor里比对 PCI ID 确定。5.2 枚举不到 Connector或 Connector 状态是 disconnected无头服务器上最常见。物理 HDMI/DP 口没插显示器KMS 枚举出来的 Connector 状态就是 disconnected很多工具会直接放弃。这时候要么接一个“假负载”让链路保持连接要么用vkms虚拟一个连接器。注意drmModeGetConnector返回的 count 是 0 时你要重新 poll因为有些驱动热插拔检测是异步的。我在调试时习惯加一个 3 秒重试循环如果一直是 0再看 dmesg 里的drm_connector日志确认 detect 回调是否真的被调用过。5.3 atomic commit 返回 EBUSY 或 EPERMEBUSY通常表示你试图改的 CRTC/Plane 正在被其他进程占用或者上次翻页还没完成。检查点有三个是否已经把 plane 的 framebuffer 关联到正确的 CRTC是否漏了DRM_MODE_ATOMIC_ALLOW_MODESET标志fence 是否在等待超时。EPERM则多半是 master 权限问题。有人会在拿到DRM_CLIENT_CAP_ATOMIC之后忘记重新drmSetMaster导致 atomic 不认。这个细节我犯过不止一次。5.4 录出来的画面颜色不对、花屏、方向反跨 DRM 录制里最折磨人的不是拿不到帧而是拿到帧后格式不对。硬件 framebuffer 常见格式有 XRGB8888、ARGB8888、NV12、P010还有一堆 tiled 变体。你如果不管drmModeGetFB2返回的 modifier直接当普通 BGRA 处理录出来的画面十有八九是绿的、红的或者上下颠倒。我的习惯是先modetest -M card0 -f看当前 framebuffer 格式再决定回读路径里要不要过一次 conversion。需要 GPU 转格式时用 Vulkan 或 Gallium 的 blit 子集做一次 copy不要图省事在 CPU 端逐像素转换否则 4K 60 帧能把 CPU 直接打满。5.5 画面撕裂和帧率漂移录制过程中如果出现撕裂多半是没配合 fence。回读和渲染之间要么加eglCreateSyncKHR要么用DRM_IOCTL_SYNCOBJ_WAIT等 GPU 完成信号。帧率漂移则是你回读耗时超过了帧间隔导致编码器排队越来越多。这种情况下别迷信 PTS以编码器输入队列的实测时间戳为准必要时丢帧保流畅。6. 一些只有踩过坑才懂的细节写到最后分享几个我在项目里反复验证过的体会。第一调试 DRM 显示路径首先打开DRM_DEBUG_MODESET。内核参数里设drm.debug0x04重启后 dmesg 会吐出 atomic state、connector 状态变化、mode 校验的完整过程。想靠眼睛猜哪个 CRTC 没配好纯属浪费时间。第二modetest不是用来看一眼的它是你的“显示器探针”。每次上手新板子我都会先跑一遍modetest -M 设备节点 -p -c -f把该显卡的 pipe、encoder、connector、mode 列表完整存下来。之后写代码就按这份“地图”来比看 datasheet 快得多。第三跨 DRM 录制别追求一步到位。先把管线拆成三段验证显存分配对不对、渲染结果能不能回读、编码器能不能吃这格式。三段各自通了再拼起来。我见过太多人第一次就写整条链路结果要么是格式不兼容要么是同步点错最后查起来焦头烂额。最后想说DRM 确实不好啃代码量大、概念重叠、文档藏在内核源码的Documentation/gpu/下。但正因为几乎所有显示驱动都在这个框架里你花时间把它摸透后面换平台、换 GPU 厂商、做虚拟化、做录制服务都能吃很久的红利。耐心把这套对象模型和 ioctl 逻辑盘顺眼前这些驱动和录屏问题都会慢慢变得通透起来。
返回列表