[TOC]
drivers/base/dd.c 驱动延迟探测(Driver Deferred Probing) 管理设备依赖和初始化顺序
drivers/base/dd.c
是Linux内核驱动核心(Driver Core)的一个关键文件,它实现并管理着**驱动延迟探测(Deferred Probing)**机制。这个机制是现代Linux内核设备模型正常运转的基石,其核心功能是解决设备驱动初始化过程中的依赖顺序问题。当一个设备的驱动在尝试“探测”(probe,即初始化)时,如果它所依赖的其他资源(如时钟、电源、GPIO等)尚未准备就绪,该机制允许该驱动的探测操作被安全地推迟,并在稍后合适的时机自动重试。
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术是为了解决在复杂的片上系统(SoC)中普遍存在的设备依赖和初始化顺序问题而诞生的。
- 复杂的硬件依赖:现代硬件中,一个设备(如一个I2C音频解码器)的正常工作通常依赖于多个其他硬件模块。例如,它需要一个时钟控制器为其提供时钟信号,需要一个电源管理芯片(PMIC)为其提供稳定的电压,还可能需要一个GPIO控制器来控制其复位引脚。
- 不确定的初始化顺序:在内核启动过程中,各个设备驱动的加载和探测顺序并不总是固定的。这取决于总线类型、模块加载顺序以及设备树(Device Tree)的解析顺序。
- 初始化死锁:如果没有延迟探测机制,当音频解码器的驱动先于电源管理芯片的驱动进行探测时,它会因为获取不到所需的电源而失败。这种失败在过去是永久性的,导致该设备在本次启动中无法使用,除非开发者通过修改链接顺序等脆弱的方式来强制规定一个固定的初始化顺序。
延迟探测机制就是为了打破这种僵局,它允许驱动在依赖未满足时优雅地“放弃”本次探测,并告知内核“请稍后重试”,从而使得整个系统的设备初始化过程能够最终成功。
它的发展经历了哪些重要的里程碑或版本迭代?
延迟探测机制是随着Linux设备模型的演进而逐步完善的。它的重要性随着Device Tree在ARM等平台上的普及而日益凸显,因为Device Tree描述了设备间的依赖关系,内核需要一种动态的机制来响应这些关系。该机制从最初简单的重试逻辑,演变为由drivers/base/dd.c
集中管理的一个健壮的框架。它引入了专门的返回码 -EPROBE_DEFER
作为驱动和驱动核心之间沟通的“信号”,并维护一个全局的延迟探测设备列表,在每次有新驱动成功注册后,触发对该列表中的设备进行重试。
目前该技术的社区活跃度和主流应用情况如何?
延迟探测是Linux内核驱动模型中一个非常核心且稳定的部分,已经完全融入了内核的日常开发中。它不是一个可选功能,而是编写具有依赖关系的设备驱动时必须遵循的标准范式。几乎所有在嵌入式系统、移动设备和服务器硬件中工作的内核开发者都会直接或间接地使用到这一机制。它被广泛应用于所有类型的总线驱动,包括平台设备(Platform Device)、I2C、SPI、USB等。
核心原理与设计
它的核心工作原理是什么?
drivers/base/dd.c
的核心工作原理是基于一个特定的错误码 -EPROBE_DEFER
和一个全局的待处理设备列表。
- 返回特定错误码:当一个设备驱动的
.probe()
函数被调用时,它会尝试获取其所有依赖资源,例如通过devm_clk_get()
获取时钟或devm_regulator_get()
获取电源。如果提供这些资源的驱动程序(例如时钟控制器驱动)尚未探测成功,这些资源获取函数将返回一个特殊的指针ERR_PTR(-EPROBE_DEFER)
。 - 传递错误码:设备驱动程序在检查到这个特定的错误码后,必须立即停止其自身的探测流程,并从自己的
.probe()
函数中原样返回-EPROBE_DEFER
。 - 驱动核心捕获:位于
drivers/base/core.c
和dd.c
中的驱动核心代码在调用驱动的.probe()
函数后,会检查其返回值。当它看到返回值是-EPROBE_DEFER
时,它不会像处理其他错误(如-ENODEV
,-EIO
)那样将该设备标记为初始化失败。 - 加入延迟列表:驱动核心会将这个设备及其驱动添加到一个全局的
deferred_probe_list
链表中。 - 触发重试:当系统中任何一个其他驱动成功探测并注册后,驱动核心会认为某些之前未满足的依赖现在可能已经就绪。于是,它会启动一个工作队列(worker),遍历
deferred_probe_list
列表中的所有设备,并再次尝试调用它们的.probe()
函数。 - 循环与完成:这个过程会不断重复,直到列表中的设备都成功探测,或者当系统中不再有新的驱动注册且列表依然不为空(这通常意味着存在无法满足的依赖或配置错误)。
它的主要优势体現在哪些方面?
- 解耦依赖:它彻底解决了驱动间的硬编码初始化顺序依赖,使得驱动开发更加模块化。
- 鲁棒性:极大地提高了系统初始化的成功率,能够自动适应不同的启动顺序和模块加载时机。
- 简化开发:驱动开发者无需编写自己的复杂重试逻辑,只需正确处理并返回
-EPROBE_DEFER
即可,遵循了统一的框架。 - 动态适应:能够很好地支持可热插拔的模块化驱动。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 掩盖问题:如果一个依赖项因为配置错误(例如Device Tree中的拼写错误)而永远无法满足,设备将无限期地处于延迟探测状态,这可能会轻微增加系统启动时间,并且需要通过检查内核日志 (
dmesg
) 才能发现根本问题。 - 调试复杂性:在出现循环依赖(A依赖B,B又依赖A)的情况下,相关的设备将永远无法探测成功,这会给调试带来困难。
- 不适用于所有错误:该机制只应用于依赖资源尚未就绪的场景。对于硬件不存在 (
-ENODEV
)、I/O错误 (-EIO
) 或内存不足 (-ENOMEM
) 等确定性的失败,驱动绝不能返回-EPROBE_DEFER
。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
延迟探测是处理内核中设备间初始化依赖的唯一标准且正确的解决方案。
- 场景一:显示面板驱动
一个LCD显示面板驱动需要从一个I2C总线上的PMIC(电源管理芯片)获取3.3V的电源。在其.probe()
函数中,它会调用regulator_get()
。如果此时PMIC的I2C驱动尚未探测成功,regulator_get()
就会返回-EPROBE_DEFER
。显示面板驱动必须将此错误码返回,其探测就会被推迟。 - 场景二:Wi-Fi模块驱动
一个通过SDIO总线连接的Wi-Fi模块,其电源使能由一个GPIO引脚控制。Wi-Fi驱动在其.probe()
函数中会调用gpiod_get()
来请求这个GPIO。如果该GPIO所属的GPIO控制器驱动还没有准备好,gpiod_get()
就会返回-EPROBE_DEFER
,Wi-Fi驱动的探测也随之被推迟。 - 场景三:声音芯片驱动
一个音频编解码器(Codec)需要主时钟(MCLK)信号。它的驱动会通过clk_get()
向时钟管理器请求时钟。如果提供此时钟的PLL(锁相环)或晶振的驱动尚未初始化,clk_get()
就会返回-EPROBE_DEFER
,音频驱动的探测将被推迟。
是否有不推荐使用该技术的场景?为什么?
如上所述,这不是一个可选技术,而是一个必须正确使用的框架。不推荐(或说错误)的用法是滥用 -EPROBE_DEFER
。
- 不应用于硬件错误:当驱动通过I2C读取设备ID发现不匹配,或者发现设备硬件损坏时,应该返回
-ENODEV
或-EIO
,而不是-EPROBE_DEFER
。因为这种错误是永久性的,重试也无济于事。 - 不应用于临时资源不足:如果驱动因
kmalloc
失败而无法分配内存,应该返回-ENOMEM
。虽然系统稍后可能有更多内存,但延迟探测机制并非为此设计。
对比分析
请将其 与 其他相似技术 进行详细对比。
延迟探测机制在内核中是独一无二的。其对比对象应该是那些“没有延迟探测机制”时可能采用的替代方案。
特性 | Deferred Probing (-EPROBE_DEFER ) |
手动延时重试 (msleep ) |
硬编码初始化顺序 |
---|---|---|---|
实现方式 | 由驱动核心统一管理的、基于事件驱动的非阻塞式回调机制。 | 在驱动的 .probe 函数中使用 msleep() 循环等待。 |
通过修改 Makefile 中的链接顺序或使用 *_initcall 宏来强制规定加载/初始化顺序。 |
性能开销/资源占用 | 低。非阻塞,不占用CPU。仅在其他驱动加载成功时才触发重试,非常高效。 | 高。阻塞了内核的初始化线程,浪费CPU时间进行忙等待,严重拖慢系统启动速度。 | 无运行时开销。但在开发和维护阶段开销巨大。 |
鲁棒性与模块化 | 非常高。完全解耦了驱动间的顺序依赖,支持模块化和动态加载。 | 差。睡眠时间难以确定,无法保证依赖一定就绪,是不可靠的“竞态”解决方案。 | 极差。非常脆弱,任何微小的系统改动都可能破坏原有的顺序,完全不适用于模块化驱动。 |
启动速度影响 | 轻微。只有在确实存在大量需要等待的依赖时,才会因重试而略微增加启动时间。 | 严重。引入了固定的、不必要的延迟,极大地增加了启动时间。 | 无直接影响,但维护成本极高。 |
适用场景 | Linux内核中所有存在初始化依赖的设备驱动的标准解决方案。 | 不应使用。是一种反模式,在现代内核开发中应被完全禁止。 | 仅可能用于某些没有操作系统的极简固件,在Linux中已被淘汰。 |
driver_deferred_probe_add: 将设备添加到延迟探测列表
此函数的作用是将一个当前无法完成探测的设备添加到一个全局的“延迟探测”列表中。这个机制是Linux内核处理驱动依赖关系的核心, 它允许一个设备的初始化过程因为其依赖的资源(例如一个时钟、一个电源调节器或其所在的物理总线)尚未就绪而安全地推迟, 而不是永久性地失败。
工作原理:
- 检查前提条件: 函数首先检查设备的
can_match
标志, 这是一个安全阀。只有当一个设备在匹配过程中, 其驱动明确返回-EPROBE_DEFER
请求延迟探测时, 这个标志才会被设置。此检查确保了不会将任意设备错误地添加到延迟列表中。 - 获取锁: 它使用一个全局互斥锁
deferred_probe_mutex
来保护全局的deferred_probe_pending_list
链表。 - 防止重复添加: 在锁的保护下, 它检查设备自身的
deferred_probe
链表节点是否为空。如果节点不为空, 意味着该设备已经被添加到了某个链表中 (也就是延迟探测列表), 这样可以有效防止同一个设备被重复添加。 - 添加到全局列表: 如果设备尚未在列表中, 函数会调用
list_add_tail
将设备私有数据中的deferred_probe
节点添加到deferred_probe_pending_list
全局链表的尾部。
1 | /** |
driver_allows_async_probing: 检查驱动程序是否允许异步探测
此函数是一个决策函数, 它的作用是根据驱动程序自身的设置以及系统级的配置, 来判断该驱动的 probe
函数是否可以被异步执行。异步执行意味着 probe
函数不会立即在当前调用上下文中运行, 而是被放入一个工作队列中, 由内核的工作线程在稍后的时间点执行。
工作原理:
函数的核心是一个 switch
语句, 它检查驱动结构体中的 probe_type
字段, 并按以下优先级进行决策:
- 强制同步 (
PROBE_FORCE_SYNCHRONOUS
): 如果驱动明确要求同步探测, 则函数立即返回false
。这通常用于那些对系统启动至关重要的基础设备, 它们的驱动必须在后续驱动加载前完成初始化。 - 偏好异步 (
PROBE_PREFER_ASYNCHRONOUS
): 如果驱动明确表示偏好异步探测, 则函数立即返回true
。这表明驱动作者已经确认其probe
过程是安全的, 并且可以从异步执行中受益。 - 默认情况 (未指定): 如果驱动没有明确指定策略, 函数会检查是否存在系统级的覆盖设置:
- 内核命令行: 它会检查内核启动参数中是否包含了强制对该特定驱动或所有驱动进行异步探测的选项。
- 模块参数: 它会检查加载驱动所属的内核模块时, 是否通过模块参数请求了异步探测。
- 如果以上覆盖设置均不存在, 则默认行为是安全的同步探测, 函数返回
false
。
1 | /* |
driver_deferred_probe_trigger: 触发对延迟探测设备的重新探测
此函数是Linux延迟探测机制的“发动机”。它的核心作用是启动一个工作流程, 来重新尝试探测所有因为依赖未满足而进入等待列表的设备。每当系统中有任何一个驱动程序成功绑定到一个设备时, 就应该调用此函数, 因为这个成功的探测可能正好满足了其他正在等待的设备的依赖关系。
工作原理:
- 全局开关检查: 函数首先检查
driver_deferred_probe_enable
这个全局布尔标志, 如果延迟探测功能在系统级被禁用, 则直接返回。 - 移动设备列表: 关键操作在
deferred_probe_mutex
锁的保护下进行。它调用list_splice_tail_init()
函数, 这是一个非常高效的链表操作, 它会把deferred_probe_pending_list
(等待列表) 中的所有设备节点, 一次性地移动到deferred_probe_active_list
(活动列表) 的末尾, 并将等待列表清空。 - 更新触发计数: 在移动列表的同时, 它会原子地增加
deferred_trigger_count
计数器。这个计数器是用来解决一个棘手的竞态条件: 如果一个设备A即将进入延迟探测, 而在它进入之前, 它所依赖的设备B恰好完成了探测并触发了本函数。如果没有这个计数器, 设备A可能会错过这次触发而永远地卡在等待列表中。driver_probe_device
函数会检查这个计数器在探测前后的变化, 从而发现并处理这种情况。 - 调度工作队列: 最后, 它调用
queue_work()
将一个预定义的deferred_probe_work
工作项放入一个系统工作队列中。这会唤醒一个内核工作线程, 该线程会执行一个函数来遍历deferred_probe_active_list
列表, 并对列表中的每一个设备重新调用探测流程。
1 | /* 一个全局布尔值, 作为整个延迟探测功能的总开关. */ |
device_is_bound: 检查设备是否已绑定驱动程序
此函数用于检查一个设备 (dev
) 是否已经成功地与一个驱动程序完成绑定。它的核心原理是检查设备私有数据结构中的一个 klist
节点 (knode_driver
) 是否已经被挂载到驱动程序的设备列表中。
此函数被设计为必须在获取了设备锁 (device lock
) 的情况下调用。在STM32H750这样的单核抢占式内核环境中, 这一点至关重要。虽然只有一个核心, 但一个低优先级的任务在执行此函数时, 可能会被一个高优先级的任务或中断抢占。持有设备锁可以防止在检查 dev->p
和 klist_node_attached
的过程中, 设备的状态被其他任务(例如, 正在解绑此设备的用户空间进程)修改, 从而确保了检查的原子性和准确性。
1 | /** |
driver_bound: 完成设备与驱动程序的绑定过程
此函数在一个驱动程序的 probe
函数成功返回后被调用, 用于完成设备与驱动程序绑定的最后步骤。它负责更新内核中各种与设备和驱动程序关系相关的状态, 并通知总线和用户空间绑定已完成。
1 | /* |
really_probe: 执行探测的最终准备、调用和收尾工作
此函数是设备探测的“最后一英里”。它位于整个探测调用链的最深处, 负责执行调用驱动程序.probe()
方法之前的所有最终准备工作、实际调用该方法, 并在调用结束后进行清理或最终化绑定。它是一个复杂的函数, 精心编排了硬件配置、sysfs
集成和严格的错误回滚, 是设备模型鲁棒性的集中体现。
工作原理:
此函数可以被看作一个主程序, 按照一份严格的清单执行操作:
- 最终检查: 首先, 它会检查系统级的
defer_all_probes
标志和设备的依赖关系(device_links
), 确保探测可以继续进行。它还会检查设备是否处于一个干净的状态(没有遗留的devres
资源)。 - 建立绑定关系: 它调用
device_set_driver()
将设备的driver
指针指向驱动。从此刻起, 即使.probe()
还未调用, 设备在逻辑上已经“拥有”了一个驱动。 - 硬件层配置: 在调用
.probe()
之前, 它会执行关键的硬件配置:- 引脚控制(
pinctrl
): 调用pinctrl_bind_pins()
来配置设备所需的GPIO引脚的复用功能和电气特性。 - DMA配置: 调用总线的
dma_configure()
方法, 为该设备设置DMA通道。
- 引脚控制(
- Sysfs集成: 调用
driver_sysfs_add()
在设备的sysfs
目录下创建指向驱动目录的符号链接, 使设备和驱动的绑定关系在用户空间可见。 - 调用驱动的Probe: 这是整个过程的核心, 它调用
call_driver_probe()
, 最终执行驱动程序开发者自己编写的.probe()
方法。 - 成功收尾: 如果
.probe()
返回成功, 它会继续创建驱动定义的设备属性文件组, 并调用driver_bound()
来宣告绑定最终完成。 - 失败回滚: 如果上述任何一步失败, 函数会通过
goto
语句跳转到相应的错误处理标签。这些标签按照与设置时相反的顺序排列, 形成一个“回滚链”, 精确地撤销所有已经成功的操作, 例如移除sysfs
文件、解除pinctrl绑定、清除dev->driver
指针等, 确保设备能回到一个干净的、可被再次探测的状态。
1 | /* |
__driver_probe_device: 执行设备与驱动的实际探测和绑定
此函数是设备探测过程的最终执行者。与上层的包装函数不同, __driver_probe_device
负责在调用驱动程序的 .probe()
方法之前, 执行所有必要的最终检查和准备工作, 特别是运行时电源管理 (Runtime Power Management)。它是确保设备在被驱动访问之前处于正确上电状态的核心。
工作原理:
- 最终有效性检查: 它首先进行最后一道防线检查, 确认设备没有正在被移除(
dead
), 仍然在sysfs
中注册, 并且尚未绑定任何其他驱动。这是防止在无效状态下进行探测的关键。 - 电源管理准备: 这是此函数最重要的职责。它执行一个严格的电源管理序列:
- 首先, 为该设备的所有“供应商”(如它所依赖的
regulator
电源或clock
时钟)增加引用计数, 确保它们处于活动状态。 - 然后, 如果设备有父设备(例如, 一个SPI设备传感器的父设备是SPI总线控制器), 它会同步地为父设备上电, 并等待其就绪。
- 通过一个电源管理屏障(
pm_runtime_barrier
), 确保所有与该设备相关的异步电源请求都已完成。
- 首先, 为该设备的所有“供应商”(如它所依赖的
- 调用实际探测: 在确保设备及其依赖都已上电后, 它调用
really_probe()
。really_probe()
是一个非常小的包装器, 它最终会直接调用驱动程序开发者编写的.probe()
方法。 - 电源管理清理: 无论
probe
成功与否, 它都会执行与准备阶段相反的清理操作: 减少父设备和供应商的电源引用计数。这使得如果总线或电源不再被任何设备使用, 它们可以自动进入低功耗状态。
1 | /* |
driver_probe_device: 尝试将设备与驱动进行绑定
此函数是一个关键的包装函数(wrapper), 它负责管理调用驱动probe
方法的整个过程。它本身不执行实际的probe
调用, 而是调用一个核心函数(__driver_probe_device
)来完成。此函数的主要职责是处理探测之后的结果, 特别是延迟探测 (deferred probe) 的复杂逻辑, 并通过原子计数器和等待队列来协调系统范围内的探测活动。
工作原理:
- 记录状态并调用核心探测: 在探测开始前, 它会记录下当前延迟探测的触发次数, 并原子地增加一个全局的”正在进行的探测”计数器(
probe_count
)。然后, 它调用__driver_probe_device
, 这个函数会最终调用驱动程序自身实现的.probe()
方法。 - 处理延迟探测: 如果核心探测函数返回
-EPROBE_DEFER
, 意味着设备的某个依赖项尚未就绪。此时, 此包装函数会:- 调用
driver_deferred_probe_add()
将该设备放入一个全局的”待重试”列表中。 - 检查在本次探测期间, 是否有其他驱动的探测完成并触发了新一轮的延迟探测。如果发生了这种情况, 它会再次触发一次延迟探测, 以确保不会错过任何可能满足当前设备依赖的机会。这是一个处理并发和依赖关系的关键机制。
- 调用
- 更新状态并唤醒等待者: 探测结束后(无论成功、失败还是延迟), 它会原子地减少
probe_count
计数器, 并唤醒所有可能正在等待探测完成的内核任务(通过probe_waitqueue
)。
1 | /** |
__driver_attach_async_helper: 异步地执行驱动与设备的实际绑定操作
此函数是一个辅助函数, 它在内核的异步执行上下文(即kworker
内核线程)中被调用, 用以完成一个驱动程序(driver
)和一个设备(device
)的绑定过程. 这通常发生在总线(bus)发现一个设备和一个潜在匹配的驱动, 但选择以异步方式执行耗时较长的探测(probe
)过程时.
1 | /* |
driver_attach: 尝试将驱动程序绑定到总线上的所有设备
此函数是一个高级封装, 其唯一目的就是启动一个过程: 遍历指定驱动程序所属总线上的每一个设备, 并尝试将该驱动程序附加到每一个设备上。
工作原理:
它并不执行任何实际的匹配逻辑。相反, 它调用 bus_for_each_dev()
, 这是一个迭代器函数。bus_for_each_dev()
会锁定总线的设备列表, 然后为列表中的每个设备调用一个由调用者提供的回调函数。在这里, driver_attach
将 __driver_attach
作为回调函数传递过去, 同时将驱动程序自身(drv
)作为上下文数据传递。
1 | /** |
__driver_attach: 对单个设备执行驱动匹配和探测
这是一个内部静态函数, 是 driver_attach
过程的真正核心。bus_for_each_dev
会为总线上的每个设备调用一次此函数。它的作用是: 判断传入的驱动(drv
)和设备(dev
)是否匹配, 如果匹配, 就调用驱动的 probe
函数来初始化设备。
工作原理:
- 匹配设备: 它首先调用
driver_match_device()
。这个函数会调用总线的match
方法来判断驱动和设备是否兼容(例如, 比较SPI设备ID或USB VID/PID)。 - 处理匹配结果:
- 不匹配 (
ret == 0
): 直接返回, 继续处理总线上的下一个设备。 - 延迟探测 (
ret == -EPROBE_DEFER
): 这意味着匹配可能成功, 但设备的某个依赖项(如一个需要的时钟或regulator)尚未就绪。设备会被加入一个”延迟探测”列表, 内核将在稍后重新尝试为其探测。 - 匹配失败 (
ret < 0
): 发生错误, 同样直接返回。 - 成功匹配 (
ret > 0
): 继续执行绑定和探测。
- 不匹配 (
- 探测 (Probing): 对于成功匹配的设备, 它会锁定设备, 然后调用
driver_probe_device()
。driver_probe_device()
会最终调用驱动程序自身实现的probe
方法 (例如,my_spi_driver->probe
)。probe
函数是驱动代码的核心, 它负责配置硬件寄存器、请求中断、设置DMA等所有与硬件交互的初始化工作。 - 异步探测: 函数还支持异步探测, 这在多核系统上可以提高启动速度。但在单核的STM32H750上, 其主要作用是将探测工作放入一个工作队列中稍后执行, 从而避免阻塞当前执行路径。
1 | static inline int driver_match_device(const struct device_driver *drv, |
wait_for_device_probe: 等待所有设备探测完成
此函数是内核中一个至关重要的同步点, 尤其在系统启动后期或关机流程开始时。它的核心原理是通过多种同步机制, 确保系统中所有正在进行的、异步的设备探测(probe)活动全部执行完毕, 从而让系统进入一个”设备稳定”的状态。
这个函数像是一个三道关卡的检查站, 每一道关卡都解决一种特定类型的异步活动:
第一关 (
flush_work(&deferred_probe_work)
): 内核中有一个专门的”延迟探测”工作队列(deferred_probe_work
)。当一个设备的探测因为其依赖项(如另一个设备或固件)尚未就绪而失败并返回-EPROBE_DEFER
时, 内核不会立即放弃, 而是会将这个探测任务放入这个工作队列中, 以便稍后重试。flush_work
函数的作用就是同步地等待这个工作队列中的所有任务全部执行完毕。这确保了所有可重试的探测都已经完成了它们的最后一次尝试。第二关 (
wait_event(probe_waitqueue, ...)
: 内核维护着一个原子计数器probe_count
, 用于记录当前有多少个设备正在执行其probe
函数。每当一个设备的探测开始时, 这个计数器会增加; 当探测结束时, 计数器会减少。wait_event
函数会让当前进程在一个名为probe_waitqueue
的等待队列上睡眠, 直到probe_count
的值变为0。这保证了即使是那些没有使用延迟探测机制的、正在进行的探测也能被等待。第三关 (
async_synchronize_full()
): 内核的异步执行框架(async
)允许驱动程序将探测过程的一部分(或全部)分派到后台线程中异步执行。async_synchronize_full
函数的作用是等待所有通过async_schedule
启动的异步函数调用全部执行完成。这是一个兜底的同步机制, 确保了所有形式的异步探测都已经结束。
1 | /** |
device_block_probing: 阻塞新的设备探测
此函数的作用是在内核中设置一个”全局开关”, 暂时阻止任何新的设备探测(probe)被启动, 并将所有新的探测请求推迟处理。它的核心原理是一个简单而有效的两阶段同步过程, 用以”冻结”设备模型的探测活动, 使其进入一个可预测的稳定状态。
这个过程如下:
设置延迟标志 (
defer_all_probes = true;
): 函数首先将一个全局布尔标志defer_all_probes
设置为true
。内核的设备模型核心在尝试为一个新设备调用其驱动的probe
函数之前, 会检查这个标志。如果标志为true
, 探测将不会被执行, 而是被自动放入”延迟探测”工作队列中, 等待该标志被清除后才重试。这相当于关上了接纳新设备初始化的大门。同步现有探测 (
wait_for_device_probe();
): 仅仅”关上大门”是不够的, 因为在设置标志的那一刻, 可能已经有其他设备的probe
函数正在执行中。因此, 函数紧接着调用了wait_for_device_probe()
。这个调用会阻塞并等待所有当前正在运行的、各种形式的异步探测全部执行完毕。这相当于等待所有已经进门的客人就座。
通过这两个步骤的组合, device_block_probing
函数确保了当它返回时, 整个系统的设备探测活动已经完全静止: 不会有新的探测开始, 并且所有之前开始的探测也已经结束。这个函数在系统进入关机流程时由device_shutdown
调用, 是一个至关重要的准备步骤, 它保证了在逐个关闭设备时, 设备列表不会被意外地修改, 从而确保了关机流程的稳定性和可预测性。
1 | /* |