[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 和一个全局的待处理设备列表。

  1. 返回特定错误码:当一个设备驱动的 .probe() 函数被调用时,它会尝试获取其所有依赖资源,例如通过 devm_clk_get() 获取时钟或 devm_regulator_get() 获取电源。如果提供这些资源的驱动程序(例如时钟控制器驱动)尚未探测成功,这些资源获取函数将返回一个特殊的指针 ERR_PTR(-EPROBE_DEFER)
  2. 传递错误码:设备驱动程序在检查到这个特定的错误码后,必须立即停止其自身的探测流程,并从自己的 .probe() 函数中原样返回 -EPROBE_DEFER
  3. 驱动核心捕获:位于 drivers/base/core.cdd.c 中的驱动核心代码在调用驱动的 .probe() 函数后,会检查其返回值。当它看到返回值是 -EPROBE_DEFER 时,它不会像处理其他错误(如 -ENODEV, -EIO)那样将该设备标记为初始化失败。
  4. 加入延迟列表:驱动核心会将这个设备及其驱动添加到一个全局的 deferred_probe_list 链表中。
  5. 触发重试:当系统中任何一个其他驱动成功探测并注册后,驱动核心会认为某些之前未满足的依赖现在可能已经就绪。于是,它会启动一个工作队列(worker),遍历 deferred_probe_list 列表中的所有设备,并再次尝试调用它们的 .probe() 函数。
  6. 循环与完成:这个过程会不断重复,直到列表中的设备都成功探测,或者当系统中不再有新的驱动注册且列表依然不为空(这通常意味着存在无法满足的依赖或配置错误)。

它的主要优势体現在哪些方面?

  • 解耦依赖:它彻底解决了驱动间的硬编码初始化顺序依赖,使得驱动开发更加模块化。
  • 鲁棒性:极大地提高了系统初始化的成功率,能够自动适应不同的启动顺序和模块加载时机。
  • 简化开发:驱动开发者无需编写自己的复杂重试逻辑,只需正确处理并返回 -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内核处理驱动依赖关系的核心, 它允许一个设备的初始化过程因为其依赖的资源(例如一个时钟、一个电源调节器或其所在的物理总线)尚未就绪而安全地推迟, 而不是永久性地失败。

工作原理:

  1. 检查前提条件: 函数首先检查设备的 can_match 标志, 这是一个安全阀。只有当一个设备在匹配过程中, 其驱动明确返回 -EPROBE_DEFER 请求延迟探测时, 这个标志才会被设置。此检查确保了不会将任意设备错误地添加到延迟列表中。
  2. 获取锁: 它使用一个全局互斥锁 deferred_probe_mutex 来保护全局的 deferred_probe_pending_list 链表。
  3. 防止重复添加: 在锁的保护下, 它检查设备自身的 deferred_probe 链表节点是否为空。如果节点不为空, 意味着该设备已经被添加到了某个链表中 (也就是延迟探测列表), 这样可以有效防止同一个设备被重复添加。
  4. 添加到全局列表: 如果设备尚未在列表中, 函数会调用 list_add_tail 将设备私有数据中的 deferred_probe 节点添加到 deferred_probe_pending_list 全局链表的尾部。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* driver_deferred_probe_add - 将一个设备添加到延迟探测列表
* @dev: 需要被添加的设备
*/
void driver_deferred_probe_add(struct device *dev)
{
/*
* 检查设备的 can_match 标志.
* 这是一个安全检查, 只有当驱动在匹配过程中主动返回 -EPROBE_DEFER 时,
* 这个标志才会被设置. 这可以防止不相关的设备被错误地添加到延迟列表.
*/
if (!dev->can_match)
return;

/*
* 获取用于保护延迟探测列表的全局互斥锁.
* 在单核抢占式内核中, 这可以防止在操作全局链表时被其他任务抢占, 从而避免数据竞争.
*/
mutex_lock(&deferred_probe_mutex);
/*
* 检查设备自身的链表节点 dev->p->deferred_probe 是否为空.
* 如果它不为空, 说明它已经被链接到某个链表中 (即延迟探测列表),
* 这个检查可以防止同一个设备被重复添加.
*/
if (list_empty(&dev->p->deferred_probe)) {
/*
* 打印一条调试信息到内核日志, 这在追踪启动过程中的设备依赖问题时非常有用.
* dev_dbg 只有在内核配置了相关调试选项时才会实际打印.
*/
dev_dbg(dev, "Added to deferred list\n");
/*
* 将设备的链表节点添加到全局的 deferred_probe_pending_list 链表的尾部.
* 这是实际将设备放入"待重试"队列的操作.
*/
list_add_tail(&dev->p->deferred_probe, &deferred_probe_pending_list);
}
/*
* 释放全局互斥锁, 允许其他任务访问该列表.
*/
mutex_unlock(&deferred_probe_mutex);
}

driver_allows_async_probing: 检查驱动程序是否允许异步探测

此函数是一个决策函数, 它的作用是根据驱动程序自身的设置以及系统级的配置, 来判断该驱动的 probe 函数是否可以被异步执行。异步执行意味着 probe 函数不会立即在当前调用上下文中运行, 而是被放入一个工作队列中, 由内核的工作线程在稍后的时间点执行。

工作原理:
函数的核心是一个 switch 语句, 它检查驱动结构体中的 probe_type 字段, 并按以下优先级进行决策:

  1. 强制同步 (PROBE_FORCE_SYNCHRONOUS): 如果驱动明确要求同步探测, 则函数立即返回 false。这通常用于那些对系统启动至关重要的基础设备, 它们的驱动必须在后续驱动加载前完成初始化。
  2. 偏好异步 (PROBE_PREFER_ASYNCHRONOUS): 如果驱动明确表示偏好异步探测, 则函数立即返回 true。这表明驱动作者已经确认其 probe 过程是安全的, 并且可以从异步执行中受益。
  3. 默认情况 (未指定): 如果驱动没有明确指定策略, 函数会检查是否存在系统级的覆盖设置:
    • 内核命令行: 它会检查内核启动参数中是否包含了强制对该特定驱动或所有驱动进行异步探测的选项。
    • 模块参数: 它会检查加载驱动所属的内核模块时, 是否通过模块参数请求了异步探测。
    • 如果以上覆盖设置均不存在, 则默认行为是安全的同步探测, 函数返回 false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
* 静态函数声明: driver_allows_async_probing
* 判断一个驱动是否允许其 probe 函数被异步执行.
* @drv: 指向需要被检查的驱动程序的 const struct device_driver 指针.
* @return: 如果允许异步探测, 返回 true; 否则返回 false.
*/
static bool driver_allows_async_probing(const struct device_driver *drv)
{
/*
* 根据驱动程序自己设置的 probe_type 字段进行判断.
*/
switch (drv->probe_type) {
/*
* 情况一: 驱动明确表示偏好异步探测.
*/
case PROBE_PREFER_ASYNCHRONOUS:
/*
* 直接返回 true.
*/
return true;

/*
* 情况二: 驱动明确要求强制同步探测.
*/
case PROBE_FORCE_SYNCHRONOUS:
/*
* 直接返回 false.
*/
return false;

/*
* 情况三: 驱动未指定明确的策略 (默认情况).
*/
default:
/*
* 检查内核启动命令行参数是否请求了对这个特定驱动进行异步探测.
* 这是一种系统级的覆盖机制.
*/
if (cmdline_requested_async_probing(drv->name))
return true;

/*
* 检查加载驱动所属的内核模块时, 是否通过模块参数请求了异步探测.
* 这是另一种覆盖机制.
*/
if (module_requested_async_probing(drv->owner))
return true;

/*
* 如果没有任何覆盖设置, 默认采用安全的同步探测方式.
*/
return false;
}
}

driver_deferred_probe_trigger: 触发对延迟探测设备的重新探测

此函数是Linux延迟探测机制的“发动机”。它的核心作用是启动一个工作流程, 来重新尝试探测所有因为依赖未满足而进入等待列表的设备。每当系统中有任何一个驱动程序成功绑定到一个设备时, 就应该调用此函数, 因为这个成功的探测可能正好满足了其他正在等待的设备的依赖关系。

工作原理:

  1. 全局开关检查: 函数首先检查 driver_deferred_probe_enable 这个全局布尔标志, 如果延迟探测功能在系统级被禁用, 则直接返回。
  2. 移动设备列表: 关键操作在 deferred_probe_mutex 锁的保护下进行。它调用 list_splice_tail_init() 函数, 这是一个非常高效的链表操作, 它会把 deferred_probe_pending_list (等待列表) 中的所有设备节点, 一次性地移动到 deferred_probe_active_list (活动列表) 的末尾, 并将等待列表清空。
  3. 更新触发计数: 在移动列表的同时, 它会原子地增加 deferred_trigger_count 计数器。这个计数器是用来解决一个棘手的竞态条件: 如果一个设备A即将进入延迟探测, 而在它进入之前, 它所依赖的设备B恰好完成了探测并触发了本函数。如果没有这个计数器, 设备A可能会错过这次触发而永远地卡在等待列表中。driver_probe_device 函数会检查这个计数器在探测前后的变化, 从而发现并处理这种情况。
  4. 调度工作队列: 最后, 它调用 queue_work() 将一个预定义的deferred_probe_work 工作项放入一个系统工作队列中。这会唤醒一个内核工作线程, 该线程会执行一个函数来遍历 deferred_probe_active_list 列表, 并对列表中的每一个设备重新调用探测流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* 一个全局布尔值, 作为整个延迟探测功能的总开关. */
static bool driver_deferred_probe_enable;
/**
* driver_deferred_probe_trigger() - 启动对延迟探测设备的重新探测
*
* 此函数将所有设备从等待列表移动到活动列表, 并调度延迟探测工作队列来处理它们.
* 每当一个驱动成功绑定到一个设备时, 都应该调用此函数.
*
* 注意, 在多线程探测中存在一个竞态条件. 在多个设备同时探测的情况下,
* 可能会发生一个探测成功完成而另一个即将延迟的情况. 如果第二个依赖于第一个,
* 那么它将在触发事件发生之后才被放入等待列表, 从而被卡住.
*
* 原子的 'deferred_trigger_count' 用于判断在探测驱动期间是否发生过一次成功的触发.
* 如果在探测期间触发计数值发生了变化, 那么延迟处理应该被再次触发.
*/
void driver_deferred_probe_trigger(void)
{
/* 检查延迟探测功能是否全局启用. */
if (!driver_deferred_probe_enable)
return;

/*
* 一次成功的探测意味着等待列表中的所有设备都应该被触发重新探测.
* 将所有延迟的设备移动到活动列表中, 以便它们可以被工作队列重试.
*/
mutex_lock(&deferred_probe_mutex);
/* 原子地增加触发计数器, 作为一个事件信号, 用于解决竞态条件. */
atomic_inc(&deferred_trigger_count);
/*
* 将 deferred_probe_pending_list (等待列表) 中的所有节点拼接到
* deferred_probe_active_list (活动列表) 的尾部, 并将等待列表重新初始化为空.
* 这是一个原子性的批量移动操作.
*/
list_splice_tail_init(&deferred_probe_pending_list,
&deferred_probe_active_list);
mutex_unlock(&deferred_probe_mutex);

/*
* 启动重新探测的线程(工作队列). 它可能已经被调度了, 但再次启动它是安全的.
* queue_work 会将 deferred_probe_work 工作项添加到 system_unbound_wq 工作队列中.
*/
queue_work(system_unbound_wq, &deferred_probe_work);
}

device_is_bound: 检查设备是否已绑定驱动程序

此函数用于检查一个设备 (dev) 是否已经成功地与一个驱动程序完成绑定。它的核心原理是检查设备私有数据结构中的一个 klist 节点 (knode_driver) 是否已经被挂载到驱动程序的设备列表中。

此函数被设计为必须在获取了设备锁 (device lock) 的情况下调用。在STM32H750这样的单核抢占式内核环境中, 这一点至关重要。虽然只有一个核心, 但一个低优先级的任务在执行此函数时, 可能会被一个高优先级的任务或中断抢占。持有设备锁可以防止在检查 dev->pklist_node_attached 的过程中, 设备的状态被其他任务(例如, 正在解绑此设备的用户空间进程)修改, 从而确保了检查的原子性和准确性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* bool device_is_bound(struct device *dev) - 检查设备是否已绑定到驱动程序
* @dev: 需要检查的设备
*
* 如果传入的设备已经成功完成了与一个驱动程序的探测(probe)过程, 则返回真.
*
* 这个函数必须在持有设备锁的情况下被调用.
*/
bool device_is_bound(struct device *dev)
{
/*
* 返回一个布尔表达式的结果.
* dev->p: 指向 struct device_private 结构体的指针. 这个结构体包含了设备的核心私有数据.
* 如果一个设备还没有被核心驱动模型完全初始化, dev->p 可能是 NULL.
* 因此, 首先检查 dev->p 是否存在, 确保后续操作的安全性.
* klist_node_attached(&dev->p->knode_driver):
* dev->p->knode_driver 是一个 klist_node 类型的节点.
* 当设备成功绑定到驱动程序时, 这个节点会被添加到驱动程序的设备列表(一个klist)中.
* klist_node_attached() 函数检查这个节点当前是否已经附加在任何 klist 列表中.
* 如果它已附加, 说明设备已经绑定.
*
* 两个条件都为真时, 整个表达式才为真, 表明设备已绑定.
*/
return dev->p && klist_node_attached(&dev->p->knode_driver);
}
/*
* EXPORT_SYMBOL_GPL(device_is_bound);
* 将 device_is_bound 函数导出, 以便其他GPL许可的内核模块可以使用它.
*/
EXPORT_SYMBOL_GPL(device_is_bound);

driver_bound: 完成设备与驱动程序的绑定过程

此函数在一个驱动程序的 probe 函数成功返回后被调用, 用于完成设备与驱动程序绑定的最后步骤。它负责更新内核中各种与设备和驱动程序关系相关的状态, 并通知总线和用户空间绑定已完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/*
* 静态函数声明: driver_bound
* 此函数在设备成功探测后, 将设备标记为已绑定.
* @dev: 刚刚成功探测并要绑定的设备.
*/
static void driver_bound(struct device *dev)
{
/*
* 调用上面解释过的 device_is_bound(dev) 函数, 检查设备是否已经处于绑定状态.
* 这是一个健壮性检查, 防止重复绑定.
*/
if (device_is_bound(dev)) {
/*
* 如果设备已经绑定, 则使用 dev_warn() 打印一条警告信息到内核日志.
* %s: 会被 __func__ 替代, 即 "driver_bound".
*/
dev_warn(dev, "%s: device already bound\n", __func__);
/*
* 直接返回, 终止后续的绑定操作.
*/
return;
}

/*
* 使用 dev_dbg() 打印一条调试信息.
* 这类信息只有在内核的动态调试功能为该设备或驱动打开时才会显示.
* dev->driver->name: 驱动程序的名称.
*/
dev_dbg(dev, "driver: '%s': %s: bound to device\n", dev->driver->name,
__func__);

/*
* 将设备的 klist 节点 (dev->p->knode_driver) 添加到驱动程序的设备列表 (dev->driver->p->klist_devices) 的尾部.
* 这是将设备和驱动程序进行实质性关联的关键一步.
*/
klist_add_tail(&dev->p->knode_driver, &dev->driver->p->klist_devices);
/*
* 调用 device_links_driver_bound, 更新设备链接的状态.
* 设备链接用于表示设备间的依赖关系(如消费者/供应者), 此调用会将相关链接标记为"活动"状态.
*/
device_links_driver_bound(dev);

/*
* 调用 device_pm_check_callbacks, 检查并设置与电源管理相关的回调函数.
* 一旦设备有了驱动, 它就可能支持运行时电源管理等高级电源状态.
*/
device_pm_check_callbacks(dev);

/*
* 调用 driver_deferred_probe_del, 将此设备从"延迟探测"列表中移除.
* 如果该设备之前因为依赖未满足而探测失败, 它会被加入该列表. 绑定成功后必须将其移除.
*/
driver_deferred_probe_del(dev);
/*
* 调用 driver_deferred_probe_trigger, 触发对"延迟探测"列表中所有其他设备的一次重新探测.
* 因为当前这个设备的成功绑定, 可能满足了其他某个设备的依赖条件.
*/
driver_deferred_probe_trigger();

/*
* 调用 bus_notify, 在设备所在的总线上发送一个 BUS_NOTIFY_BOUND_DRIVER 通知.
* 其他关心设备绑定事件的内核子系统可以订阅此类通知.
*/
bus_notify(dev, BUS_NOTIFY_BOUND_DRIVER);
/*
* 调用 kobject_uevent, 生成一个 KOBJ_BIND 类型的 uevent (用户空间事件).
* 这个事件会通过 netlink 套接字发送给用户空间的 udevd 等守护进程,
* 通知它们设备已绑定驱动, 可以进行创建设备节点(/dev/..), 加载固件等后续操作.
*/
kobject_uevent(&dev->kobj, KOBJ_BIND);
}

really_probe: 执行探测的最终准备、调用和收尾工作

此函数是设备探测的“最后一英里”。它位于整个探测调用链的最深处, 负责执行调用驱动程序.probe()方法之前的所有最终准备工作、实际调用该方法, 并在调用结束后进行清理或最终化绑定。它是一个复杂的函数, 精心编排了硬件配置、sysfs集成和严格的错误回滚, 是设备模型鲁棒性的集中体现。

工作原理:
此函数可以被看作一个主程序, 按照一份严格的清单执行操作:

  1. 最终检查: 首先, 它会检查系统级的defer_all_probes标志和设备的依赖关系(device_links), 确保探测可以继续进行。它还会检查设备是否处于一个干净的状态(没有遗留的devres资源)。
  2. 建立绑定关系: 它调用device_set_driver()将设备的driver指针指向驱动。从此刻起, 即使.probe()还未调用, 设备在逻辑上已经“拥有”了一个驱动。
  3. 硬件层配置: 在调用.probe()之前, 它会执行关键的硬件配置:
    • 引脚控制(pinctrl): 调用pinctrl_bind_pins()来配置设备所需的GPIO引脚的复用功能和电气特性。
    • DMA配置: 调用总线的dma_configure()方法, 为该设备设置DMA通道。
  4. Sysfs集成: 调用driver_sysfs_add()在设备的sysfs目录下创建指向驱动目录的符号链接, 使设备和驱动的绑定关系在用户空间可见。
  5. 调用驱动的Probe: 这是整个过程的核心, 它调用call_driver_probe(), 最终执行驱动程序开发者自己编写的.probe()方法。
  6. 成功收尾: 如果.probe()返回成功, 它会继续创建驱动定义的设备属性文件组, 并调用driver_bound()来宣告绑定最终完成。
  7. 失败回滚: 如果上述任何一步失败, 函数会通过goto语句跳转到相应的错误处理标签。这些标签按照与设置时相反的顺序排列, 形成一个“回滚链”, 精确地撤销所有已经成功的操作, 例如移除sysfs文件、解除pinctrl绑定、清除dev->driver指针等, 确保设备能回到一个干净的、可被再次探测的状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/*
* 静态函数声明: really_probe
* 这是实际执行探测和绑定的最终函数.
* @dev: 要被探测的设备.
* @drv: 要绑定的驱动.
* @return: 成功时返回0, 失败时返回负的错误码.
*/
static int really_probe(struct device *dev, const struct device_driver *drv)
{
/* 一个用于调试的标志, 如果设置了, 会在探测成功后立即尝试移除再重新探测. */
bool test_remove = IS_ENABLED(CONFIG_DEBUG_TEST_DRIVER_REMOVE) &&
!drv->suppress_bind_attrs;
int ret, link_ret;

/* 检查系统是否处于“全部延迟探测”模式. */
if (defer_all_probes) {
dev_dbg(dev, "Driver %s force probe deferral\n", drv->name);
return -EPROBE_DEFER;
}

/* 检查设备的依赖项(供应商)是否都已就绪. 如果没有, 返回-EPROBE_DEFER. */
link_ret = device_links_check_suppliers(dev);
if (link_ret == -EPROBE_DEFER)
return link_ret;

dev_dbg(dev, "bus: '%s': %s: probing driver %s with device\n",
drv->bus->name, __func__, drv->name);
/* 这是一个重要的健全性检查: 确保在探测前没有任何遗留的devres资源. */
if (!list_empty(&dev->devres_head)) {
dev_crit(dev, "Resources present before probing\n");
ret = -EBUSY;
goto done;
}

re_probe: /* 用于调试的重新探测的跳转点 */
/* 关键一步: 将设备的driver指针指向该驱动. 从此刻起, 设备在逻辑上已绑定. */
/* WRITE_ONCE(dev->driver, (struct device_driver *)drv); */
device_set_driver(dev, drv);

/* 如果设备使用pinctrl, 在探测前先绑定引脚. */
ret = pinctrl_bind_pins(dev);
if (ret)
goto pinctrl_bind_failed;

/* 如果总线需要配置DMA. */
if (dev->bus->dma_configure) {
ret = dev->bus->dma_configure(dev);
if (ret)
goto pinctrl_bind_failed;
}

/* 在sysfs中创建驱动的符号链接等. */
ret = driver_sysfs_add(dev);
if (ret) {
dev_err(dev, "%s: driver_sysfs_add failed\n", __func__);
goto sysfs_failed;
}

/* 如果设备属于一个电源域(power domain), 激活它. */
if (dev->pm_domain && dev->pm_domain->activate) {
ret = dev->pm_domain->activate(dev);
if (ret)
goto probe_failed;
}

/* 核心调用: 调用驱动开发者自己编写的 .probe() 方法. */
ret = call_driver_probe(dev, drv);
if (ret) {
/* 如果是 -EAGAIN, 可能是“尽力而为”的设备链接模式, 将其当作延迟探测处理. */
if (link_ret == -EAGAIN)
ret = -EPROBE_DEFER;

/* 将probe的错误码转为正值返回, 以便上层调用者区分. */
ret = -ret;
goto probe_failed;
}

/* .probe() 成功后, 添加驱动定义的设备属性文件组. */
ret = device_add_groups(dev, drv->dev_groups);
if (ret) {
dev_err(dev, "device_add_groups() failed\n");
goto dev_groups_failed;
}

/* 如果设备有同步状态, 创建 state_synced sysfs 文件. */
if (dev_has_sync_state(dev)) {
ret = device_create_file(dev, &dev_attr_state_synced);
if (ret) {
dev_err(dev, "state_synced sysfs add failed\n");
goto dev_sysfs_state_synced_failed;
}
}

/* 用于调试的移除-再探测流程. */
if (test_remove) {
test_remove = false;
device_remove(dev);
driver_sysfs_remove(dev);
if (dev->bus && dev->bus->dma_cleanup)
dev->bus->dma_cleanup(dev);
device_unbind_cleanup(dev);
goto re_probe;
}

/* 通知pinctrl子系统, 探测已完成, 可以将引脚配置应用到硬件. */
pinctrl_init_done(dev);

/* 如果属于电源域, 同步其状态. */
if (dev->pm_domain && dev->pm_domain->sync)
dev->pm_domain->sync(dev);

/* 最终宣告绑定成功. */
driver_bound(dev);
dev_dbg(dev, "bus: '%s': %s: bound device to driver %s\n",
drv->bus->name, __func__, drv->name);
goto done;

/* --- 错误处理回滚链 --- */
dev_sysfs_state_synced_failed:
dev_groups_failed:
device_remove(dev); /* 移除设备(包括从驱动解绑). */
probe_failed:
driver_sysfs_remove(dev); /* 移除sysfs链接. */
sysfs_failed:
bus_notify(dev, BUS_NOTIFY_DRIVER_NOT_BOUND); /* 发送解绑通知. */
if (dev->bus && dev->bus->dma_cleanup) /* 清理DMA. */
dev->bus->dma_cleanup(dev);
pinctrl_bind_failed:
device_links_no_driver(dev); /* 更新设备链接状态. */
device_unbind_cleanup(dev); /* 清理绑定状态, 包括将dev->driver设为NULL. */
done:
return ret; /* 返回最终结果. */
}

__driver_probe_device: 执行设备与驱动的实际探测和绑定

此函数是设备探测过程的最终执行者。与上层的包装函数不同, __driver_probe_device 负责在调用驱动程序的 .probe() 方法之前, 执行所有必要的最终检查和准备工作, 特别是运行时电源管理 (Runtime Power Management)。它是确保设备在被驱动访问之前处于正确上电状态的核心。

工作原理:

  1. 最终有效性检查: 它首先进行最后一道防线检查, 确认设备没有正在被移除(dead), 仍然在 sysfs 中注册, 并且尚未绑定任何其他驱动。这是防止在无效状态下进行探测的关键。
  2. 电源管理准备: 这是此函数最重要的职责。它执行一个严格的电源管理序列:
    • 首先, 为该设备的所有“供应商”(如它所依赖的regulator电源或clock时钟)增加引用计数, 确保它们处于活动状态。
    • 然后, 如果设备有父设备(例如, 一个SPI设备传感器的父设备是SPI总线控制器), 它会同步地为父设备上电, 并等待其就绪。
    • 通过一个电源管理屏障(pm_runtime_barrier), 确保所有与该设备相关的异步电源请求都已完成。
  3. 调用实际探测: 在确保设备及其依赖都已上电后, 它调用 really_probe()really_probe() 是一个非常小的包装器, 它最终会直接调用驱动程序开发者编写的 .probe() 方法。
  4. 电源管理清理: 无论 probe 成功与否, 它都会执行与准备阶段相反的清理操作: 减少父设备和供应商的电源引用计数。这使得如果总线或电源不再被任何设备使用, 它们可以自动进入低功耗状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/*
* 静态函数声明: __driver_probe_device
* 这是执行实际设备探测的核心内部函数.
* @drv: 指向要绑定的驱动程序.
* @dev: 指向要被探测的设备.
* @return: 返回探测结果.
*/
static int __driver_probe_device(const struct device_driver *drv, struct device *dev)
{
int ret = 0;

/*
* 检查设备是否已被标记为 "dead" (正在移除), 或者是否已从 sysfs 注销.
* 如果是, 说明设备已失效, 不能再进行探测.
*/
if (dev->p->dead || !device_is_registered(dev))
return -ENODEV;
/*
* 检查设备是否已经绑定了驱动程序. 如果是, 返回 -EBUSY.
* 这是一个重要的并发检查, 防止一个设备被多个驱动绑定.
*/
if (dev->driver)
return -EBUSY;

/*
* 标记此设备可以被匹配. 这个标志是延迟探测机制所需要的.
*/
dev->can_match = true;
/*
* 打印一条调试信息, 记录哪个驱动匹配到了哪个设备.
*/
dev_dbg(dev, "bus: '%s': %s: matched device with driver %s\n",
drv->bus->name, __func__, drv->name);

/*
* 运行时电源管理: 为此设备的所有"供应商"(如时钟, regulator)增加使用计数, 请求它们上电.
*/
pm_runtime_get_suppliers(dev);
/*
* 如果设备有父设备 (例如, 它在一个总线上).
*/
if (dev->parent)
/*
* 同步地增加父设备的使用计数, 并等待其完全进入活动状态.
* 这是确保总线控制器等已上电的关键步骤.
*/
pm_runtime_get_sync(dev->parent);

/*
* 运行时电源管理: 设立一个屏障, 等待所有与此设备相关的、正在进行的异步电源状态切换完成.
*/
pm_runtime_barrier(dev);
/*
* 如果内核开启了 initcall_debug 调试选项.
*/
if (initcall_debug)
/*
* 调用带有额外调试信息的探测函数.
*/
ret = really_probe_debug(dev, drv);
else
/*
* 调用标准的、最终的探测函数 really_probe().
* really_probe() 会直接调用驱动的 .probe() 方法.
*/
ret = really_probe(dev, drv);
/*
* 运行时电源管理: 向电源管理核心请求让此设备进入空闲状态.
* 如果设备支持运行时PM, PM核心可能会在一段时间后将其置于低功耗模式.
*/
pm_request_idle(dev);

/*
* 如果设备有父设备, 在探测结束后, 减少对父设备的电源使用计数.
* 如果没有其他设备在使用父设备, 父设备就可以进入低功耗模式.
*/
if (dev->parent)
pm_runtime_put(dev->parent);

/*
* 同样地, 减少对所有"供应商"的电源使用计数.
*/
pm_runtime_put_suppliers(dev);
/*
* 返回从 really_probe() 获得的最终结果.
*/
return ret;
}

driver_probe_device: 尝试将设备与驱动进行绑定

此函数是一个关键的包装函数(wrapper), 它负责管理调用驱动probe方法的整个过程。它本身不执行实际的probe调用, 而是调用一个核心函数(__driver_probe_device)来完成。此函数的主要职责是处理探测之后的结果, 特别是延迟探测 (deferred probe) 的复杂逻辑, 并通过原子计数器和等待队列来协调系统范围内的探测活动。

工作原理:

  1. 记录状态并调用核心探测: 在探测开始前, 它会记录下当前延迟探测的触发次数, 并原子地增加一个全局的”正在进行的探测”计数器(probe_count)。然后, 它调用 __driver_probe_device, 这个函数会最终调用驱动程序自身实现的 .probe() 方法。
  2. 处理延迟探测: 如果核心探测函数返回 -EPROBE_DEFER, 意味着设备的某个依赖项尚未就绪。此时, 此包装函数会:
    • 调用 driver_deferred_probe_add() 将该设备放入一个全局的”待重试”列表中。
    • 检查在本次探测期间, 是否有其他驱动的探测完成并触发了新一轮的延迟探测。如果发生了这种情况, 它会再次触发一次延迟探测, 以确保不会错过任何可能满足当前设备依赖的机会。这是一个处理并发和依赖关系的关键机制。
  3. 更新状态并唤醒等待者: 探测结束后(无论成功、失败还是延迟), 它会原子地减少probe_count计数器, 并唤醒所有可能正在等待探测完成的内核任务(通过probe_waitqueue)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* driver_probe_device - 尝试将设备和驱动绑定在一起
* @drv: 要绑定设备的驱动程序
* @dev: 尝试绑定到驱动的设备
*
* 如果设备未注册, 此函数返回 -ENODEV; 如果设备已有驱动, 返回 -EBUSY;
* 如果设备成功绑定, 返回 0; 如果 ->probe 方法失败, 返回一个正的(反转的)错误码.
*
* 调用此函数时, 必须持有 @dev 的锁. 当为USB接口调用时, 还必须持有 @dev->parent 的锁.
*
* 如果设备有父设备, 在进行驱动探测之前, 会对其父设备进行运行时恢复(runtime-resume).
*/
static int driver_probe_device(const struct device_driver *drv, struct device *dev)
{
/* 在探测开始前, 读取并保存当前延迟探测触发器的全局计数值. */
int trigger_count = atomic_read(&deferred_trigger_count);
int ret;

/* 原子地增加全局的 probe_count 计数器, 表明有一个新的探测正在进行. */
atomic_inc(&probe_count);
/*
* 调用核心函数 __driver_probe_device 来执行实际的探测工作,
* 该函数会最终调用驱动程序的 .probe() 方法.
*/
ret = __driver_probe_device(drv, dev);
/*
* 检查探测结果是否是请求延迟探测 (-EPROBE_DEFER).
*/
if (ret == -EPROBE_DEFER || ret == EPROBE_DEFER) {
/* 如果需要延迟, 调用此函数将设备添加到全局的延迟探测等待列表中. */
driver_deferred_probe_add(dev);

/*
* 在探测期间是否有新的触发发生? 如果是, 需要重新触发.
* 这是一个重要的并发处理: 检查在本次探测执行期间, 全局的触发计数值是否发生了变化.
* 并且当前系统没有处于“全部延迟”模式.
*/
if (trigger_count != atomic_read(&deferred_trigger_count) &&
!defer_all_probes)
/* 如果计数值变了, 说明有其他驱动探测完成并触发了新的探测周期, 我们需要再次触发以确保依赖关系被及时处理. */
driver_deferred_probe_trigger();
}
/* 探测结束 (无论结果如何), 原子地减少全局的 probe_count 计数器. */
atomic_dec(&probe_count);
/* 唤醒所有正在 probe_waitqueue 等待队列上睡眠的任务, 通知它们有一个探测已完成. */
wake_up_all(&probe_waitqueue);
/* 返回从 __driver_probe_device 获得的原始结果. */
return ret;
}

__driver_attach_async_helper: 异步地执行驱动与设备的实际绑定操作

此函数是一个辅助函数, 它在内核的异步执行上下文(即kworker内核线程)中被调用, 用以完成一个驱动程序(driver)和一个设备(device)的绑定过程. 这通常发生在总线(bus)发现一个设备和一个潜在匹配的驱动, 但选择以异步方式执行耗时较长的探测(probe)过程时.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*
* 静态函数声明: __driver_attach_async_helper
* 这是一个异步绑定的辅助函数, 它由异步工作队列回调.
* @_dev: 一个 void 指针, 指向需要被附加驱动的设备(struct device).
* @cookie: 一个 async_cookie_t 值, 由异步框架传递, 用于同步. 此函数内并未使用它.
*/
static void __driver_attach_async_helper(void *_dev, async_cookie_t cookie)
{
/*
* 定义一个指向 struct device 的指针 dev.
* 将 void 指针 _dev 强制类型转换为 struct device 指针.
* 异步执行框架通过 void 指针来传递通用数据.
*/
struct device *dev = _dev;
/*
* 定义一个指向常量 struct device_driver 的指针 drv.
* 它将用于存储要附加到设备上的驱动程序的地址.
*/
const struct device_driver *drv;
/*
* 定义一个整型变量 ret, 用于存储驱动探测函数 driver_probe_device 的返回值.
*/
int ret;

/*
* 调用 __device_driver_lock 获取设备驱动锁. 这是一个封装了互斥锁(mutex)的宏.
* 在单核可抢占内核中, 它保护了设备和驱动之间的关联关系(如 dev->driver 指针)
* 在被修改时不被其他任务(或中断)并发访问, 防止竞态条件.
* 它会锁定设备自身以及其父设备.
*/
__device_driver_lock(dev, dev->parent);
/*
* 从设备的私有数据区(dev->p)中, 获取之前由调度者存放的、将要进行异步绑定的驱动指针.
*/
drv = dev->p->async_driver;
/*
* 将私有数据区中的异步驱动指针清空. 这是一个重要的清理步骤,
* 表明我们已经取走了这个待处理的指针, 防止后续的重复处理.
*/
dev->p->async_driver = NULL;
/*
* 调用 driver_probe_device 函数, 这是执行设备-驱动绑定的核心函数.
* 这个函数会检查驱动是否支持此设备, 如果支持, 最终会调用驱动程序中实现的 .probe() 方法.
* 这是一个可能耗时较长、甚至可能睡眠的操作, 因此将它放在异步上下文中执行.
* 其返回值存入 ret 变量.
*/
ret = driver_probe_device(drv, dev);
/*
* 调用 __device_driver_unlock 释放之前获取的设备驱动锁.
*/
__device_driver_unlock(dev, dev->parent);

/*
* 使用 dev_dbg() 打印一条调试信息到内核日志, 报告异步绑定操作的结果.
* dev_dbg() 是一种动态调试信息, 只有在内核特定配置下才会显示,
* 在生产环境中通常被编译优化掉, 没有性能开销.
*/
dev_dbg(dev, "driver %s async attach completed: %d\n", drv->name, ret);

/*
* 调用 put_device(dev) 来减少设备的引用计数.
* 这与调度此异步操作的代码中的 get_device(dev) 调用相对应.
* 在调度异步任务前, 必须增加设备引用计数, 以确保在异步任务执行前,
* 设备结构体不会因为其他原因被释放掉. 此处的 put_device() 完成了计数的平衡.
*/
put_device(dev);
}

driver_attach: 尝试将驱动程序绑定到总线上的所有设备

此函数是一个高级封装, 其唯一目的就是启动一个过程: 遍历指定驱动程序所属总线上的每一个设备, 并尝试将该驱动程序附加到每一个设备上。

工作原理:
它并不执行任何实际的匹配逻辑。相反, 它调用 bus_for_each_dev(), 这是一个迭代器函数。bus_for_each_dev() 会锁定总线的设备列表, 然后为列表中的每个设备调用一个由调用者提供的回调函数。在这里, driver_attach__driver_attach 作为回调函数传递过去, 同时将驱动程序自身(drv)作为上下文数据传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* driver_attach - 尝试将驱动程序绑定到设备.
* @drv: 要附加的驱动程序.
*
* 遍历总线上的设备列表, 并尝试将驱动与每一个设备进行匹配.
* 如果 driver_probe_device() 返回 0 并且 @dev->driver 被设置,
* 我们就找到了一个兼容的配对.
*/
int driver_attach(const struct device_driver *drv)
{
/*
* 调用 bus_for_each_dev 来遍历 drv->bus 上的所有设备.
* @drv->bus: 指定要遍历哪个总线.
* @NULL: 第一个设备参数, 设置为NULL表示从头开始遍历.
* @(void *)drv: 传递给回调函数的上下文数据, 这里就是驱动程序自身.
* (void *) 的类型转换是必要的, 因为迭代器接口是通用的,
* 但在回调函数 __driver_attach 内部会安全地转换回来.
* @__driver_attach: 将要对每个设备执行的回调函数.
*/
return bus_for_each_dev(drv->bus, NULL, (void *)drv, __driver_attach);
}
/*
* 将 driver_attach 函数导出, 使其对其他GPL兼容的内核模块可用.
*/
EXPORT_SYMBOL_GPL(driver_attach);

__driver_attach: 对单个设备执行驱动匹配和探测

这是一个内部静态函数, 是 driver_attach 过程的真正核心。bus_for_each_dev 会为总线上的每个设备调用一次此函数。它的作用是: 判断传入的驱动(drv)和设备(dev)是否匹配, 如果匹配, 就调用驱动的 probe 函数来初始化设备。

工作原理:

  1. 匹配设备: 它首先调用 driver_match_device()。这个函数会调用总线的 match 方法来判断驱动和设备是否兼容(例如, 比较SPI设备ID或USB VID/PID)。
  2. 处理匹配结果:
    • 不匹配 (ret == 0): 直接返回, 继续处理总线上的下一个设备。
    • 延迟探测 (ret == -EPROBE_DEFER): 这意味着匹配可能成功, 但设备的某个依赖项(如一个需要的时钟或regulator)尚未就绪。设备会被加入一个”延迟探测”列表, 内核将在稍后重新尝试为其探测。
    • 匹配失败 (ret < 0): 发生错误, 同样直接返回。
    • 成功匹配 (ret > 0): 继续执行绑定和探测。
  3. 探测 (Probing): 对于成功匹配的设备, 它会锁定设备, 然后调用 driver_probe_device()driver_probe_device() 会最终调用驱动程序自身实现的 probe 方法 (例如, my_spi_driver->probe)。probe 函数是驱动代码的核心, 它负责配置硬件寄存器、请求中断、设置DMA等所有与硬件交互的初始化工作。
  4. 异步探测: 函数还支持异步探测, 这在多核系统上可以提高启动速度。但在单核的STM32H750上, 其主要作用是将探测工作放入一个工作队列中稍后执行, 从而避免阻塞当前执行路径。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
static inline int driver_match_device(const struct device_driver *drv,
struct device *dev)
{
return drv->bus->match ? drv->bus->match(dev, drv) : 1;
}

/*
* 这是一个内部静态函数, 作为 bus_for_each_dev 的回调.
* @dev: 正在被迭代的总线上的某个设备.
* @data: 从 driver_attach 传递过来的上下文数据, 实际上是一个 const struct device_driver 指针.
*/
static int __driver_attach(struct device *dev, void *data)
{
/* 将 void* 指针安全地转换回其原始类型. */
const struct device_driver *drv = data;
bool async = false;
int ret;

/*
* 尝试将驱动和设备进行匹配.
*/
ret = driver_match_device(drv, dev);
if (ret == 0) {
/* 返回值为 0 表示不匹配. 直接返回0, 让 bus_for_each_dev 继续处理下一个设备. */
return 0;
} else if (ret == -EPROBE_DEFER) {
/*
* 返回值为 -EPROBE_DEFER 是一个特殊请求, 表示"现在不行, 稍后再试".
* 这通常是因为此设备依赖的另一个设备或资源还未就绪.
*/
dev_dbg(dev, "Device match requests probe deferral\n");
/* 标记此设备可以被匹配, 以便后续的通用探测可以再次尝试它. */
dev->can_match = true;
/* 将设备添加到全局的延迟探测列表中. */
driver_deferred_probe_add(dev);
/* 返回 0, 继续处理总线上的下一个设备. */
return 0;
} else if (ret < 0) {
/* 其他负值表示总线匹配函数出错. */
dev_dbg(dev, "Bus failed to match device: %d\n", ret);
/* 返回 0, 继续处理总线上的下一个设备. */
return 0;
} /* ret > 0 表示成功匹配. */

/* 检查驱动是否允许异步探测. */
if (driver_allows_async_probing(drv)) {
/*
* 与其同步探测设备, 不如异步探测以获得更好的并行性.
* 在单核系统上, 这意味着将工作推迟到工作队列中执行, 以免阻塞当前上下文.
*/
dev_dbg(dev, "probing driver %s asynchronously\n", drv->name);
/* 锁定设备以保护 dev->driver 和 dev->p->async_driver 字段. */
device_lock(dev);
/* 检查设备是否没有驱动, 并且也没有正在进行的异步探测任务. */
if (!dev->driver && !dev->p->async_driver) {
get_device(dev); // 增加设备引用计数, 因为异步任务将持有它.
dev->p->async_driver = drv; // 标记将要探测此设备的驱动.
async = true; // 标记我们将要调度一个异步任务.
}
device_unlock(dev);
if (async)
/* 调度一个异步工作, 该工作将调用 __driver_attach_async_helper 来执行实际的探测. */
async_schedule_dev(__driver_attach_async_helper, dev);
/* 返回0, 继续处理下一个设备. */
return 0;
}

/*
* 如果是同步探测, 锁定设备.
* __device_driver_lock 同时也锁定了设备的父设备, 防止在操作期间设备层次结构发生变化.
*/
__device_driver_lock(dev, dev->parent);
/*
* 调用 driver_probe_device(), 这是最终调用驱动程序probe()方法的地方.
*/
driver_probe_device(drv, dev);
/* 解锁设备. */
__device_driver_unlock(dev, dev->parent);

/*
* 总是返回 0.
* 这是为了确保 bus_for_each_dev 会继续遍历总线上的所有设备,
* 而不会因为某一次 attach 的结果而提前中止.
*/
return 0;
}

wait_for_device_probe: 等待所有设备探测完成

此函数是内核中一个至关重要的同步点, 尤其在系统启动后期或关机流程开始时。它的核心原理是通过多种同步机制, 确保系统中所有正在进行的、异步的设备探测(probe)活动全部执行完毕, 从而让系统进入一个”设备稳定”的状态

这个函数像是一个三道关卡的检查站, 每一道关卡都解决一种特定类型的异步活动:

  1. 第一关 (flush_work(&deferred_probe_work)): 内核中有一个专门的”延迟探测”工作队列(deferred_probe_work)。当一个设备的探测因为其依赖项(如另一个设备或固件)尚未就绪而失败并返回-EPROBE_DEFER时, 内核不会立即放弃, 而是会将这个探测任务放入这个工作队列中, 以便稍后重试。flush_work函数的作用就是同步地等待这个工作队列中的所有任务全部执行完毕。这确保了所有可重试的探测都已经完成了它们的最后一次尝试。

  2. 第二关 (wait_event(probe_waitqueue, ...): 内核维护着一个原子计数器probe_count, 用于记录当前有多少个设备正在执行其probe函数。每当一个设备的探测开始时, 这个计数器会增加; 当探测结束时, 计数器会减少。wait_event函数会让当前进程在一个名为probe_waitqueue的等待队列上睡眠, 直到probe_count的值变为0。这保证了即使是那些没有使用延迟探测机制的、正在进行的探测也能被等待。

  3. 第三关 (async_synchronize_full()): 内核的异步执行框架(async)允许驱动程序将探测过程的一部分(或全部)分派到后台线程中异步执行。async_synchronize_full函数的作用是等待所有通过async_schedule启动的异步函数调用全部执行完成。这是一个兜底的同步机制, 确保了所有形式的异步探测都已经结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* wait_for_device_probe - 等待设备探测完成.
*/
void wait_for_device_probe(void)
{
/*
* 步骤1: 等待"延迟探测"工作队列完成.
* flush_work() 会阻塞当前执行流程, 直到 &deferred_probe_work 这个工作队列中
* 所有挂起的任务都被执行完毕.
* 这确保了所有因为依赖未满足而需要重试的设备探测都已经完成了它们的最后尝试.
*/
flush_work(&deferred_probe_work);

/*
* 步骤2: 等待所有正在进行的探测完成.
* wait_event() 是一个标准的内核同步原语. 它会让当前进程在 probe_waitqueue
* 这个等待队列上睡眠, 直到第二个参数(一个布尔表达式)变为真.
* 在这里, 它等待 probe_count 这个原子计数器的值变为0. probe_count
* 记录了当前有多少个驱动的 probe() 函数正在执行.
*/
wait_event(probe_waitqueue, atomic_read(&probe_count) == 0);
/*
* 步骤3: 同步所有异步函数调用.
* async_synchronize_full() 会等待所有由 async_schedule() 提交的
* 异步任务全部执行完成. 这是为了处理那些将探测过程的一部分或全部
* 放在后台线程中执行的驱动程序.
*/
async_synchronize_full();
}
/*
* 将此函数导出, 使其对内核其他部分(如关机流程)可用.
*/
EXPORT_SYMBOL_GPL(wait_for_device_probe);

device_block_probing: 阻塞新的设备探测

此函数的作用是在内核中设置一个”全局开关”, 暂时阻止任何新的设备探测(probe)被启动, 并将所有新的探测请求推迟处理。它的核心原理是一个简单而有效的两阶段同步过程, 用以”冻结”设备模型的探测活动, 使其进入一个可预测的稳定状态。

这个过程如下:

  1. 设置延迟标志 (defer_all_probes = true;): 函数首先将一个全局布尔标志defer_all_probes设置为true。内核的设备模型核心在尝试为一个新设备调用其驱动的probe函数之前, 会检查这个标志。如果标志为true, 探测将不会被执行, 而是被自动放入”延迟探测”工作队列中, 等待该标志被清除后才重试。这相当于关上了接纳新设备初始化的大门

  2. 同步现有探测 (wait_for_device_probe();): 仅仅”关上大门”是不够的, 因为在设置标志的那一刻, 可能已经有其他设备的probe函数正在执行中。因此, 函数紧接着调用了wait_for_device_probe()。这个调用会阻塞并等待所有当前正在运行的、各种形式的异步探测全部执行完毕。这相当于等待所有已经进门的客人就座

通过这两个步骤的组合, device_block_probing函数确保了当它返回时, 整个系统的设备探测活动已经完全静止: 不会有新的探测开始, 并且所有之前开始的探测也已经结束。这个函数在系统进入关机流程时由device_shutdown调用, 是一个至关重要的准备步骤, 它保证了在逐个关闭设备时, 设备列表不会被意外地修改, 从而确保了关机流程的稳定性和可预测性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* device_block_probing() - 阻塞/推迟设备的探测
*
* 此函数将禁用设备的探测, 并代之以推迟它们的探测.
*/
void device_block_probing(void)
{
/*
* 步骤1: 设置全局标志.
* 将 defer_all_probes 设置为 true. 内核驱动核心在执行 probe 之前
* 会检查此标志. 如果为 true, 探测将被推迟, 并被放入 deferred_probe_work
* 工作队列中, 等待将来此标志被清除后重试.
*/
defer_all_probes = true;
/*
* 步骤2: 与正在进行的探测进行同步, 以避免竞争条件.
* 调用 wait_for_device_probe() 会阻塞执行, 直到所有在设置
* defer_all_probes 标志之前就已经开始的 probe() 函数全部执行完毕.
* 这确保了当此函数返回时, 系统中没有任何正在进行的探测活动.
*/
wait_for_device_probe();
}