Linux 内核设备链接(Device Links)详解:让驱动核心自动管理设备间依赖
2026/9/17 18:11:43 网站建设 项目流程

Linux 内核设备链接(Device Links)详解:让驱动核心自动管理设备间依赖

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

设备链接(Device Links)是 Linux 内核驱动核心(driver core)中用于表达设备间依赖关系的机制。默认情况下,驱动核心只依据设备树中的父子关系来排序挂起、恢复与关机操作,并不关心兄弟设备之间的先后顺序,更不关心"某个设备必须绑定驱动之后另一个设备才能探测"这类依赖。读完本篇,你将理解设备链接如何在驱动核心中建立"供应商(supplier)/消费者(consumer)"模型、如何用它同时解决 suspend/resume 排序与驱动存在性两大依赖、六个关键标志位(DL_FLAG_*)的语义与非法组合,以及驱动核心内部的状态机与循环依赖检测是如何工作的。

一、问题背景:驱动核心默认只理解父子关系

按 设备链接文档 的阐述,驱动核心的默认行为有两个"盲区":

  1. 排序盲区:挂起、恢复或关机系统时,设备仅按设备层次结构中的父子关系排序——子设备总是先于父设备挂起,父设备总是先于子设备恢复。兄弟设备(siblings)之间、甚至"姑妈/姨妈"(aunts)关系上的先后顺序完全不受约束。
  2. 驱动存在性盲区:驱动核心默认不强制任何"驱动存在依赖",即不保证一个设备必须在另一个设备探测(probe)之前或正常工作之前绑定到驱动。

实际上这两类依赖经常成对出现:一个设备既依赖另一个设备提供正确的 suspend/resume/关机顺序,又依赖对方已经绑定驱动。设备链接正是把这两类依赖统一表达在驱动核心中的机制。

二、核心概念:supplier 与 consumer

在设备链接模型中:

  • supplier(供应商):提供资源/服务的设备;
  • consumer(消费者):依赖该资源的设备。

struct device_link在 include/linux/device.h 中的定义印证了这种双向组织方式:

struct device_link { struct device *supplier; struct list_head s_node; /* 挂入 supplier 的 consumers 链表 */ struct device *consumer; struct list_head c_node; /* 挂入 consumer 的 suppliers 链表 */ struct device link_dev; /* 用于在 sysfs 中暴露链接细节 */ enum device_link_state status; u32 flags; refcount_t rpm_active; struct kref kref; struct work_struct rm_work; bool supplier_preactivated; };

每个设备通过 include/linux/device.h 中的struct dev_links_info维护两组链表:suppliers(指向供应商设备的链接)与consumers(指向消费者设备的链接),外加一个记录驱动状态(DL_DEV_NO_DRIVER/DL_DEV_PROBING/DL_DEV_DRIVER_BOUND/DL_DEV_UNBINDING,见 enum dl_dev_state)的status字段。

设备链接有两种形态:

  • 受管(managed)链接(默认形态,未设置DL_FLAG_STATELESS时):同时保证两类依赖——supplier 与 consumer 之间正确的 suspend/resume 与关机顺序,以及 supplier 上的驱动存在性。consumer 不会在 supplier 绑定驱动之前被探测,也不会在 supplier 解绑之前被解绑。
  • 无状态(stateless)链接(设置DL_FLAG_STATELESS):只保证 suspend/resume 与关机顺序,不强制 supplier 上的驱动存在性。

三、标志位详解(DL_FLAG_*)

所有标志位定义于 include/linux/device.h:

#define DL_FLAG_STATELESS BIT(0) #define DL_FLAG_AUTOREMOVE_CONSUMER BIT(1) #define DL_FLAG_PM_RUNTIME BIT(2) #define DL_FLAG_RPM_ACTIVE BIT(3) #define DL_FLAG_AUTOREMOVE_SUPPLIER BIT(4) #define DL_FLAG_AUTOPROBE_CONSUMER BIT(5)
标志语义典型使用场景
DL_FLAG_STATELESS不需要驱动存在性依赖,只要求正确的 suspend/resume 与关机顺序只需要恢复/挂起顺序的场合
DL_FLAG_PM_RUNTIME集成 runtime PM:只要 consumer 处于 runtime resumed 状态,就 runtime 恢复 supplier 并保持其活动MMU 为 busmaster 服务的电源域场景
DL_FLAG_RPM_ACTIVE创建链接时 runtime 恢复 supplier,并阻止 consumer runtime 挂起之前 supplier 先挂起链接从 consumer 的->probe回调中添加时
DL_FLAG_AUTOREMOVE_CONSUMER当 consumer 探测失败或日后解绑时,自动清除链接链接从 consumer 的->probe回调中添加时
DL_FLAG_AUTOREMOVE_SUPPLIER当 supplier 探测失败或日后解绑时,自动清除链接链接从 supplier 的->probe回调中添加时
DL_FLAG_AUTOPROBE_CONSUMERsupplier 成功绑定驱动后,驱动核心自动为 consumer 探测驱动受管链接且未设置任一 AUTOREMOVE 标志时

需要注意的关键约束(文档与源码一致):

  • DL_FLAG_AUTOREMOVE_CONSUMERDL_FLAG_AUTOREMOVE_SUPPLIERDL_FLAG_AUTOPROBE_CONSUMER三者与DL_FLAG_STATELESS的任何组合均非法。这一约束在device_link_add()入口直接体现,见 drivers/base/core.c:
struct device_link *device_link_add(struct device *consumer, struct device *supplier, u32 flags) { if (!consumer || !supplier || consumer == supplier || flags & ~DL_ADD_VALID_FLAGS || (flags & DL_FLAG_STATELESS && flags & DL_MANAGED_LINK_FLAGS) || (flags & DL_FLAG_AUTOPROBE_CONSUMER && flags & (DL_FLAG_AUTOREMOVE_CONSUMER | DL_FLAG_AUTOREMOVE_SUPPLIER))) return NULL;

即:非法标志组合、self-link(consumer == supplier)、同时要求 AUTOPROBE 与任一 AUTOREMOVE,都会直接返回NULL

  • 若同时设置DL_FLAG_PM_RUNTIMEDL_FLAG_RPM_ACTIVEdevice_link_add()会在建链时立即对 supplier 执行pm_runtime_get_sync();失败则回退pm_runtime_put_noidle()并返回NULL(drivers/base/core.c)。若未设置DL_FLAG_PM_RUNTIMEDL_FLAG_RPM_ACTIVE会被忽略。
  • 同时设置两个 AUTOREMOVE 标志没有意义(DL_FLAG_AUTOREMOVE_SUPPLIER的生命周期更长),内核会优先保留DL_FLAG_AUTOREMOVE_SUPPLIER并清掉DL_FLAG_AUTOREMOVE_CONSUMER(drivers/base/core.c)。

四、使用时机与并发安全约束

文档给出了添加设备链接的时序与一致性要求,这些是驱动开发者最容易踩坑的地方:

  1. 最早时机:supplier 已调用device_add(),consumer 已调用device_initialize()之后。
  2. 避免与 suspend/resume 转换并发:不允许在挂起/恢复转换"进行到一半"时添加链接。可选方案:
    • lock_system_sleep()阻止挂起/恢复转换开始;
    • 或从保证不会与挂起/恢复转换并发执行的上下文中添加,例如设备的->probe回调、或启动期的 PCI quirk。
  3. 驱动存在性依赖的微妙时序:若一个带驱动存在性依赖的链接是在 consumer 的->probe回调中才添加,而此时 supplier 尚未开始探测——如果驱动核心早知道这条链接,它根本不会去探测 consumer。因此责任在 consumer 一侧:添加链接后必须检查 supplier 的驱动是否已经存在,若不存在则推迟探测(defer probing)。有一个合法例外:supplier 仍在探测过程中时建链也是允许的,但前提是 consumer 在创建链接这一刻就能确定 supplier 已经可用(例如 consumer 刚刚获取到"只有 supplier 正常工作才可能存在的资源")。
  4. 无状态链接的对称性惯例:在 supplier 或 consumer 驱动->probe中添加的DL_FLAG_STATELESS链接,通常应在对应驱动的->remove中删除。这样当驱动编译为模块时,链接随模块加载而添加、随卸载而有序删除。删除与添加受同样的限制约束(例如不得与挂起/恢复转换并发)。
  5. 受管链接由驱动核心自动删除:无需(也不允许)手动删除,删除时机由DL_FLAG_AUTOREMOVE_CONSUMER/DL_FLAG_AUTOREMOVE_SUPPLIER决定。

五、已知限制(Limitations)

文档明确列出了若干限制,驱动作者必须了解:

  1. 探测可能被无限期推迟:受管链接的驱动存在性依赖会导致 consumer 的探测被无限期推迟。若 consumer 必须在某个 initcall 级别之前完成探测,这会成为问题;更糟的是,如果 supplier 驱动被拉黑(blacklist)或根本不存在,consumer永远不会被探测
  2. 受管链接不能直接删除:它们由驱动核心在"不再需要时"按照两个 AUTOREMOVE 标志的语义自动删除。
  3. 无状态链接必须由调用方删除:谁调用device_link_add()添加的,就应通过device_link_del()device_link_remove()移除。
  4. DL_FLAG_RPM_ACTIVE+DL_FLAG_STATELESS的引用计数陷阱:若对同一 consumer-supplier 对连续两次调用device_link_add()且中间没有移除链接,随后移除链接时 supplier 的 PM-runtime 使用计数可能残留为非零(内核宁可让它保持非零,也不愿在 consumer 仍处于 PM-runtime-active 时让 supplier 被挂起)。文档给出的规避方法:在两次device_link_add()与随后的device_link_del()/device_link_remove()之间,让 consumer 至少 runtime 挂起一次,或在禁用 PM-runtime 的情况下为其调用pm_runtime_set_suspended()
  5. 可选资源不在设备链接的适用范围:某些驱动依赖可选资源(如 SPI 控制器可用 DMA 引擎或退回 PIO 模式)。probe 时资源缺席的情况下,无法判断它是"即将由 supplier 驱动探测后出现"还是"永远不会出现",因而无法决定是否推迟探测。让驱动在运行时根据资源可用性切换工作模式远比基于 probe 推迟的机制复杂,所以可选资源被明确排除在设备链接的范畴之外。

六、文档中的五个典型应用场景

以下例子完整继承自 设备链接文档 的 Examples 章节,是判断"该不该用设备链接"的参考基准:

  1. MMU 与 busmaster 设备:MMU 与 busmaster 设备同处一个电源域,MMU 为 busmaster 做 DMA 地址翻译。希望 busmaster 处于 runtime resumed 期间 MMU 被 runtime 恢复并保持活动,且 busmaster 驱动不得先于 MMU 绑定。解法:从 busmaster(consumer)向 MMU(supplier)添加一条带 runtime PM 集成的设备链接,其 runtime PM 效果与"MMU 是 busmaster 的父设备"等价。之所以不用struct dev_pm_domain/struct generic_pm_domain,是因为这两者面向"共享一个电源开关的独立设备",而 MMU 设备是服务于busmaster 的——离开后者它毫无用处。设备链接在设备之间制造了一种"合成的层次关系",更贴切。
  2. Thunderbolt 主机控制器:控制器包含若干 PCIe 热插拔端口和一个用于管理 PCIe 交换芯片的 NHI 设备。从系统睡眠恢复时,NHI 必须先重建到已接入设备的 PCI tunnel,热插拔端口才能恢复。热插拔端口在设备层次上却是 NHI 的"姑妈辈",无法靠父子关系自动排序。解法:从热插拔端口(consumers)向 NHI(supplier)添加设备链接;此场景无需驱动存在性依赖(即用DL_FLAG_STATELESS)。
  3. 混合图形笔记本的独显 HDA 控制器:独显通常带有用于 HDMI/DP 音频的 HDA 控制器。设备层次上 HDA 控制器是 VGA 设备的兄弟,但两者共享同一电源域,且 HDA 控制器仅在 HDMI/DP 显示器接入 VGA 设备时才有用。从 HDA 控制器(consumer)到 VGA 设备(supplier)的设备链接恰当地表达了这种关系。
  4. ACPI 的 _DEP 对象:ACPI 允许通过 _DEP 对象定义设备启动顺序。经典例子:某设备的 ACPI 电源管理方法用 I²C 访问实现,必须有一个特定的 I²C 控制器存在且正常工作,该设备的电源管理才能工作。
  5. SoC 中的功能依赖:某些 SoC 上,显示、视频编解码与视频处理 IP 核功能性地依赖处理突发访问和压缩/解压缩的透明内存访问(TMA)IP 核。

七、与替代方案的对比

文档 Alternatives 章节给出了与两种电源域机制的取舍,整理如下:

机制suspend/resume 顺序runtime PM 状态跟踪关机顺序驱动存在性依赖备注
设备链接自动保证DL_FLAG_PM_RUNTIME时由 PM 核心维护自动保证受管链接保证轻量;可表达兄弟设备等非父子关系
struct dev_pm_domain不保证(需自行实现)不跟踪,不保证"全部 runtime 挂起才关电源"不能强制不能强制可覆盖总线/类/设备类型回调;面向共享单一开关的独立设备
struct generic_pm_domain不保证重量级电源域对象不能强制不能强制比设备链接重得多;且不能用于 ACPI 系统

从文档的论述看,若设备是"共享一个电源开关的独立实体",电源域机制更合适;若一个设备服务于另一个设备、离开它没有意义,设备链接(合成层次关系)是更贴切的工具。

八、实现原理:从树到有向无环图

设备层次结构本身是一棵树;加入设备链接后,它变成一个有向无环图(DAG)。排序机制如下(依据 设备链接文档 的 Implementation 章节):

  • suspend/resume 顺序dpm_list决定;关机顺序devices_kset决定。
  • 没有设备链接时,这两个列表是设备树的"扁平化一维表示":通过自顶向下遍历 ACPI 命名空间或 OpenFirmware 设备树,把设备按发现顺序追加到列表尾部,从而保证任何设备位于其所有祖先之后。
  • 一旦添加设备链接,列表还需满足新约束:设备必须位于其所有 supplier(递归地)之后。为此,添加链接时会把 consumer 及其整个子图(consumer 的所有子设备和消费者)移动到列表末尾——即device_link_add()内部调用device_reorder_to_tail()。这一点也被 drivers/base/core.c 的注释印证:"建链的副作用是将 dpm_list 与 devices_kset 重排,把 consumer 及所有依赖它的设备移到这两个列表的末尾"。
  • 循环依赖检测:为防止向图中引入依赖环,添加链接时会验证 supplier 不依赖于 consumer 或 consumer 的任何子设备/消费者(device_link_add()内部调用device_is_dependent(),见 drivers/base/core.c)。若约束被违反,device_link_add()返回NULL并记录一条WARNING

一个值得注意的推论:该检查会阻止从父设备到子设备添加设备链接,但允许从子设备到父设备添加。由于驱动核心本就保证父/子之间的 suspend/resume 与关机顺序,这种反向链接只有在"额外需要驱动存在性依赖"时才有意义——此时驱动作者应仔细权衡:设备链接是否真的是合适的工具?更合适的做法可能只是使用 deferred probing,或添加一个设备标志使父驱动先于子驱动探测。

九、状态机:链接生命周期的六个状态

链接状态枚举定义于 include/linux/device.h:

enum device_link_state { DL_STATE_NONE = -1, DL_STATE_DORMANT = 0, DL_STATE_AVAILABLE, DL_STATE_CONSUMER_PROBE, DL_STATE_ACTIVE, DL_STATE_SUPPLIER_UNBIND, };

状态转移图(原文档 State machine 章节):

.=============================. | | v | DORMANT <=> AVAILABLE <=> CONSUMER_PROBE => ACTIVE ^ | | | '============ SUPPLIER_UNBIND <============'

各状态的含义与驱动(触发点)如下,括号内为文档给出的调用链:

状态含义何时进入
DL_STATE_DORMANTsupplier 与 consumer 两侧均无驱动创建链接前的自动初始状态:若在任何设备探测之前创建链接,即为 DORMANT
DL_STATE_AVAILABLEsupplier 驱动已存在,consumer 尚无驱动supplier 绑定驱动时,指向其 consumers 的链接推进到 AVAILABLE(driver_bound()调用device_links_driver_bound()
DL_STATE_CONSUMER_PROBEconsumer 正在探测(supplier 驱动存在)consumer 探测前,really_probe()调用device_links_check_suppliers()验证 supplier 驱动存在(consumer 不在 wait_for_suppliers 列表中,且指向 supplier 的链接处于 AVAILABLE),随后状态更新为 CONSUMER_PROBE;device_links_unbind_consumers()中的wait_for_device_probe()会阻止 supplier 此时解绑
DL_STATE_ACTIVEsupplier 与 consumer 的驱动均存在consumer 探测成功后,driver_bound()调用device_links_driver_bound()推进链接
DL_STATE_SUPPLIER_UNBINDsupplier 驱动正在解绑supplier 驱动移除前,指向"未绑定驱动的 consumers"的链接被更新为该状态(__device_release_driver()调用device_links_busy()),从而阻止这些 consumers 再绑定(再次经由really_probe()device_links_check_suppliers()拦截)

其余转移规则:

  • 探测失败回退:consumer 探测失败时,指向 suppliers 的链接退回DL_STATE_AVAILABLEreally_probe()调用device_links_no_driver())。
  • consumer 驱动移除:consumer 的驱动日后被移除时,链接退回DL_STATE_AVAILABLE__device_release_driver()经由device_links_driver_cleanup()调用__device_links_no_driver())。
  • supplier 解绑流程__device_release_driver()调用device_links_unbind_consumers()——已绑定驱动的 consumers 被从驱动上释放(unbind),正在探测的 consumers 会被等待到探测完成;当所有指向 consumers 的链接都进入SUPPLIER_UNBIND后,supplier 驱动才真正释放,链接退回DL_STATE_DORMANTdevice_links_driver_cleanup())。

十、API 速查

设备链接的公开 API 声明于 include/linux/device.h,实现于 drivers/base/core.c:

struct device_link *device_link_add(struct device *consumer, struct device *supplier, u32 flags); void device_link_del(struct device_link *link); void device_link_remove(void *consumer, struct device *supplier);
  • device_link_add():创建(或获取已存在的)链接。要求 supplier 在调用时已注册(否则返回NULL),consumer 则无需已注册。注意:若该 consumer-supplier 对的链接已存在,会直接返回既有链接(无论其当前类型与状态,链接的标志可能被更新),调用方需视同刚创建的链接处理。
  • device_link_del()/device_link_remove():用于删除无状态链接(后者通过 consumer 指针与 supplier 指针定位,适用于不再持有struct device_link *的场景,如模块卸载)。
  • 受管链接的返回值仅用于判断链接是否存在,其删除完全交给驱动核心按 AUTOREMOVE 标志执行。

小结

设备链接机制以"supplier/consumer + 六个标志位"的极简模型,把 Linux 内核中跨设备的两大类依赖——suspend/resume/关机顺序与驱动存在性——统一交由驱动核心管理:受管链接在device_link_add()时重排dpm_list/devices_kset、通过device_is_dependent()防止依赖环,并经由 DORMANT → AVAILABLE → CONSUMER_PROBE → ACTIVE 的状态机与device_links_*()系列函数把 supplier 的绑定/解绑与 consumer 的探测严格串联起来。对于兄弟设备间的顺序、MMU/总线主控这类"服务型"依赖、Thunderbolt NHI 或独显 HDA 音频等场景,设备链接比电源域机制更轻量、更贴切;而理解第五节列出的探测推迟、计数残留等限制,是正确选择并安全使用这一机制的前提。

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询