资讯动态

DRM KMS初始化和清理详解:顺序依赖与常见崩溃排查

发布时间:2026/10/26 3:00:49 来源:尧图企业网站定制
1. 从一次黑屏事故说起KMS初始化和清理到底在做什么几年前接手一个显示子系统的调试任务板子上电后内核日志一切正常DRM设备节点也注册成功了但屏幕就是黑的。查了两天才发现问题出在KMS初始化的顺序上——connector在encoder之前完成了注册导致后续的探测流程拿不到有效的显示通路。那次经历让我意识到KMS的初始化和清理虽然看起来只是“申请资源、释放资源”的流水账但里面的顺序依赖、状态机流转、引用计数管理任何一个环节出问题都会让整个显示管线瘫痪。这篇内容聊的就是DRM框架里KMSKernel Mode Setting的初始化与清理流程。具体来说我会把drm_mode_config_init、drmm_mode_config_init、drm_mode_config_cleanup这几条主线拆开讲清楚每个阶段内核到底做了什么、为什么这么做、顺序为什么不能乱、清理时哪些坑最容易踩。适合正在写DRM驱动、调试显示管线、或者单纯想搞明白“内核是怎么把一块屏幕点亮的”的开发者参考。不管你是刚接触DRM的新手还是已经写过几个驱动但清理路径总是出问题的老手下面这些内容应该都能对上你的某些困惑。KMS的本质是一套显示管线的抽象和管理框架。它把CRTC、encoder、connector、plane这些硬件模块抽象成软件对象通过mode_config统一管理它们的生命周期。初始化就是把这些对象一个个创建出来、挂到全局链表上、建立彼此之间的关联清理则是反过来把关联拆掉、对象销毁、内存回收。听起来简单但实际操作中初始化顺序决定了后续所有功能的可用性清理顺序决定了模块卸载时会不会panic。这两件事做对了显示子系统才算真正稳了。2. KMS初始化的整体设计与顺序逻辑2.1 为什么初始化要分阶段进行KMS初始化不是一个函数调用就能搞定的事。内核把它拆成了几个阶段mode_config初始化、对象创建与注册、late_register回调、以及最终的管线激活。这种分阶段设计不是随意为之而是由硬件探测的实际约束决定的。显示硬件的探测往往依赖总线枚举的结果。比如一个DP connector它的存在与否取决于AUX通道能不能读到EDID一个DSI panel它的初始化时序依赖供电和复位引脚的状态。这些条件在驱动probe的早期阶段未必全部满足。所以内核把初始化拆成“先建框架、再填内容、最后激活”三步让驱动可以在不同时间点完成不同层次的准备工作。另一个原因是错误回滚的需要。如果所有初始化都塞在一个函数里中间任何一步失败前面的工作全部白费而且很难精确回滚。分阶段之后每个阶段有明确的成功/失败边界失败时只需要回滚当前阶段及之前的状态清理逻辑也更清晰。从代码层面看drm_mode_config_init负责的是第一阶段——初始化mode_config结构体本身设置默认的回调函数指针初始化各种链表头和互斥锁。这个阶段不涉及任何硬件操作纯粹是软件数据结构的准备。drmm_mode_config_init是它的托管版本把清理工作挂到devres机制上驱动卸载时自动触发。对象创建阶段则由驱动自己调用drm_crtc_init_with_planes、drm_encoder_init、drm_connector_init等函数完成每个函数内部会做参数校验、对象初始化、链表插入等操作。2.2 mode_config结构体的核心字段与初始化动作struct drm_mode_config是整个KMS子系统的中枢。它里面有几个关键字段值得单独拎出来说。num_crtc、num_encoder、num_connector、num_total_plane这几个计数器记录了当前注册的对象数量。初始化时它们被清零后续每注册一个对象就递增。这些计数器不只是统计用途很多遍历操作和合法性检查都依赖它们。比如drm_mode_config_cleanup在清理时会根据这些计数判断是否有残留对象。crtc_list、encoder_list、connector_list、plane_list是四个链表头分别挂载对应类型的对象。初始化时用INIT_LIST_HEAD把它们置为空链表。后续所有对象的注册本质上就是往这些链表里插入节点清理则是遍历链表逐个销毁。mutex是mode_config的全局互斥锁保护所有KMS对象的状态变更。初始化时通过mutex_init完成。这把锁的粒度比较粗覆盖整个mode setting操作所以在高频更新场景下可能成为瓶颈但胜在逻辑简单、不容易出死锁。min_width、max_width、min_height、max_height定义了显示管线的尺寸范围。驱动需要根据硬件能力设置这些值内核在后续的mode校验中会用它们做边界检查。如果设得太小合法的显示模式会被拒绝设得太大非法模式可能通过校验导致硬件异常。poll_enabled、poll_running这些字段和热插拔检测相关初始化时清零。fb_base记录framebuffer的基地址某些老式硬件需要它来做地址映射。drm_mode_config_init的实现大致是这样的先对传入的dev做空指针检查然后逐字段初始化。链表头用INIT_LIST_HEAD互斥锁用mutex_init计数器清零回调函数指针设为NULL或默认实现。最后设置dev-mode_config指向这个结构体完成绑定。整个过程不涉及任何硬件访问也不应该失败——如果这里失败了说明传入的参数有问题或者内存已经损坏。2.3 托管版本drmm_mode_config_init的取舍drmm_mode_config_init是drm_mode_config_init的devres托管版本。它的核心逻辑和后者一样区别在于初始化完成后会通过devm_add_action_or_reset注册一个清理回调把drm_mode_config_cleanup挂到设备的资源管理链上。这样驱动卸载时devres框架会自动调用清理函数不需要驱动手动处理。这个设计的好处是减少了驱动代码里的清理样板。传统写法需要在驱动的remove回调里显式调用drm_mode_config_cleanup而且必须保证调用顺序正确——先清理KMS对象再释放其他资源。用托管版本之后清理时机由devres统一管理驱动只需要关注业务逻辑。但托管版本也不是没有代价。它要求驱动的probe流程必须严格遵循devres的规则所有通过devm_系列函数申请的资源释放顺序和申请顺序相反。如果驱动在drmm_mode_config_init之后又手动申请了一些非托管资源并且在remove回调里提前释放了它们就可能出现清理时访问已释放内存的问题。我见过一个案例驱动在mode_config初始化后手动申请了一块DMA缓冲区remove时先释放了缓冲区然后devres触发mode_config清理清理过程中某个回调又去访问了那块缓冲区直接触发use-after-free。所以选择哪个版本取决于驱动的资源管理策略。如果驱动整体采用devres风格用drmm_mode_config_init更省事如果驱动有大量手动管理的资源或者清理顺序有特殊要求用drm_mode_config_init加显式清理更可控。两者不能混用——同一个dev上不能既调drm_mode_config_init又调drmm_mode_config_init否则清理会被执行两次。2.4 对象注册的顺序依赖关系CRTC、encoder、connector、plane这四类对象的注册顺序是有讲究的。内核文档里没有强制规定必须谁先谁后但从实际运行逻辑来看合理的顺序应该是plane → CRTC → encoder → connector。plane先注册是因为CRTC初始化时需要绑定primary plane。drm_crtc_init_with_planes的签名里要求传入primary和cursor plane的指针如果plane还没创建这个调用就没法完成。当然你也可以先创建CRTC再单独调drm_crtc_set_planes绑定但那样多了一步而且中间状态不一致容易出问题。CRTC在encoder之前是因为encoder需要知道它对应哪个CRTC。drm_encoder_init本身不强制要求指定CRTC但后续的drm_encoder_bind_crtc或者atomic模式下的路由计算都依赖这个关联。如果encoder先于CRTC注册驱动就得在CRTC创建后回头去补绑定关系代码更啰嗦。connector最后注册是因为它的探测流程可能依赖encoder的状态。比如一个HDMI connector在detect回调里需要读取EDID而EDID读取可能要通过encoder的I2C通道。如果encoder还没初始化完这个调用就会失败。另外connector注册后会触发一次初始状态探测如果此时encoder和CRTC还没就绪探测结果就是错的。这个顺序不是绝对的。某些硬件架构下connector的物理探测和encoder完全独立先注册connector也没问题。但遵循plane → CRTC → encoder → connector这个顺序能覆盖绝大多数场景减少调试时的困惑。3. 核心细节解析每个初始化步骤到底在做什么3.1 drm_mode_config_init的内部动作拆解把drm_mode_config_init拆开看它做的事情可以分成四组。第一组是链表初始化。crtc_list、encoder_list、connector_list、plane_list、property_list、privobj_list这些链表头全部用INIT_LIST_HEAD置空。这一步的意义在于给后续的注册操作提供一个合法的插入点。如果某个链表头没初始化第一次往里面插节点时就会踩到野指针。第二组是锁和同步原语初始化。mutex_init(mode_config-mutex)初始化全局互斥锁。部分内核版本还会初始化mode_config-connection_mutex用于保护connector的状态变更。这些锁在后续的atomic commit、热插拔检测、sysfs属性读写中都会被用到。第三组是默认值设置。min_width和min_height默认设为0max_width和max_height默认设为0——注意这里默认值是0意味着驱动必须显式设置合理的范围否则后续所有mode校验都会失败。fb_base设为0。poll_enabled设为false。async_page_flip根据内核配置决定。第四组是回调函数指针的初始化。mode_config-funcs指向驱动提供的drm_mode_config_funcs结构体里面包含fb_create、atomic_check、atomic_commit等关键回调。如果驱动没有提供这些回调后续的framebuffer创建和atomic提交就会失败。drm_mode_config_init本身不设置funcs需要驱动在调用之后自己赋值。这里有一个容易忽略的细节drm_mode_config_init不会检查dev-mode_config是否已经被初始化过。如果驱动不小心调了两次第二次会覆盖第一次的链表头和锁状态导致第一次注册的对象全部丢失清理时直接panic。所以驱动里一定要确保这个函数只被调用一次通常放在probe函数的早期且只在一个路径上调用。3.2 CRTC初始化的参数选择与常见陷阱drm_crtc_init_with_planes是创建CRTC的主要接口。它的参数包括dev、crtc结构体指针、primary plane、cursor plane、funcs回调和driver_private数据。primary plane是必传的。如果传NULL函数会返回错误。这是因为现代DRM驱动要求每个CRTC至少有一个primary plane来承载扫描输出。cursor plane是可选的传NULL表示该CRTC不支持硬件光标内核会用软件方式模拟。funcs回调里最重要的是atomic_duplicate_state、atomic_destroy_state、atomic_reset这三个。它们负责CRTC状态的分配、销毁和重置。如果驱动使用atomic模式这三个回调必须实现否则atomic commit流程会在状态管理阶段失败。我见过有驱动只实现了atomic_duplicate_state但忘了atomic_destroy_state结果每次commit都泄漏一份状态内存跑几个小时就OOM了。drm_crtc_init_with_planes内部会做几件事检查primary plane是否为空、检查crtc是否已经初始化、调用drm_crtc_init完成基础初始化、把primary和cursor plane绑定到crtc、把crtc插入mode_config-crtc_list、递增num_crtc。如果中间任何一步失败函数会回滚已做的操作并返回错误码。一个常见的陷阱是plane的possibile_crtcs掩码设置。每个plane有一个possible_crtcs字段表示它可以被哪些CRTC使用。这个掩码在plane创建时设置CRTC初始化时会检查primary plane的掩码是否包含当前CRTC的索引。如果不包含绑定会失败。很多驱动在创建plane时随手填了一个掩码没仔细核对CRTC的索引分配顺序结果CRTC初始化时报“plane not possible for crtc”的错误。解决办法是在plane创建前先确定好CRTC的索引或者用drm_crtc_index动态获取。3.3 encoder与connector的注册细节drm_encoder_init的参数相对简单dev、encoder结构体、可能的encoder类型、funcs回调、driver_private。encoder类型是一个位掩码表示这个encoder支持哪些输出接口HDMI、DP、DSI等。这个类型信息在后续的connector绑定和用户空间查询中会用到。encoder的funcs里最重要的是atomic_duplicate_state、atomic_destroy_state、atomic_reset和CRTC类似。另外detect回调在部分场景下也会用到但现代驱动更多把探测逻辑放在connector的detect里。drm_connector_init的参数包括dev、connector结构体、funcs回调、connector类型。connector类型决定了用户空间看到的接口名称HDMI-A-1、DP-1等。funcs里必须实现detect、fill_modes、get_modes这几个回调否则connector注册后无法正常探测和枚举模式。connector初始化时还会创建一批默认属性比如DPMS、EDID、PATH等。这些属性通过sysfs暴露给用户空间用于查询和控制连接器状态。如果驱动需要额外的自定义属性可以在drm_connector_init之后调用drm_connector_attach_xxx_property系列函数添加。connector和encoder的绑定通过drm_connector_attach_encoder完成。这个函数把encoder指针加入connector的encoder_ids数组同时更新encoder的possible_connectors掩码。绑定关系建立后connector在探测时就能通过encoder访问到对应的CRTC和plane。这里有一个顺序上的细节drm_connector_attach_encoder必须在connector注册之后、late_register之前调用。如果在注册之前调用connector的encoder_ids数组还没初始化写入会越界。如果在late_register之后调用某些依赖绑定关系的初始化动作比如DP MST的拓扑建立可能已经执行过了绑定不会生效。3.4 late_register回调的时机与用途late_register是connector的一个可选回调在drm_connector_register阶段被调用。这个阶段的含义是“connector已经完成了所有软件层面的初始化现在可以对外暴露了”。late_register的典型用途包括创建sysfs属性文件、注册背光设备、初始化硬件相关的探测通道。为什么需要这个回调而不是直接在初始化函数里做因为有些操作依赖于connector已经注册到全局列表、用户空间已经能看到设备节点。比如创建一个sysfs属性如果connector还没注册device_create_file的目标设备可能还不存在。late_register提供了一个“万事俱备、只差暴露”的时间点。对应的early_unregister回调在drm_connector_unregister阶段调用用于撤销late_register里做的事情。这两个回调必须成对实现否则清理时会出现资源泄漏或者sysfs文件残留。一个实际踩过的坑某驱动在late_register里注册了一个中断处理程序用于检测HDMI热插拔。但early_unregister里忘了注销这个中断。模块卸载后中断仍然挂着下一次插入HDMI线时中断触发访问了已经释放的connector结构体直接oops。所以late_register和early_unregister的对称性检查应该成为代码review的固定项。4. 清理流程drm_mode_config_cleanup的完整拆解4.1 清理的总体顺序与设计意图drm_mode_config_cleanup的清理顺序和初始化顺序大致相反但不是简单的逆序。它的实际执行顺序是先清理connector再清理encoder然后清理CRTC最后清理plane中间穿插属性清理和framebuffer清理。为什么connector先清理因为connector持有对encoder的引用还可能持有对CRTC的引用通过当前绑定的模式。如果先清理encoderconnector的清理过程中访问encoder就会踩空。同理encoder持有对CRTC的引用CRTC持有对plane的引用所以清理顺序必须是connector → encoder → CRTC → plane。这个顺序在代码里的体现是drm_mode_config_cleanup先遍历connector_list对每个connector调用drm_connector_cleanup然后遍历encoder_list调用drm_encoder_cleanup接着遍历crtc_list调用drm_crtc_cleanup最后遍历plane_list调用drm_plane_cleanup。每清理一个对象对应的计数器递减链表节点被移除。属性清理穿插在对象清理之间。每个connector、encoder、CRTC、plane都可能挂载了自定义属性这些属性在对象清理时一并销毁。另外mode_config-property_list里还有一批全局属性比如TV_MARGIN、DEGAMMA_LUT等它们在所有对象清理完之后统一销毁。framebuffer的清理在对象清理之前还是之后取决于内核版本。较新的内核里framebuffer通过drm_framebuffer_cleanup在drm_mode_config_cleanup的早期阶段处理因为framebuffer可能被plane引用必须在plane销毁之前释放。如果顺序反了plane清理时访问已释放的framebuffer就会出问题。4.2 connector清理中的引用计数与状态销毁drm_connector_cleanup做的事情比看起来多。它首先要处理connector的引用计数。每个connector有一个refcount字段初始为1注册时设置。用户空间打开设备节点、sysfs属性被访问、内核内部持有引用时计数会递增。清理函数会先递减这个计数如果计数不为零说明还有地方持有引用清理会延迟到最后一个引用释放时执行。这个机制的目的是防止connector在还被使用时被销毁。但实际调试中引用计数泄漏是常见问题。比如某个驱动在detect回调里对connector调了drm_connector_get但忘了对应的drm_connector_put导致connector永远无法清理模块卸载时卡住。排查这类问题可以用/sys/kernel/debug/dri/*/connectors下的调试信息或者在内核里加tracepoint跟踪引用计数的变化。引用计数归零后drm_connector_cleanup会依次做这些事调用early_unregister回调如果实现了、从connector_list移除、销毁所有挂载的属性、释放EDID数据、销毁connector状态、释放proc节点如果有、最后释放connector结构体本身如果是动态分配的。EDID数据的释放容易被忽略。connector在探测时可能通过drm_add_edid_modes加载了EDID这块内存挂在connector-edid_blob_ptr上。如果驱动在清理前手动释放了EDID但没有清空指针drm_connector_cleanup会再次释放导致double-free。正确的做法是让清理函数统一处理驱动不要手动干预。4.3 CRTC与plane清理的依赖处理drm_crtc_cleanup的核心工作是解除CRTC和plane的绑定、销毁CRTC状态、从全局链表移除。它首先检查CRTC是否还有绑定的plane如果有逐个解绑。解绑操作会清除plane的crtc指针和CRTC的primary/cursor指针确保双方不再互相引用。然后销毁CRTC的当前状态和所有pending状态。atomic模式下CRTC可能维护着多个状态对象current、pending、commit时的临时状态。清理函数会遍历这些状态并逐个销毁。如果驱动自定义了状态结构体并在atomic_duplicate_state里分配了额外内存atomic_destroy_state必须负责释放这些内存否则清理时只释放了基础结构体额外内存就泄漏了。drm_plane_cleanup相对简单从plane_list移除、销毁plane状态、释放格式信息数组、释放属性。plane的格式信息format_types和format_count在初始化时通过drm_universal_plane_init设置清理时由内核统一释放驱动不需要手动处理。这里有一个隐蔽的依赖plane清理时如果还被某个CRTC引用plane-crtc不为空内核会打印警告但继续清理。这会导致CRTC的primary指针变成悬空指针后续如果还有代码访问这个CRTC就会崩溃。所以清理前必须确保所有CRTC已经解绑了plane。drm_mode_config_cleanup的顺序保证了这一点——CRTC在plane之前清理CRTC清理时会解绑所有plane。4.4 清理阶段的常见崩溃场景与规避清理阶段最常见的崩溃是use-after-free。典型场景是驱动在remove回调里先释放了某个自定义数据结构然后devres触发drm_mode_config_cleanup清理过程中某个回调比如atomic_destroy_state又去访问那个数据结构。规避方法是确保自定义数据的释放时机在mode_config清理之后或者把自定义数据的释放也挂到devres上让释放顺序自动正确。第二个常见问题是清理不完整导致的资源泄漏。比如connector的late_register里创建了sysfs文件但early_unregister里没删除。模块卸载后/sys/class/drm/下残留一堆文件再次加载模块时创建同名文件失败。这类问题的排查可以用ls /sys/class/drm/对比加载前后的差异或者用kmemleak检查内存泄漏。第三个问题是清理顺序错误导致的死锁。drm_mode_config_cleanup会获取mode_config-mutex如果驱动在持有其他锁的情况下调用清理而清理过程中的某个回调又去获取那把锁就会死锁。比如驱动在持有自己的dev_lock时调用drm_mode_config_cleanup而某个connector的early_unregister回调里也去获取dev_lock直接卡死。解决办法是清理前释放所有驱动私有的锁或者确保回调里不获取这些锁。5. 实操过程从零搭建一个最小KMS初始化与清理流程5.1 驱动probe阶段的初始化代码骨架下面这段代码展示了一个最小化的KMS初始化流程基于常见的PCI显示驱动结构。实际驱动会根据硬件差异调整但主干逻辑是一致的。static int demo_drm_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct drm_device *drm; struct demo_device *demo; int ret; drm drm_dev_alloc(demo_drm_driver, pdev-dev); if (IS_ERR(drm)) return PTR_ERR(drm); demo devm_kzalloc(pdev-dev, sizeof(*demo), GFP_KERNEL); if (!demo) { ret -ENOMEM; goto err_free_drm; } demo-drm drm; drm-dev_private demo; ret drmm_mode_config_init(drm); if (ret) goto err_free_drm; drm-mode_config.min_width 0; drm-mode_config.min_height 0; drm-mode_config.max_width 4096; drm-mode_config.max_height 4096; drm-mode_config.funcs demo_mode_config_funcs; ret demo_planes_init(drm); if (ret) goto err_cleanup; ret demo_crtcs_init(drm); if (ret) goto err_cleanup; ret demo_encoders_init(drm); if (ret) goto err_cleanup; ret demo_connectors_init(drm); if (ret) goto err_cleanup; drm_mode_config_reset(drm); ret drm_dev_register(drm, 0); if (ret) goto err_cleanup; return 0; err_cleanup: drm_mode_config_cleanup(drm); err_free_drm: drm_dev_put(drm); return ret; }这段代码里有几个关键点值得展开。drmm_mode_config_init在drm_dev_alloc之后立即调用确保mode_config在任何对象创建之前就绪。max_width和max_height根据硬件能力设置为4096这个值要覆盖所有支持的显示模式否则高分辨率模式会被拒绝。funcs在mode_config初始化之后赋值因为drmm_mode_config_init内部不会设置它。drm_mode_config_reset在所有对象注册完成后调用它的作用是把每个对象的状态重置为初始值。这个调用不是必须的但推荐加上——它确保对象状态和硬件实际状态一致避免后续第一次commit时出现状态不一致的警告。错误处理路径统一跳到err_cleanup调用drm_mode_config_cleanup。注意这里用的是非托管版本因为drmm_mode_config_init已经注册了devres清理回调如果这里再手动调一次清理会执行两次。所以要么用drmm_mode_config_init且不在错误路径手动清理要么用drm_mode_config_init且手动清理。上面代码里用的是drmm_mode_config_init但错误路径又手动调了drm_mode_config_cleanup这是一个常见的错误写法——实际上应该只依赖devres错误路径直接drm_dev_put即可。我故意保留这个写法是为了说明这个坑混用托管和非托管清理会导致double-free。正确的写法是用drmm_mode_config_init时错误路径只调drm_dev_put让devres在设备释放时自动清理用drm_mode_config_init时错误路径手动调drm_mode_config_cleanup且remove回调里也要调一次。5.2 plane和CRTC初始化的具体参数plane初始化用drm_universal_plane_init。参数包括dev、plane结构体、possible_crtcs掩码、格式数组、格式数量、plane类型、funcs、name。static const uint32_t demo_primary_formats[] { DRM_FORMAT_XRGB8888, DRM_FORMAT_ARGB8888, DRM_FORMAT_RGB565, }; ret drm_universal_plane_init(drm, demo-primary_plane, 1 0, demo_primary_formats, ARRAY_SIZE(demo_primary_formats), NULL, demo_plane_funcs, DRM_PLANE_TYPE_PRIMARY, primary);possible_crtcs传的是1 0表示这个plane只能被索引为0的CRTC使用。如果驱动有多个CRTC需要根据实际路由关系设置掩码。格式数组里列出的格式是plane支持的所有像素格式用户空间在创建framebuffer时会根据这个列表做校验。如果格式列表不全某些应用可能无法创建framebuffer。CRTC初始化用drm_crtc_init_with_planesret drm_crtc_init_with_planes(drm, demo-crtc, demo-primary_plane, NULL, demo_crtc_funcs, crtc-0);cursor plane传NULL表示不支持硬件光标。如果硬件支持需要先创建一个DRM_PLANE_TYPE_CURSOR类型的plane再传进来。demo_crtc_funcs里必须实现atomic_duplicate_state、atomic_destroy_state、atomic_reset否则atomic commit会失败。5.3 connector注册与late_register的完整实现connector初始化分三步drm_connector_init、drm_connector_attach_encoder、drm_connector_register。ret drm_connector_init(drm, demo-connector, demo_connector_funcs, DRM_MODE_CONNECTOR_HDMIA); if (ret) return ret; ret drm_connector_attach_encoder(demo-connector, demo-encoder); if (ret) return ret; demo-connector.polled DRM_CONNECTOR_POLL_HPD; ret drm_connector_register(demo-connector); if (ret) return ret;polled字段设置为DRM_CONNECTOR_POLL_HPD表示该connector支持热插拔检测。如果硬件不支持HPD可以设为DRM_CONNECTOR_POLL_CONNECT | DRM_CONNECTOR_POLL_DISCONNECT让内核定期轮询。late_register回调在drm_connector_register内部被调用。一个典型的实现是创建背光设备static int demo_connector_late_register(struct drm_connector *connector) { struct demo_connector *demo_conn to_demo_connector(connector); demo_conn-backlight backlight_device_register(demo-backlight, connector-dev-dev, demo_conn, demo_backlight_ops, NULL); if (IS_ERR(demo_conn-backlight)) return PTR_ERR(demo_conn-backlight); return 0; } static void demo_connector_early_unregister(struct drm_connector *connector) { struct demo_connector *demo_conn to_demo_connector(connector); backlight_device_unregister(demo_conn-backlight); }这两个回调必须成对实现。late_register返回错误时drm_connector_register会失败驱动需要回滚之前的操作。early_unregister在connector清理时被调用负责释放late_register里申请的资源。5.4 清理路径的代码实现与验证方法如果使用drmm_mode_config_init清理路径不需要驱动做任何事——devres会在设备释放时自动调用drm_mode_config_cleanup。驱动的remove回调只需要释放自己手动申请的资源static void demo_drm_remove(struct pci_dev *pdev) { struct drm_device *drm pci_get_drvdata(pdev); drm_dev_unregister(drm); drm_dev_put(drm); }drm_dev_unregister会触发connector的early_unregisterdrm_dev_put在引用计数归零时触发devres清理进而调用drm_mode_config_cleanup。整个流程不需要驱动显式调用清理函数。验证清理是否完整的方法有几个。第一模块卸载后检查dmesg里有没有“BUG: KASAN: use-after-free”或者“WARNING: ... at drm_mode_config_cleanup”之类的信息。第二用kmemleak扫描看有没有残留的drm相关内存块。第三反复加载卸载模块几十次观察内存占用是否持续增长。第四检查/sys/class/drm/下有没有残留的connector目录。我自己的习惯是在remove回调里加一行drmm_mode_config_init的对称检查——虽然devres会自动清理但手动确认一下drm-mode_config.num_connector等计数器在清理前是否归零能提前发现引用计数泄漏。如果计数器不为零说明有对象没被正确清理需要进一步排查。6. 常见问题与排查技巧实录6.1 初始化失败时的错误码解读KMS初始化各阶段返回的错误码有比较明确的含义熟悉这些能大幅缩短排查时间。错误码常见触发点排查方向-EINVALdrm_mode_config_init参数为空、plane格式数组为空、connector类型非法检查传入的结构体指针和枚举值-ENOMEM对象分配失败、状态分配失败检查内存压力确认没有内存泄漏-ENOSPCCRTC数量超限、connector数量超限检查num_crtc等计数器是否超过硬件上限-EIOencoder绑定失败、EDID读取失败检查硬件连接和I2C通道-EEXIST重复注册同名对象检查是否调了两次初始化函数-EINVAL是最常见的。drm_crtc_init_with_planes在primary plane为NULL时返回-EINVALdrm_connector_init在connector类型超出DRM_MODE_CONNECTOR_*范围时返回-EINVAL。遇到这个错误码先检查参数合法性再看内核日志里有没有更具体的提示。6.2 清理阶段崩溃的典型堆栈分析清理阶段的崩溃堆栈通常指向drm_mode_config_cleanup内部的某个kfree或者回调调用。根据堆栈特征可以快速定位问题类型。如果堆栈顶部是kfree且调用链经过drm_connector_cleanup或drm_crtc_cleanup大概率是double-free。原因通常是驱动手动释放了某个对象清理函数又释放了一次。排查方法是检查驱动里有没有对connector-edid_blob_ptr、crtc-state、plane-state这些字段手动调kfree。如果堆栈顶部是某个驱动的回调函数比如demo_atomic_destroy_state说明清理过程中回调访问了已释放的数据。典型场景是驱动在remove里先释放了私有数据然后devres触发清理回调里又去访问私有数据。解决办法是调整释放顺序或者把私有数据的释放也挂到devres上。如果堆栈里出现mutex_lock且卡住不返回说明死锁。检查清理路径上是否持有其他锁以及回调里是否尝试获取同一把锁。6.3 引用计数泄漏的定位方法connector引用计数泄漏的表现是模块卸载时drm_connector_cleanup里的refcount递减后不为零清理被延迟但延迟的清理永远不会到来最终模块卸载卡住或者内存泄漏。定位方法一在drm_connector_get和drm_connector_put里加printk打印调用者和当前计数。跑一遍加载卸载流程看计数在哪里增加了但没有对应减少。方法二用/sys/kernel/debug/dri/*/connectors查看每个connector的引用计数。正常情况下列表里每个connector的计数应该是1注册时的初始引用。如果大于1说明有泄漏。方法三用ftrace跟踪drm_connector_get和drm_connector_put的调用栈对比两者的调用次数和调用者。如果某个函数只调get不调put就是泄漏点。我遇到过一次泄漏原因是connector的detect回调里对connector调了drm_connector_get但探测失败时提前返回跳过了drm_connector_put。修复方法是在所有返回路径上都确保put被调用或者用goto统一清理。6.4 热插拔场景下的初始化时序问题热插拔场景对KMS初始化的时序要求更严格。一个典型问题是connector注册后立即触发了一次HPD中断中断处理程序调用drm_helper_hpd_irq_event后者去读取connector状态但此时encoder或CRTC还没完全初始化读取失败connector被标记为disconnected用户空间看到的就是“设备存在但无显示”。规避方法是在connector注册前确保encoder和CRTC已经就绪并且在late_register里再启用HPD中断。这样中断触发时所有依赖对象都已经初始化完成。另外drm_connector_register之后可以手动调一次drm_helper_connector_dpms或者触发一次探测确保初始状态正确。另一个时序问题是清理时的HPD中断。如果connector正在清理但HPD中断还没禁用中断触发后访问已释放的connector结构体就会崩溃。所以early_unregister里必须先禁用HPD中断再释放其他资源。这个顺序不能反。6.5 调试工具与日志开关速查排查KMS初始化清理问题几个工具和日志开关能省很多时间。CONFIG_DRM_DEBUG_KMS打开后内核会打印每个KMS操作的详细日志包括对象创建、状态变更、commit流程。日志量很大建议只在复现问题时临时打开。/sys/kernel/debug/dri/*/目录下有丰富的调试信息connectors列出所有connector及其状态crtcs列出CRTC配置framebuffer列出当前framebuffer。清理前后对比这些文件的内容能快速发现残留对象。kmemleak适合检测内存泄漏。打开CONFIG_DEBUG_KMEMLEAK加载卸载模块后执行echo scan /sys/kernel/debug/kmemleak然后cat /sys/kernel/debug/kmemleak查看泄漏报告。报告里会列出泄漏内存的分配调用栈直接指向问题代码。KASAN适合检测use-after-free和越界访问。打开CONFIG_KASAN后任何非法内存访问都会在发生时立即报告附带完整的调用栈。清理阶段的崩溃用KASAN跑一遍基本都能定位到具体行。最后分享一个我常用的笨办法在drm_mode_config_cleanup的入口和每个子清理函数的入口加pr_info打印当前对象计数。清理完成后对比计数是否归零。如果某个计数不为零就针对那类对象单独排查。这个方法虽然原始但在没有复杂工具的环境下非常有效。

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

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

免费获取报价 →
↑