资讯动态

KMS初始化与清理实战:顺序、对称性与避坑指南

发布时间:2026/10/26 3:00:49 来源:尧图企业网站定制
DRM 这套显示子系统在内核里算得上是比较“重”的一块很多做嵌入式显示、车载座舱或者桌面发行版适配的朋友第一次啃 KMS 代码时都会被那一堆drm_device、drm_mode_config、connector、crtc绕晕。我这次想聊的是 KMS 的初始化和清理这条主线也就是一个 DRM 设备从注册、模式配置初始化、对象创建到最终卸载时怎么把这些资源一层层拆干净。标题里带了个“五”说明这是系列里偏后期的一篇前面大概率已经讲过 DRM 基础对象和驱动注册这里我更聚焦在“初始化顺序”和“清理对称性”这两个最容易出问题的地方。如果你正在写一个 DRM 驱动或者接手了别人的显示驱动要做 bring-up、要做休眠唤醒、要做热插拔稳定性排查那这篇内容应该能帮上忙。我会把 KMS 初始化拆成几个阶段讲清楚每一步在干什么、为什么这么排、哪些坑是文档里不会写的清理部分则重点讲“逆序释放”和“引用计数”这两条铁律。全文基于常见的 DRM 驱动实践来展开具体函数名和结构体以你手上内核版本为准但思路是通用的。1. KMS 初始化整体设计与顺序考量1.1 为什么初始化顺序不能随便调KMS 初始化最核心的一条原则是先有设备再有模式配置再有对象最后才对外暴露。这句话听起来像废话但真到写代码时顺序错一步就是 oops。我见过不少新手驱动把drm_connector_init放在drm_mode_config_init之前结果 connector 里的dev-mode_config还是空的后面一访问就崩。从数据结构依赖来看drm_device是整个 DRM 子系统的根它持有mode_config而mode_config又持有各类对象链表connector_list、crtc_list、encoder_list、plane_list。所以逻辑上必须是分配并初始化drm_device通常由drm_dev_alloc完成调用drm_mode_config_init初始化模式配置把各种 list 头、mutex、IDR 都准备好创建具体的 KMS 对象plane、crtc、encoder、connector并挂到对应链表上注册设备drm_dev_register让用户空间能看到最后才是 framebuffer 相关的运行时操作。这个顺序背后的道理很朴素对象创建时要往链表里挂链表得先存在设备注册后用户空间可能立刻来查询所以对象必须已经建好。你要是把注册提前用户空间一 ioctl 查询 connector驱动还没建完轻则返回空重则空指针。提示不同内核版本里drm_mode_config_init的返回值处理略有差异早期版本返回 void较新版本返回 int写代码时对着自己内核的头文件确认别照抄网上老代码。1.2 初始化阶段划分与职责边界我习惯把 KMS 初始化分成四个阶段每个阶段职责清晰出问题时也容易定位阶段主要动作关键函数常见实践失败后果设备分配分配 drm_device绑定 driverdrm_dev_alloc后续全部无法进行模式配置初始化初始化 mode_config、链表、锁drm_mode_config_init对象无法创建对象创建plane/crtc/encoder/connectordrm_universal_plane_init 等显示通路不完整设备注册对用户空间暴露drm_dev_register用户空间看不到设备这个划分的好处是每一阶段失败都有明确的回滚点。比如对象创建到一半失败你只需要把已创建的对象销毁不用动设备本身设备注册失败则要把对象和 mode_config 全部清掉。很多驱动写崩就崩在“失败路径没想清楚”成功路径写得漂亮一出错就资源泄漏。1.3 与驱动 probe 流程的衔接KMS 初始化通常嵌在驱动的 probe 里。以常见的平台驱动为例probe 大致是拿资源寄存器基址、时钟、中断→ 初始化硬件 → 分配 drm_device → KMS 初始化 → 注册。这里有个经验硬件初始化要放在 KMS 对象创建之前因为创建 connector 时往往要读 EDID 或者探测连接状态硬件没起来读出来就是垃圾。反过来清理时的顺序要严格对称先注销设备再销毁对象再清 mode_config最后释放 drm_device。这个“逆序”不是形式主义而是因为销毁对象时可能还要访问 mode_config 里的锁和链表你先把 mode_config 清了销毁对象就踩空。2. 核心对象创建细节与实操要点2.1 mode_config 初始化里到底做了什么drm_mode_config_init看着就一行调用实际干了不少事。它会把dev-mode_config里的几个关键字段初始化好num_connector、num_encoder、num_crtc、num_total_plane归零初始化connector_list、encoder_list、crtc_list、plane_list这几个链表头初始化idr用于对象 ID 分配还会初始化mutexmode_config.mutex和connection_mutex。这里有个容易忽略的点mode_config 里的 mutex 是后续所有 KMS ioctl 的并发保护基础。你在对象创建阶段如果手动去操作链表记得拿对应的锁虽然初始化阶段通常是单线程的但养成习惯没坏处。另外drm_mode_config_init之后一般还要设置mode_config.min_width/max_width、min_height/max_height这些值决定了用户空间能设置多大的模式设小了高分辨率屏直接不可用设大了又可能超出硬件能力得按实际显示控制器能力填。我踩过的一个坑是max_width填得比硬件实际支持的大结果用户空间尝试设置一个超宽模式驱动在 atomic check 阶段没拦住最后硬件配置寄存器时溢出屏幕花屏。所以这几个边界值一定要对着硬件手册填宁可保守一点。2.2 plane 创建从 primary 到 overlayplane 是 KMS 里最基础的扫描输出单元。创建 plane 用drm_universal_plane_init需要传入 plane 类型DRM_PLANE_TYPE_PRIMARY / OVERLAY / CURSOR、支持的格式列表、格式数量、可能的 crtc 掩码等。实操要点有这么几个。第一primary plane 必须每个 crtc 至少一个否则这个 crtc 没法正常显示atomic 提交时会报错。第二格式列表要按硬件能力填别一股脑把 RGB 和 YUV 全列上硬件不支持的格式列上去用户空间选了这个格式驱动在 atomic check 里又没拒绝最后配置寄存器失败。第三possible_crtcs这个掩码要算对它表示这个 plane 能挂到哪些 crtc 上算错了会导致 plane 分配失败或者显示错位。格式列表的写法通常是定义一个uint32_t formats[]数组然后传给 init 函数。这里注意数组的生命周期有些驱动把格式数组定义在栈上init 函数内部只是拷贝引用还是深拷贝要看实现稳妥做法是定义成静态或者放在驱动私有结构里别用局部栈数组。2.3 crtc 与 encoder 的配对关系crtc 代表扫描输出通路encoder 代表信号编码器比如 HDMI 的 TMDS 编码两者是多对多关系通过drm_encoder_init和drm_crtc_init_with_planes建立再用drm_connector_attach_encoder把 connector 和 encoder 关联起来。这里的关键是possible_crtcs 和 possible_clones 两个掩码。encoder 的possible_crtcs表示它能被哪些 crtc 驱动connector 通过 attach 到 encoder 间接确定可用 crtc。掩码算错是 bring-up 阶段的高频问题表现是用户空间枚举出来的显示配置不对或者设置模式时报“无效的 crtc”。我的经验是把 crtc 和 encoder 的对应关系画个表对着硬件拓扑填掩码别凭感觉。比如两个 crtc、两个 encoder如果硬件上 crtc0 只能接 encoder0crtc1 只能接 encoder1那掩码就是一一对应如果交叉也能走掩码就要把对应位都置上。2.4 connector 创建与 EDID 探测时机connector 是用户空间最常打交道的对象创建用drm_connector_init然后设置connector-display_info、attach encoder、注册 helper funcs。这里最需要注意的是EDID 探测的时机。很多驱动在 connector 创建时就同步读 EDID这在热插拔场景下会出问题设备刚上电显示器还没准备好读 EDID 超时connector 状态就是错的。更稳的做法是把 EDID 读取放到get_modes回调或者热插拔检测回调里按需读取。drm_connector_init阶段只做静态初始化。另外 connector 的polled模式轮询检测连接状态和中断模式要选对。中断模式响应快但要求硬件支持 HPD 中断轮询模式兼容性好但费 CPU。车载和嵌入式场景里如果 HPD 中断不稳定宁可用轮询稳定性优先。注意connector 创建后如果 attach encoder 失败记得把已创建的 connector 清理掉别直接 return否则 connector_list 里留个半成品后续遍历会出问题。3. 完整初始化流程与关键代码环节3.1 一个典型初始化的骨架下面这段是基于常见平台驱动整理的骨架函数名以你手上内核为准重点是顺序和失败处理static int xxx_drm_probe(struct platform_device *pdev) { struct xxx_drm_private *priv; struct drm_device *drm; int ret; priv devm_kzalloc(pdev-dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; /* 1. 硬件资源初始化时钟、寄存器、中断 */ ret xxx_hw_init(pdev, priv); if (ret) return ret; /* 2. 分配 drm_device */ drm drm_dev_alloc(xxx_drm_driver, pdev-dev); if (IS_ERR(drm)) return PTR_ERR(drm); priv-drm drm; drm-dev_private priv; /* 3. 模式配置初始化 */ ret drm_mode_config_init(drm); if (ret) goto err_dev; /* 4. 创建 KMS 对象 */ ret xxx_create_planes(drm); if (ret) goto err_mode_config; ret xxx_create_crtcs(drm); if (ret) goto err_mode_config; ret xxx_create_encoders(drm); if (ret) goto err_mode_config; ret xxx_create_connectors(drm); if (ret) goto err_mode_config; /* 5. 设置模式配置边界 */ drm-mode_config.min_width 0; drm-mode_config.min_height 0; drm-mode_config.max_width 4096; drm-mode_config.max_height 4096; /* 6. 注册设备 */ ret drm_dev_register(drm, 0); if (ret) goto err_mode_config; return 0; err_mode_config: drm_mode_config_cleanup(drm); err_dev: drm_dev_put(drm); return ret; }这段骨架里失败路径的 goto 标签是层层回退的err_mode_config会清掉所有 KMS 对象和 mode_configerr_dev再释放设备。注意drm_mode_config_cleanup本身会遍历各链表销毁对象所以对象创建到一半失败时不需要你手动一个个销毁直接调 cleanup 就行它会处理已挂到链表上的对象。这一点很多人不知道白白写了大量重复清理代码。3.2 对象创建中的参数计算实例拿possible_crtcs掩码来说假设驱动有 2 个 crtc编号 0 和 1。如果某个 plane 只能挂到 crtc0掩码就是1 0 0x1能挂到两个就是(1 0) | (1 1) 0x3。这个位运算看着简单但 crtc 的 index 是在创建时分配的你得确保创建 plane 时 crtc 已经建好否则拿不到正确的 index。再比如格式数量drm_universal_plane_init的format_count参数要和格式数组实际长度一致。我见过有人用ARRAY_SIZE算但数组是指针传进来的ARRAY_SIZE算出来是指针大小除以元素大小结果完全错。正确做法是在定义数组的作用域里算好长度传进去或者用宏配合固定长度数组。3.3 初始化阶段的并发与中断处理初始化阶段通常是单线程的但有个例外如果你在 probe 里就使能了 HPD 中断中断处理函数可能在对象还没建完时就被触发。这时候中断处理里如果去访问 connector就可能拿到半成品。稳妥做法是中断使能放在drm_dev_register之后或者中断处理里先判断设备是否已注册。另外drm_dev_register之后用户空间可能立刻发起 ioctl所以注册前所有对象必须就绪且 mode_config 的锁已经可用。注册后再去补建对象是危险操作用户空间可能正在遍历链表。4. 清理流程与资源释放的对称性4.1 逆序释放为什么是铁律清理的核心原则就一条按初始化的逆序释放。初始化是设备→mode_config→对象→注册清理就是注销→对象/mode_config→设备。drm_mode_config_cleanup会负责销毁所有 KMS 对象并清理 mode_config 内部资源drm_dev_put负责释放设备引用。为什么不能反过来因为销毁对象时drm_mode_config_cleanup要遍历 mode_config 里的链表你先把 mode_config 清了链表头都没了遍历就是野指针。同理注销设备要在销毁对象之前否则用户空间还在访问对象就被拆了典型的 use-after-free。4.2 引用计数与 drm_dev_put 的正确用法drm_dev_alloc拿到的设备带一个初始引用drm_dev_put减引用减到零才真正释放。这里常见的错误是重复 put 或者忘记 put。重复 put 会导致引用计数下溢忘记 put 则设备泄漏。在 probe 失败路径里如果drm_dev_alloc成功但后续失败必须drm_dev_put。在 remove 路径里先drm_dev_unregister再drm_mode_config_cleanup最后drm_dev_put。注意drm_dev_unregister和drm_dev_put是两回事前者是把设备从用户空间可见列表里摘掉后者是减引用两个都要做。4.3 清理阶段的常见泄漏点我整理了几个清理阶段的高频泄漏点做成表格方便对照泄漏点表现排查方法connector 未销毁内存缓慢增长检查 cleanup 是否被调用encoder 引用未释放卸载后模块无法重载看 encoder_list 是否清空fbdev 未清理卸载时 oops确认 drm_fbdev 相关清理中断未释放卸载后中断仍触发检查 free_irq 调用时钟未关闭功耗异常检查 clk_disable_unprepare其中 fbdev 这块特别容易漏。很多驱动用了drm_fbdev_generic_setup但清理时忘了对应的 fbdev 清理结果卸载时 framebuffer 还在被访问。稳妥做法是在 remove 里先处理 fbdev再走 KMS 清理。提示调试资源泄漏时可以在 cleanup 前后打印各链表的节点数确认是否归零。这个方法土但有效比盯着代码看强。5. 常见问题排查与实操避坑5.1 初始化失败怎么定位初始化失败最常见的是对象创建返回错误。定位思路是看返回值-ENOMEM是内存问题-EINVAL多半是参数不对格式列表、掩码、类型-ENODEV可能是硬件没起来。我一般会在每个创建函数后加日志打印返回值快速缩小范围。还有一种隐蔽的失败是“创建成功但显示不对”。比如 connector 建好了但用户空间枚举不到模式多半是get_modes回调没实现或者 EDID 没读到。这时候用modetest之类的工具枚举一下看 connector 状态和模式列表比看代码快。5.2 卸载时 oops 的典型原因卸载 oops 十有八九是清理顺序错了或者有对象没清。典型场景先drm_mode_config_cleanup再drm_dev_unregister用户空间在中间窗口访问设备对象已销毁直接崩。或者 fbdev 没清framebuffer 还在被 console 使用。排查方法是看 oops 的调用栈如果落在drm_mode_config_cleanup里的链表遍历说明有对象在 cleanup 之后还被访问如果落在 fb 相关函数就是 fbdev 没清干净。我遇到过一个是中断处理函数在设备注销后还在跑访问了已释放的私有数据这种要在 remove 里先disable_irq再清理。5.3 休眠唤醒对初始化清理的影响休眠唤醒本质上是“清理一部分 重新初始化一部分”。suspend 时通常要保存 KMS 状态、关闭显示通路resume 时恢复。这里的关键是不要重复初始化 mode_config 和对象它们是常驻的休眠唤醒只处理硬件状态和 framebuffer。我见过驱动在 resume 里重新调drm_mode_config_init结果链表被重置原来的对象全丢了唤醒后显示直接黑屏。正确做法是 suspend 里用drm_mode_config_helper_suspend保存状态resume 里drm_mode_config_helper_resume恢复对象和 mode_config 不动。5.4 一份速查表现象可能原因处理方向probe 返回 -EINVAL格式列表或掩码参数错核对格式数组长度和 crtc 掩码用户空间枚举不到 connectorconnector 未注册或 attach 失败检查创建和 attach 返回值设置模式失败max_width/height 太小或格式不支持调整边界值核对格式卸载 oops清理顺序错或对象泄漏按逆序清理检查 fbdev唤醒黑屏resume 重复初始化用 helper 保存恢复状态6. 我在实际项目里的几点体会做显示驱动这些年KMS 初始化这块最深的体会是顺序和对称性比代码技巧重要得多。你把初始化顺序理清楚把失败路径的逆序释放写对大部分稳定性问题就没了。反过来代码写得再花哨顺序一乱问题就是随机的今天能跑明天崩。另一个体会是别迷信“能显示就行”。bring-up 阶段屏幕亮了不代表初始化是对的很多隐患比如掩码算错、边界值不对在简单场景下不暴露一到多屏、高分辨率、热插拔就全出来了。我的习惯是初始化完成后用工具把 connector、crtc、plane 的枚举结果打出来对着硬件拓扑核一遍确认无误再往下走。最后分享一个小技巧调试清理流程时可以在drm_mode_config_cleanup前后加计数打印确认各链表节点数归零。这个习惯帮我抓到过好几次隐蔽的泄漏比事后用内存检测工具省事。至于这个系列后续还可以往 atomic 提交路径和自刷新PSR方向扩展那是 KMS 运行时另一块硬骨头。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑