☰
Linux显示驱动绕不开DRM:从KMS原子提交到跨设备录制实践
2026/10/8 11:42:53 网站建设 项目流程

最近公司在调一款国产SoC的显示控制器驱动,白天还在代码里跟 vblank 中断较劲,晚上群里就有人问:“DRM 到底是什么?为什么所有显示驱动都在说 DRM?”这问题其实很典型——看内核文档、看 libdrm、看 Weston 源码,到处是 DRM,可很多人只把它当成一个库或一层接口,没真正想明白它在显示驱动里的位置。这里先把一个容易混淆的点说清楚:在 Linux 图形/显示领域,DRM 不是“数字版权管理”,而是 Direct Rendering Manager,即 Linux 内核里的直接渲染管理器。它既是一套内核框架,也是一组用户态 API,更是当代显示驱动绕不开的核心路径。这篇文章就围绕这三点展开,适合写驱动的同学、搞图形栈移植的工程师,也适合想弄明白屏幕录制工具为什么总跟 DRM 扯上关系的爱好者。

先从一个大家都能遇到的画面说起。把一个 HDMI 输出从 1920x1080 切到 3840x2160,老式驱动往往黑屏一瞬,屏幕像被拔掉重插;而走现代 DRM 的机器,切换过程几乎无感,最多闪一帧。为什么?因为 DRM 的 KMS 子系统用原子提交把所有状态变更打包处理,而不是每次改一个寄存器。所以,今天再写显示驱动,绕开 DRM 几乎等于绕开整个现代 Linux 图形生态。

1. 从闪屏和撕裂说起:你说的 DRM 到底管的是哪一段活

如果把显示链路比作一条流水线:应用或 GPU 渲染出帧,把帧放进显存,显示控制器再周期性地把像素从显存读出来,经过编码器变成 HDMI/DP/MIPI 信号送到屏幕。DRM 恰好横跨了从“帧”到“显示接口”的整个管理过程:管显存分配(GEM)、管显示输出模式(KMS)、管 GPU/显示控制器的同步(fence),还管让多个用户态程序安全共享显卡资源。

很多人第一次接触“DRM”这三个字母时,会跑去搜出一堆版权保护的内容,那是 Digital Rights Management,跟显示驱动没有关系。在 Linux 内核和开源图形栈的语境里,DRM 就是 Direct Rendering Manager。我建议检索时加上“kernel / KMS / libdrm”这类关键词,才不会跑偏。

那么 DRM 具体管了什么?可以拆成两大部分来看。第一部分是 KMS(Kernel Mode Setting),它管的是“显示模式”:分辨率、刷新率、像素时钟、行场同步时序,以及 CRTC、Encoder、Connector、Plane 这些显示管线的对象模型。第二部分是 GEM(Graphics Execution Manager),它管的是“显存对象”:创建 Buffer Object、映射到用户空间、跨进程传递、以及与 GPU 命令提交的同步。简单说,KMS 负责把画面准确地送到屏幕,GEM 负责让画面所在的显存被安全高效地使用。

一个显示驱动挂到 DRM 框架上之后,用户态就不再需要直接操作寄存器了。Xorg、Wayland 合成器、FFmpeg 的 kmsgrab、以及各种截屏工具,全部通过 /dev/dri/card0 或 /dev/dri/renderD128 调用 DRM 的 ioctl 来工作。这也是“显示驱动绕不开 DRM”最直白的原因:生态已经统一到了这套接口上。

2. 拆开 DRM 的内核家底:KMS 与 GEM 两条主线

2.1 KMS 这半边:显示输出管线的四件套

KMS 全称 Kernel Mode Setting,它最核心的贡献是把显示链路抽象成了四个内核对象:PLANE、CRTC、ENCODER、CONNECTOR。

  • PLANE(平面):可以理解成显示控制器里的图层。一个 Plane 承载一块 framebuffer,支持缩放、旋转、格式转换,多个 Plane 可以被硬件叠加混合。典型例子:背景层、视频播放层、鼠标光标层。
  • CRTC(显示引擎/扫描器):按固定的像素时钟从 Plane 取数据,生成一行行的像素流,并输出行场同步信号。它是整个显示管线的“节拍器”。
  • ENCODER(编码器):把 CRTC 输出的并行像素信号转换成 HDMI、DP、eDP、DSI、LVDS 这类物理接口信号。
  • CONNECTOR(连接器):对应物理接口和显示设备,比如 HDMI 座、DP 座、eDP 面板、MIPI 屏。它负责读取 EDID、上报分辨率列表。

我个人喜欢用一个类比:Plane 是几张叠放在一起的幻灯片,CRTC 是投影仪的灯泡和镜头,Encoder 是投到墙上之前的信号转换器,Connector 就是墙上的插座。用户态干的事情,无非是选哪几张幻灯片、用哪个投影仪、投到哪个插座。

在现代 SoC 的显示控制器里,多 Plane 叠加是标配能力。驱动初始化时要做的第一件事,就是把硬件支持的 Plane 全部注册进 DRM,告诉用户态“我支持几个主平面、几个光标平面、哪些格式和 modifier”。这也是为什么新建驱动不能简单像 fbdev 那样“给个内存地址就能显示”——那根本没把硬件的合成、叠加能力暴露出来。

2.2 GEM 这半边:显存对象、PRIME 与 fence

GEM 解决的是显存对象的创建、管理和共享。一个用户态程序想要一块显存,就通过 DRM 的 ioctl 创建 Buffer Object,得到一个 handle;需要 CPU 直接读写时,把 handle mmap 到用户空间;需要跨进程传递时,通过 PRIME 机制把它导出成 dma-buf 文件描述符。

最简单的显示驱动可以用 DRM_IOCTL_MODE_CREATE_DUMB 分配一块 dumb buffer,然后 mmap 出来写像素,再作为 framebuffer 提交给 KMS。很多嵌入式方案里的 UI 层就是这么跑的。但现代 GPU 驱动的显存要复杂得多:可能是 tiled 布局、带压缩、有各种 format modifier,显存对象的管理必须更抽象,这就是 GEM 从早期“Graphics Execution Manager”演变到今天依然存在的原因。

另一个 GEM 的核心内容是 fence(栅栏)。显示控制器读显存和 CPU/GPU 写显存,两者之间的时机需要同步。没有 fence,CPU 可能刚写了一半,显示控制器就把帧扫出去了,于是出现撕裂。DRM 里的同步对象(drm_syncobj)和 dma_fence 就是解决这个问题的:GPU 完成渲染后发出 signal,显示控制器等信号后才去读帧。录制屏幕的时候也一样——如果你拿到的 framebuffer 还在被 GPU 写入,不等待 fence 就直接读,抓出来的必然是半张残帧甚至黑屏。

2.3 Atomic 原子提交:为什么最终都收敛到它

早期的 KMS 接口是分步操作的:drmModeSetCrtc 设置分辨率,drmModeSetPlane 设置图层,一个配置项一个配置项地改。但现实中,一次分辨率切换往往要同时动 CRTC、Encoder、Plane、飞机带宽、时钟等一系列状态。中间任何一步临时状态不合法,屏幕就会闪烁、黑屏或显示错乱。

原子提交(Atomic commit)的思路是:把一次更新描述成一个完整的状态对象,内核里通过 drm_atomic_commit 先做校验,再一步提交;要么全部成功,要么失败回滚到旧状态。你改分辨率时,Plane 格式、CRTC 时钟、Connector 带宽这些约束是一起校验的,不合法就直接拒绝,而不是让用户态在黑屏中慢慢试错。

对驱动开发者来说,这意味着大部分工作量集中在两个回调上:atomic_check负责检查状态组合是否合法,atomic_update负责把新状态真正落到底层寄存器。现在的 KMS 已经全面原子化,新的显示驱动如果还用旧的 set_config 路径,在上游基本不会被接受。写驱动时第一步就要想清楚:哪些属性需要走 atomic、校验逻辑写在哪一层、vblank 上报和 fence 怎么配合。

3. 显示驱动为什么必须姓“DRM”:新旧框架的账本对比

3.1 fbdev 不是不能用,而是卡在了新时代的门槛上

在 DRM 成熟之前,Linux 显示输出主要靠 fbdev,也就是 /dev/fb0 那套框架。嵌入式老工程师对它的感情很深:调一块 LCD 屏,fbset 改分辨率,往 fb0 里写像素,画面立刻就出来了。但是今天再看,fbdev 的问题非常明显。

  • 它只有一个 framebuffer 的概念,表达不了多屏拼接、旋转、缩放、多 Plane 叠加。
  • 它没有 CRTC/Encoder/Connector 这样的物理链路模型,动态电源管理、HDMI 热插拔、EDID 处理都很难扩展。
  • 它没有 fence 和 dma-buf 的统一机制,GPU 渲染完还得自己去查事件,现代合成工作流无法顺畅对接。
  • 它不知道 modifier,也就是显存布局信息,面对 tiled 格式只能瞎猜,一旦猜错就是花屏。

所以内核现在把 fbdev 降级成一个兼容层:底层依然走 DRM,再由 drm_fb_helper 模拟出 /dev/fb0 给旧应用使用。如果你在某块板子的内核里看到 drivers/video/fbdev 目录,那多半是为了兼容旧程序,而不是新的主路径。我见过不少项目想在新平台上继续用 fbdev 快速出图,最后都折回来走 DRM——因为用户态工具、合成器、编码器根本不认 fbdev 那套私有接口。

3.2 新驱动统一挂在 drivers/gpu/drm 下:一个最小驱动要准备什么

读者可以打开内核源码看一眼 drivers/gpu/drm 目录:i915、amdgpu、msm、vc4、rockchip、mediatek,以及大量行业方案,全部挂在这个框架下。一个新显示驱动要进入这个体系,至少需要准备四块内容:drm_driver 注册、KMS 对象初始化、GEM 相关回调、中断与状态同步。

一个最小驱动的骨架大概是这样的(只列关键部分示意):

static const struct drm_driver my_display_driver = { .driver_features = DRIVER_MODESET | DRIVER_GEM | DRIVER_ATOMIC, .fops = &my_drm_fops, .prime_handle_to_fd = drm_gem_prime_handle_to_fd, .prime_fd_to_handle = drm_gem_prime_fd_to_handle, }; static int my_display_probe(struct platform_device *pdev) { struct drm_device *dev = drm_dev_alloc(&my_display_driver, &pdev->dev); drm_mode_config_init(dev); // 注册 CRTC、Encoder、Connector、Plane // 注册各项属性,如 rotation、scaling mode drm_dev_register(dev, 0); }

但这只是骨架。真正能点亮屏幕,还需要为每个 Connector 实现 get_modes 回调读取 EDID 或面板参数,为整个管线实现 atomic_check / atomic_update,提供 vblank 中断处理,并在中断里调用 drm_crtc_handle_vblank 上报。

被问得最多的一个问题是:为什么不能把所有功能做成 fbdev,自己加点私有 ioctl?答案很简单:上游不会收,下游也不会为你的私有接口适配生态。Xorg、Wayland、kmscube、FFmpeg 全部认 DRM,只有 DRM 是所有显示相关组件共同承认的内核边界。

3.3 硬件能力已经不允许我们绕开 DRM

现代 SoC 的显示控制器天生就是按多 Plane、旋转、Alpha 混合、HDR、VRR(可变刷新率)这些能力设计的。举个例子,某平台要在 MIPI DSI 屏上同时显示主界面和鼠标光标,硬件明明有叠加层,用 fbdev 只能把光标软件合并进主 framebuffer;用 DRM 只需要注册两个 Plane,让用户态直接提交两层。薄一层拷贝,省下的 CPU/内存带宽在低端平台上非常明显。

另一个例子是高清屏的缩放。DRM Plane 支持 scaling mode 属性,硬件可以直接把低分辨率视频放大到全屏,用户态只要设置属性,不用动帧数据。这类特性如果每个厂商都搞一套私有实现,工作量不谈,光兼容性测试就能让人崩溃。所以“为什么显示驱动绕不开它”最朴素的回答就是:硬件已经绕不开了。

4. 用户态为什么也绕不开:合成、Wayland 与“跨 DRM 录制”

4.1 /dev/dri/card0、renderD128 与 master 权限

DRM 在 /dev/dri 下提供两类设备。card0 这类 primary node 负责 KMS 控制、模式设置、扫描输出,使用它需要 DRM master 权限,而且同一时间只能有一个 master;renderD128 这类 render node 只负责渲染计算,可以做显存分配和 GPU 命令提交,不需要 master,所以普通应用都能打开。

这个分工对安全很关键:不能让每个 App 都去切分辨率、关 CRTC、动显示链路。现代桌面里,Xorg 或 Wayland 合成器持有 card0 的 master,应用走 render 节点提交渲染结果。如果应用要截屏,成熟的做法是走合成器协议,而不是绕过协议去抢 master。

“跨 DRM 录制”这个说法,严格来说不是某个官方 API,而是社区里描述一类场景的约定俗成说法:帧从一个 DRM 设备或一个 Plane 出来,经过 dma-buf 传递、格式转换或额外拷贝,最终送到编码器或虚拟显示设备。比如在 FFmpeg 里你能看到-f kmsgrab直接抓 DRM 的 framebuffer,再用 hwupload 转给 GPU 做后续处理,这中间跨了 DRM 和编码器两套体系,很容易出问题。

4.2 合成器、屏幕录制与 kmsgrab 的关系

Wayland 合成器(Weston、wlroots 派生的各类 compositor)负责把应用窗口合成到一块输出 framebuffer 上,然后通过 drmModeAtomicCommit 提交给 KMS。如果 KMS 上有多个 Plane,合成器可以把视频窗口放在一个独立 Plane 上,实现 scanout bypass,完全避开 GPU 合成。

屏幕录制工具(比如 grim、wl-recorder)走的是合成器提供的 screencopy 协议拿到帧,再转成 PNG 或交给编码器。底层来看,拿到的究竟是不是原始 DRM framebuffer,取决于合成器是走 Plane 还是走合成路径,但用户态看到的通常已经是 dma-buf,编码器直接导入即可。

如果你直接用 kmsgrab 抓卡:

  • 抓到的 Buffer 可能带 modifier,也就是 tiled 或压缩布局,不能当线性 RGB 直接读。
  • 帧可能还在被显示控制器使用,抓取前要等 fence,否则抓到半帧。
  • 同一时刻可能有多个 Plane 在叠加,单一 framebuffer 抓出来并不是最终画面。

我在实际调录制链路时,最少见的坑反而是权限:直接在 root 下打开 card0 会报 DRM master 权限问题,因为当前会话的 master 在合成器手里。想测 kmsgrab,要么在纯控制台环境、要么把合成器停掉,总之别指望跟正在跑的桌面共存。

4.3 “跨 DRM 录制”的典型形态和推荐做法

“跨 DRM 录制”最常见的三种形态:

第一种是多 GPU/混合显卡系统。渲染发生在 A 卡的 renderD128,显示输出走 B 卡的 card0,录制要从 A 卡显存拿帧。正确做法是等渲染 fence 完成后,用 PRIME 把 A 卡 buffer 导出成 dma-buf fd,传给编码器的 DRM/VAAPI 路径,再把处理后的帧送给 B 卡显示或虚拟设备。中间任何一步试图直接读原始显存,都会遇到格式不对或同步不全的问题。

第二种是虚拟化/云显示场景。宿主机的 DRM framebuffer 需要送到虚拟机里的虚拟显示设备,virtio-gpu、vmwgfx 这类虚拟设备本质上也是 DRM 驱动,但它们的 buffer 生命周期、modifier 和物理设备不一样,跨界引用经常出现 handle 冲突或者引用计数问题。推荐做法是走设备自己的 copy 协议,或者通过 dma-buf 显式地 export/import,而不是直接复制 /dev/dri/card0 的抓取结果。

第三种是格式/布局转换。录制端希望得到 NV12 线性数据,但显示控制器只能输出 tiled modifier 的 NV12,不能直接把内存抄走。要先导出 dma-buf,用 GPU blit 或计算着色器做一次 modifier 转换,再交给编码器。这也是很多人抱怨“kmsgrab 抓出来花屏”的真正原因——根本不是抓取逻辑错了,是没做转换。

我总结下来的推荐链路是:先获取 DRM master 帧 → 导出 dma-buf → 等待 fence → 用 GPU 做格式/modifier 转换 → 最后导入编码器。如果你只是想录桌面,更省事的路径是直接走合成器的 screencopy 协议,因为合成器已经帮你把各种 Plane 和 format 处理好了,“跨 DRM”的复杂度被整体封装掉了。

5. 上手 DRM 开发最容易踩的坑和我推荐的路径

5.1 从工具到源码:建议的入门顺序

别一上来就造轮子。先在装了 DRM 驱动的机器上跑一条命令:

modetest -M card0 -p

它会列出所有 Connector、CRTC、Plane 以及每层的 format/modifier 支持列表。再用drm_info这种工具看更细的属性树。然后跑kmscube,验证 KMS 基本扫描输出链路是通的,再拿 Weston 或 wlroots 源码当教科书读。

想读内核的话,我推荐从drivers/gpu/drm/tiny/simpledrm.c看起。这个驱动很小,覆盖了 simple KMS helper、dumb buffer 创建和驱动的注册流程,是最干净的入门样本。等把这块读明白了,再去看 i915 或 msm 这类大型驱动,就不会被各种宏和回调淹没。

我个人的建议是给自己定一个可验证的目标:让一块普通 LCD 屏正常点亮,支持原子提交,能通过 modetest 切换分辨率。然后再逐步添加多 Plane、modifier、VRR 这类进阶能力。每个阶段都以“用户态能通过 DRM 标准接口控制”为标准,而不是“我自己能写寄存器”。

5.2 高频问题清单:现象、根因与对策

我把这几年调试 DRM 相关问题时最常遇到的几类情况整理了一下,方便直接对照排查。

现象根因对策
打开 /dev/dri/card0 报 Permission denied没有 DRM master 权限,master 可能在合成器或登录会话手中用 loginctl 激活会话,或在纯控制台环境测试,不要靠 chmod 裸授权
drmModeAtomicCommit 返回 EINVALPlane 支持不了你提交的 format/modifier,或属性组合不合法先用 modetest 查 Plane 支持列表,提交时加 TEST_ONLY 标志迭代验证
切分辨率黑屏几秒提交没等 vblank,或带宽约束没通过看 dmesg 里 atomic_check 的失败日志,配合 drm.debug 打开原子提交调试
跨 DRM 录制花屏modifier 不匹配,得到的 buffer 不是线性布局导出 dma-buf 后用 GPU blit 做 modifier 转换,再交给编码器
GPU 渲染完抓帧是黑的没等待 fence,抓到了还没写入完成的 buffer抓帧前等待同步对象,或用 poll 等待 dma-buf fence 信号
多屏复制,只有主屏有画面Encoder/Connector 配对错误,或硬件 pipeline 不支持检查 mode_config 里 CRTC/Encoder/Connector 拓扑,逐一确认链路

5.3 两个非常实用的调试技巧

第一个是内核启动参数drm.debug=0x1f。它会打开 DRM 驱动的各种调试日志,包括原子提交的 check 和 commit 过程。遇到黑屏、闪屏、提交失败时,这条日志能直接告诉你是哪个属性、哪个格式出了问题,省去反复猜硬件的痛苦。

第二个技巧是学会用 TEST_ONLY 提交。原子提交支持DRM_MODE_ATOMIC_TEST_ONLY标志,只校验、不真正生效。每次改属性前先做一次 TEST_ONLY 提交,合法性验证通过再去掉这个标志做正式提交。我写很多调试小工具都是这个套路:先测试,再提交,失败后立刻能定位到具体属性,而不是等屏幕坏了再去翻寄存器。

最后说下我个人的习惯。调不同板子的 DRM 驱动时,我会把每块板子的 modetest 输出存成一份基线日志,每次改驱动或改设备前先 diff 一下。遇到花屏和录制异常,第一件事永远是查 modifier 和 fence,这两项至少能解决掉七成以上的“跨 DRM”疑难杂症。希望这篇能给准备碰显示驱动的朋友指个方向,少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询