ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux显示驱动必知:DRM内核图形栈核心与跨DRM录制实践

Linux显示驱动必知:DRM内核图形栈核心与跨DRM录制实践 如果你的工作跟 Linux 显示驱动沾边不管你是写 GPU 驱动、调显示管线、还是做投屏和录屏早晚都会撞上一个词DRM。可问题在于这个缩写本身就有歧义很多人的第一反应是数字版权管理但在驱动开发这个语境下DRM 指的是 Direct Rendering Manager直接渲染管理器。它不是什么可选项而是 Linux 内核里掌管显示设备的核心子系统所有送显的帧都要从它手里过一遍任何显示相关的上层组件 Wayland、X11、Mesa、FFmpeg 的 GPU 解码输出最终都得落到 DRM 接口上。这篇就把这事拆开讲清楚DRM 在内核图形栈里到底扮演什么角色显示驱动为什么绕不开它以及最近经常被提到的跨 DRM 录制这类需求到底是在做什么、要从哪里入手。项目文本、配套工具和调试思路我都会聊尽量做到看完就能上手摸一摸自己的设备。1. DRM 到底是个什么角色先理清它在图形栈里的位置1.1 用“快递分拣”理解 DRM 在做什么拿快递分拣来做类比。GPU 是生产车间负责把画面渲染成一块一块的帧缓冲显示器是签收人每隔一段时间就要收到一帧新鲜的画面并显示出来而 DRM 就是那个分拣中心和调度员。它负责管理 GPU 里显存怎么分配、哪块 buffer 可以被送显、什么时候该把 buffer 切到显示器上、屏幕分辨率刷新率怎么设、HDMI 还是 DP 接口怎么选。用户态程序不能直接摸寄存器而是通过 DRM 提供的 ioctl 接口告诉内核“我要把这块 buffer 切到当前 CRTC 上”“我要把分辨率改成 1920x108060”内核里的 DRM 核心和具体驱动再帮你执行这些动作。这层抽象非常关键。Linux 的图形栈是出了名的多层叠加应用层 - Wayland/X11 合成器 - Mesa/EGL/GL - 内核 DRM - 显卡硬件。每一层职责都很明确而 DRM 是在最底下承托一切的内核底座。如果没有 DRM 这样一个统一框架每家厂商都自己搞一套用户态接口那 Wayland 合成器为了适配 N 块显卡就得写 N 套后端代码这显然不可接受。DRM 之所以能成为事实标准核心在于它把硬件差异藏在标准对象和 ioctl 后面。你写的是酷睿的核显还是高通的 Adreno对外暴露给用户态的都是一套东西打开 /dev/dri/card0拿到 resources找到 connector配置 crtc提交 framebuffer。硬件特性差异则通过通用属性property来暴露比如旋转、缩放、色彩深度、HDR 元数据之类的。底层具体怎么实现是驱动工程师自己的事对外接口的形状DRM 已经定死。1.2 从 DRI 到 KMSDRM 怎么成为 Linux 的事实标准DRM 不是一天长成这样的它有一段很典型的 Linux 式演进史。早期显卡驱动都是 X Server 一统天下用户态 X 驱动直接操作显卡寄存器做 mode setting也就是设置分辨率、时序这些。这种做法最大的问题是没有一个统一管事的机构进程一多就乱套两个程序同时改寄存器屏幕闪烁、撕裂都是家常便饭。后来社区搞出了 DRIDirect Rendering Infrastructure目的是让用户态能够直接渲染到硬件不需要每次都让 X Server 中转。再往后内核开发者决定把 Mode Setting 也从用户态收归内核管理这就是 KMSKernel Mode Setting。KMS 的好处非常明显mode 设置这种涉及全局状态的操作必须在受控的内核上下文里原子地完成不能你改一半我改一半。KMS 是 DRM 的一部分也是今天介绍的核心主线。DRM 本身是从 DRI 时代的内核模块演变来的现在它包含了 KMS显示模式管理、GEM图形执行管理器负责显存和 buffer 管理、以及各种同步原语fence、vblank。这些年里树莓派的 SoC、各种 ARM 开发板、Intel AMD NVIDIA全都统一到了 DRM 框架下。你可能还会听到一些很老的说法叫“DRM master”说的是当前有权限操作显示状态的进程这对应的是 X Server 和 Wayland compositor 的职责而普通渲染进程通常只需要 render node 就够了不需要 master 权限。所以当你看到有人问“为什么现在的显示驱动绕不开 DRM”原因就在于整个 Linux 图形生态的上下游都已经围绕 DRM 建好了。你的驱动如果不走 DRM你在用户态就接不上任何合成器也接不上主流的媒体录制框架这就是现实。2. 为什么显示驱动绕不开 DRM四个让我服气的理由2.1 只有内核态才敢碰“垂直消隐”和时序很多刚入门的朋友有个疑问我直接在驱动里给寄存器写像素时钟和 sync 极性不也能点亮屏幕吗确实能但问题在于这种操作一点都不安全而且极其容易闪屏。显示控制器有一套严格的时间要求扫描线要一行一行地扫扫完一帧之后会有一个短暂的消隐窗口专业点叫 vblank。在输出新一帧的画面时如果刚好赶上扫描进行到一半你把 framebuffer 地址改了屏幕上就会出现一条撕裂带上半部分是旧画面下半部分是新画面。要解决这个问题就得让换帧动作严格发生在 vblank 窗口内。怎么知道当前是不是 vblank这在硬件上依赖中断而中断只有内核态才有权限处理。用户态程序不可能优雅地接收到垂直消隐中断更不可能保证注册中断回调后还能在几微秒内完成寄存器切换。所以 page flip、vblank 计数这些机制天然就只能放在内核里做。DRM 把这类能力封装成了标准接口驱动实现好相应的回调用户态就能用 dma_fence 之类的机制来同步渲染和展示避免撕裂。说个实际体会我在调试早期 ARM 平台的显示驱动时曾经试过直接裸写寄存器来切 buffer确实能亮但跑视频播放时会发现撕裂明显因为完全没有跟 vblank 对齐。后来老老实实按 DRM 框架提供 .atomic_flush 回调让硬件在 vblank 安全窗口里完成切换撕裂问题就再也没出现过。这就是为什么“内核态 垂直消隐”这两个词在显示驱动里是绑定的。2.2 对象化管理CRTC、Encoder、Connector、Plane 不是随便叫叫GPU 显示子系统是一堆硬件模块的集合有显示控制器、有编码器、有物理端口、有图层合成器。DRM 的最高明之处就是把这堆硬件抽象成了几个固定对象CRTC、Encoder、Connector、Plane、Framebuffer外加若干动态属性。无论你是哪家芯片最终的显示链路都能表示成 CRTC 从 Framebuffer 取帧经过 Encoder 编码从 Connector 输出到显示器。这就是一个数据流的流水线模型。对象化管理的最大好处是让用户态可以统一枚举和配置硬件。用 libdrm 打开一个设备之后你不需要知道这颗 GPU 是 Mali 还是骁龙的 Adreno只需要遍历 connectors遍历 encoders遍历 crtcs然后按一定规则匹配连接最后创建一个 framebuffer 再 bind 到 plane 上。这样的设计也大大降低了驱动里出 bug 的难度因为每个模块只负责自己那一块状态状态流转由 DRM core 来统筹。我记得第一次在公司代码里看到一堆 struct drm_crtc_state、struct drm_connector_state 这种结构体的时候感觉状态管理怎么这么复杂。后来才发现正是这一堆 state 对象让原子提交变成可能。每个对象都有自己的 state互相独立又能联合提交脏标记和回滚机制都建立在它上面。如果没有这层对象化设计光是想清楚“改分辨率时哪些寄存器要联动更新”就能让人头大。2.3 原子提交把一大堆状态当一个事务来更新之前传统的 modeset 路径是分步进行的先设置 CRTC、再设置 Encoder、然后 Connector每一步之间屏幕可能处于一个中间状态闪屏、黑屏、花屏都有可能。DRM 后来的原子提交atomic commit把这一堆操作打包成一个事务。你要改 2 个 plane 的 framebuffer、改 1 个 connector 的链路、再改端口使能状态可以一次性通过 DRM_MODE_ATOMIC_ALLOW_MODESET 提交出去内核会先做一套完整性校验校验通过后尽量在一个硬件刷屏周期内完成切换。这个设计对多屏输出尤其重要。比如笔记本上同时接着内屏和外接显示器你要从“内屏用核显、外屏用独显”切成“双屏都走独显”如果一个个改中间绝对会出现某个屏闪黑、某个屏分辨率错乱、合成器崩溃这种事。原子提交给了内核一个整体视图驱动可以在 .atomic_check 阶段把所有依赖关系都算好比如带宽是否充足、PLL 能不能锁住、像素时钟是否够用然后再进入 .atomic_commit 阶段做真正的硬件操作。任何一环失败整个事务直接回滚用户态不会看到恶心的半残状态。做驱动的人真的应该感谢这套机制因为调试导致的闪屏问题有很大一部分原子提交帮你兜住了。要是没有原子提交驱动工程师在 bringup 阶段可能每调一次显示都要重启一次系统那开发效率没法看。2.4 最现实的原因你写的是“驱动”不是“裸机程序”从底层看写一个显示驱动有三个层次一是实现基本点亮功能二是实现完整的显示管线和多屏支持三是跑通用户态合成器、录制、调试等生态工具。如果只做第一层你觉得不依赖 DRM自己开一个字符设备、暴露一组自定义 ioctl、直接在驱动里操作 CRTC 寄存器也能做到。但问题是任何 Linux 发行版都不会搭理你这样的一套私货接口Wayland compositor 根本不知道该怎么调用你Mesa 也不知道怎么跟你的 GBM 对接连最简单的 modetest 都跑不起来。到了第二层和第三层你其实已经不可能绕开 DRM 了。GBMGeneric Buffer Manager是 Mesa 用来分配显存的标准而它在底层就是调 libdrm 的 GEM 接口Wayland 的 DRM backend 走的全是 DRM KMS 的 ioctl视频录制走 DMA-BUF 导出也是来自 DRM 的 PRIME 机制。跨 DRM 录制这类需求更是直接建立在主设备、渲染节点、多个 DRM 设备之间 buffer 互通的能力上。所以结论很简单DRM 不是你愿不愿意用的问题而是你必须实现、必须对接、必须理解的生态底座。你关起门来写自己的三体式驱动当然也行但那只是自娱自乐永远不会进入主线的代码审查。3. 把 DRM 的对象模型拆开吃透谁管什么、谁连谁3.1 用一张责任表看清各对象边界很多人刚看 DRM 代码时最晕的就是那六个核心对象来回飞。我给一张责任列表这是我给团队做培训时反复用的对象职责生活化类比CRTC显示控制器的核心通道负责从内存取帧、产生时序、扫描输出流水线主管决定最终一帧从哪块 buffer 来、什么时候送Encoder把像素数据转换成特定接口信号TMDS、LVDS、DP 等快递打包员把包裹装到运输车上Connector物理端口和显示器信息EDID、热插拔状态、可用模式快递箱上的面单标明这是 HDMI-1 还是 DP-1Plane可叠加的图层主图层、光标层、视频层可做缩放旋转舞台上的不同灯光层能各自启停、混合Framebuffer内存中的像素存储区包含格式、宽高、stride、dma-buf 地址一张已经画好的画布Property动态属性用来暴露色彩空间、HDR、宽频带等扩展能力水单上的备注项可以做灵活扩展显示链路连接关系基本是Framebuffer - Plane - CRTC - Encoder - Connector - 显示器。一个 CRTC 可以接多个 Connector 做镜像输出一个 Connector 在同一时刻只能跟一个 CRTC 绑上。如果你的芯片支持 MIPI DSI 并联一个 Connector 可能对应多个 DSI 面板这些都是通过驱动里的回调建链来处理的。理解这条链是理解所有 DRM API 的关键。比如你想“点亮一块屏”本质上是找一个 connector找一个匹配的 encoder再找一个空闲的 crtc给 crtc 绑定一个 framebuffer然后提交。再比如你想做“跨 DRM 录制”本质上是把设备 A 上某个 plane/渲染目标对应的 dma-buf 拿过来映射到设备 B 的显存或系统内存里再用另一套管线读走或者编码。3.2 用 modetest 把硬件链路“看”出来纸上谈兵没用打开终端直接看。modetest 是 libdrm 自带的测试工具几乎所有发行版都有。先列出当前的 DRM 设备ls -l /dev/dri/一般会看到 card0、card1 和 renderD128、renderD129 一类的节点。card 节点是同时包含 KMS 和 render 能力的复合节点通常在 X/Wayland 环境中只有 compositor 能拿到 master 权限render 节点不给 KMS 权限仅用于渲染。接下来用 modetest 看某个 card 的详细硬件链路modetest -M card0 -c这条命令会列出所有 connector显示的字段包括 connector id、type、statusconnected/disconnected、modes一行一个分辨率、以及当前使用的 encoder。再跑modetest -M card0 -p这是列出所有 plane 的信息能看到 plane id、与 CRTC 的绑定关系、支持的像素格式、尺寸限制等。最后再看 crtc 和 encoder 的情况modetest -M card0 -e modetest -M card0 -d我建议任何一个做显示相关开发的同事拿到一台新机器第一件事就是跑这三条命令。它能让你像 X 光一样看到系统当前究竟是怎么布线、怎么用的。很多排查场景里你根本不需要翻内核代码modetest 输出已经能告诉你 connector 有没有识别到显示器、mode 列表是否完整、plane 数量是否够用、当前在用的是哪条通道。我在一次客户支持中遇到“外接 4K 显示器黑屏”的问题modetest -c 一看发现 connector 支持的 max display clock 只有 300MHz而 4K 60Hz 已经压到了 586MHz 的 TMDS clock 上限问题立刻定位到是 DP 转 HDMI 2.0 芯片的限制跟驱动本身毫无关系。3.3 手动配置一条显示链路观察状态变化光看不够动手配一条链路才能把模型焊死在脑子里。modetest 可以直接设置一条模式链路比如把 HDMI-A-1 接口以 1920x1080 模式输出modetest -M card0 -s 32:1920x1080-60这里 32 是 connector id1920x1080 是分辨率60 是刷新率。这条命令的内部逻辑就是寻找可用的 encoder 和 crtc组合成一条从 plane 到 connector 的输出链路然后发起一次 modeset。运行之后屏幕如果被另一台合成器占着可能不会生效因为当前进程没有 master 权限你可以在终端里先退出图形界面或者用 DRM master 工具抢一下权限但这会弄黑屏建议在一个测试环境里操作。如果你还是觉得不过瘾可以写一个极简 C 程序走一遍流程打开 /dev/dri/card0调用 drmModeGetResources 拿资源drmModeGetConnector 查连接状态挑一个 mode再调用 drmModeSetCrtc 把 mode 设上去。这一步走完你对 CRTC、Connector、Mode 三个对象的关系基本就通了。再往下走就是创建一个 framebufferdrmModeAddFB把它绑定到 planedrmModeSetPlane然后驱动它在 vblank 安全的时机做 page flip。这一整套动作做完你就已经用过一次完整的 KMS 原子流程了。4. 实操拿到 DRM 设备后能做什么以及跨 DRM 录制的起点4.1 哪些设备节点是 DRM 的它们有什么区别Linux 里 DRM 设备节点常见的有三类。第一类是 /dev/dri/card0、card1它们通常包含了 KMS 和 GEM 的完整功能是合成器和 mode 设置程序主要使用的入口。第二类是 /dev/dri/renderD128、renderD129它们只做渲染相关的 buffer 分配和提交没有 KMS 能力普通渲染进程通过它们和 Mesa 交互避免互相抢 master。第三类是老式 /dev/fb0这是 fbdev 兼容层为了老旧软件兼容而存在的功能非常有限新的显示栈基本不依赖它。判断一个 DRM 设备是好是坏、支不支持你要的功能可以写一个小程序去查询能力位。比如调用 DRM_IOCTL_VERSION 拿驱动名和版本调用 DRM_IOCTL_GET_CAP 查 DRM_CAP_DUMB_BUFFER、DRM_CAP_ADDFB2_MODIFIERS。很多开发者会遇到开了 /dev/dri/card0 但 drmModeGetResources 返回空的情况先用 ls -l 看节点的权限和你所在的用户组通常需要把用户加到 video 组否则打开设备成功但 ioctl 被拒绝这类问题我在调试嵌入式设备时遇到太多次了。4.2 跨 DRM 录制到底是什么场景“跨 DRM 录制”这个词乍一听有点新但实际场景在多 GPU 平台和虚拟化环境里非常常见。我拿一台同时有核显和独显的笔记本举例系统里有两个 DRM 主设备card0 是 Intel 核显card1 是 NVIDIA 独显。如果某个程序跑在独显上渲染结果缓冲位于独显的显存里但当前输出链路可能走的是核显也可能是独显直连。这时你想做一个录屏工具把当前屏幕上某个区域的内容抓下来那你面对的就不是一个简单的 read framebuffer 操作而是要决定到底从哪个设备拿帧怎么把另一个设备的 buffer 导出到当前进程能访问的空间怎么保证帧同步正确。这背后的核心机制是 DMA-BUF 和 PRIME。DRM 驱动通过 dma_buf 这个内核机制把显存 buffer 导出成一个文件描述符再通过 PRIME 机制导入到另一个设备。用户态调 drmPrimeHandleToFD 拿到一个 FD把这个 FD 传给另一个设备drmPrimeFDToHandle 再拿到一个 handle之后就能像操作本地 buffer 一样去映射和读取它。如果是同一条 PCIe 总线上的设备这一步通常可以做到零拷贝共享如果设备之间没有 IOMMU 或者不支持 P2P那就要回落到 CPU 拷贝或者通过 system memory 中转。“跨 DRM 录制”的另一个常见场景是虚拟化里的帧转发。宿主机的 DRM 平面可以被虚拟化环境捕获再以 fdm 方式传给虚拟机里的显示管线。你听过的很多视频采集卡方案其实也类似并不是采集卡抓 HDMI 信号而是把 DRM plane 里的 framebuffer 通过 dma-buf 导出然后整个 DMA 到采集卡的 capture buffer 里。4.3 一个简化版实验思路给你一个实操思路不用写太多代码就能感受一下跨 DRM 的流程。首先用 modetest 确认当前哪个设备上有活跃的 planemodetest -M card0 -p | head -20然后写一个 C 小程序打开 card0用 drmModeGetResources 和 drmModeGetConnector 找到当前连接的 connector再通过 drmModeGetPlaneResources 找到正在用的 plane。如果这个 plane 的 framebuffer ID 不为 0就可以通过 drmModeGetFB 拿到 framebuffer 的 handle。再调用 drmPrimeHandleToFD 把这个 handle 转成 FD之后你可以读 FD 里的内容或者把它作为一个普通 DMA-BUF 传给别的设备。整个过程看起来简单但真正做录制的时候要注意格式转换DRM 的 framebuffer 通常使用 ARGB8888、XRGB8888 等格式而录制编码器要的是 YUV中间必须经过 GPU 或 CPU 做色彩空间转换。很多人一开始直接在 FD 里按 RGB 预期去读字节结果画面偏色偏得离谱就是这个原因。如果你是做多设备的场景先确认两件事两个设备是否都能打开对应的 DMA-BUF 导入接口导出的 FD 能不能 inotify 到同步事件同步这块在跨设备里最容易被忽视。你在设备 A 渲染了一帧还没等 GPU 执行完设备 B 就把 buffer 读出去了那录出来的就是花屏或者半帧。DRM 里有 implicit fence 和 explicit fence 两条路录制工具做得专业一点会去等 DMA_BUF_IOCTL_SYNC或者等 GL fence粗糙一点就直接在 CPU 里 sleep 几毫秒再读纯靠运气。我的建议是不要靠 sleep老老实实等 SYNC ioctl踩过的坑实在太多。5. 常见问题与排查技巧实录5.1 我踩过的几个坑第一是权限问题。很多人打开 /dev/dri/card0 显示成功但一调 drmModeGetResources 就报 EACCES或者直接看到“Permission denied”。这通常是系统里 systemd-logind 在管理设备权限而你的用户不在 seat 或者没被 logind 授权导致 ioctl 被拦。简单处理是把用户加入 video 组并重启更规范的做法是通过 logind 的 DRM device 管理接口获取 fd 再传给程序Wayland compositor 就是这么干的。第二是 Atomic commit 老是失败但内核日志看不到任何东西。解剖这一类问题要开启 DRM debug 动态日志echo 0x1f /sys/module/drm/parameters/debug dmesg -w | grep drm这样内核会打印 atomic_check 的失败原因通常能直接看到某个 property 值非法、某个 plane 的状态不被 CRTC 支持之类的信息。很多新手导致的花屏问题最后定位到的是 plane 的 pixel format 不被当前硬件合成器支持日志里会非常明确地显示 “not supported with current CRTC”。第三是 vblank 计数不更新。一些虚拟化平台或者桥接芯片的显示驱动里如果 vblank 中断没有正确注册DRM 的页翻转永远卡在等待状态。这时候用 modetest -v 观察 vblank 频率如果计数停在零附近基本可以断定是中断没有送到 GPU 或者屏蔽位没清。这种问题在内核日志可能什么都看不到必须靠测量中断计数来判断。第四是 encoder 和 connector 的匹配不满足硬件限制。我见过有人拿到一个 eDP connector 和一个 DPI encoder想绑定起来结果硬件根本不支持最后屏幕上只出不亮。这不是 DRM 框架的问题而是驱动能力描述和真实硬件状态不一致。排查这类问题要盯着 device tree 或者 ACPI 里面 encoder 和 connector 的拓扑描述以及驱动有没有正确设置可能连接的标记位。5.2 速查表症状与排查路径现象可能原因排查手段modetest -c 都列不出 connector驱动没注册 connector或者 GPIO 检测引脚坏了dmesg 看 probe 日志用 i2cdetect 看 DDC 通道设置了 4K60 但屏幕不稳定TMDS clock 超限查 connector max clock看 DP 带宽 lane 数和 HBR3 支持画面撕裂没有对齐 vblank检查是否用了 page flip / atomic commit是否主动等待 vblankatomic commit 返回 EINVALproperty 值非法或者状态依赖冲突开 DRM debug逐个 property test 去掉找失效项开机点不亮但驱动 probe 成功connector 状态判断错误强制输出但时序不对对比 mode list手动用 -s 指定已知模式试多 GPU 间 buffer 导入失败PRIME 支持不足或者没有 IOMMUcat /sys/module/drm/parameters/debug 之后查 EOPNOTSUPP录制帧错乱偏色格式转换没做或者 fence 没等检查 framebuffer 的 format、stride走 DMA_BUF_IOCTL_SYNC双屏切换闪黑没有用原子提交传统 modeset 分步操作合成器那边有没有用 ATOMIC_ALLOW_MODESET另外还有一个小经验驱动调试时经常需要手动触发一次全链路状态 dump。我用得最多的是 sysfs 里的一些节点比如 /sys/kernel/debug/dri/0/state能一次打印出所有 crtc、connector、plane 的当前状态比 dmesg 里零散的日志直观得多。配合 DRM 的 debugfs很多状态冲突的问题一眼就能看穿根本不用反复改代码重新编译内核。还有一点心得想特别讲一下DRM 驱动和普通字符驱动不一样的地方在于状态一致性比代码路径更重要。很多 bug 不是因为你写错了寄存器而是因为你把一个 crtc 的 mode 改了却忘了检查另一个 connector 还在用它导致输出冲突。所以我在接触新平台的时候会先把 state 对象、引用计数和回滚路径搞明白再去碰硬件寄存器。凡是按这个顺序做的项目bringup 周期普遍少一个月。跨 DRM 录制这件事本身不是一个新的内核特性而是把 DRM 的 PRIME、DMA-BUF、fence 这几个基础能力组合起来做应用。真正难的也不是调用接口而是理解设备之间的拓扑关系、内存归属和同步语义。先把 DRM 对象模型这个根扎稳后续无论是写驱动、做合成器还是做采集工具你都会有那种“一切尽在掌握”的感觉。希望这篇能让你少走一些我当年走过的弯路。
返回列表