Linux总线,设备和驱动关系以及匹配机制解析
在 Linux 内核中,总线(Bus)、设备(Device)和驱动(Driver) 构成了设备模型的核心,它们之间的关系以及匹配机制是实现硬件抽象、设备发现和驱动加载的关键。其核心思想是 “分离” 和 “基于约定的匹配”。
核心关系:
-
总线(Bus):
- 角色: 充当媒介和管理者。
- 职责:
- 定义一种连接设备的方式(如 PCI、USB、I2C、SPI、平台总线等)。
- 提供一组标准操作(
struct bus_type
),用于枚举、探测、管理挂载在其上的设备和驱动。 - 维护两个重要的内核链表:设备链表 和 驱动链表(所有注册到该总线类型的设备和驱动都会被分别加入这些链表)。
- 实现设备与驱动的匹配逻辑(这是最关键的功能)。
- 当新设备添加或新驱动注册时,负责在各自的链表上进行扫描匹配。
- 提供设备的热插拔事件通知机制。
-
设备(Device):
- 角色: 代表一个物理或虚拟的硬件实体。
- 职责:
- 包含描述该硬件特征的信息(
struct device
或更具体的如struct pci_dev
,struct usb_device
)。 - 这些信息是匹配的关键依据,通常包括:
- 设备标识符: Vendor ID, Device ID, Subsystem ID, Class Code (PCI/USB), 兼容字符串 (设备树/ACPI)。
- 资源信息: I/O 端口地址、内存映射地址(MMIO)、中断号(IRQ)、DMA 通道等。
- 通过
device_register()
或总线特定的注册函数(如pci_device_register()
,platform_device_register()
)向所属的总线注册自己。 - 当成功匹配到一个驱动后,会被“绑定”到该驱动。
- 包含描述该硬件特征的信息(
-
驱动(Driver):
- 角色: 包含控制特定类型设备的代码。
- 职责:
- 实现设备的初始化、配置、数据传输、电源管理、中断处理等操作(
struct device_driver
或更具体的如struct pci_driver
,struct platform_driver
)。 - 声明它能支持哪些设备。这是通过一个设备 ID 表(
id_table
)或兼容性字符串列表(of_match_table
,acpi_match_table
)来实现的。 - 包含一个核心的
probe()
函数。当总线匹配到一个设备和该驱动时,总线会调用驱动的probe()
函数。probe()
负责:- 检查设备是否真的是该驱动能控制的(二次验证)。
- 分配驱动特定的数据结构。
- 初始化硬件(配置寄存器、申请资源 IRQ, IO, DMA)。
- 注册设备到内核框架(如网络设备注册
netif_register()
,块设备注册blk_register()
,字符设备注册cdev_add()
等)。
- 包含
remove()
函数(设备断开或驱动卸载时调用)以及其他可选函数(suspend
,resume
,shutdown
等)。 - 通过
driver_register()
或总线特定的注册函数(如pci_driver_register()
,platform_driver_register()
)向所属的总线注册自己。
- 实现设备的初始化、配置、数据传输、电源管理、中断处理等操作(
总结关系图:
+---------------------+| Bus | <--- (管理)| (e.g., pci_bus_type,| | 维护设备列表| platform_bus_type)| | 维护驱动列表+----------+----------+ | 实现匹配逻辑| |+------------------+-----------------+| |
+----------v----------+ +----------v----------+
| Device | | Driver |
| (struct device, | | (struct device_driver,|
| pci_dev, etc.) | | pci_driver, etc.) |
| - Vendor/Device ID | <---(匹配)--| - id_table |
| - Resources (IRQ, | | - probe() function |
| MMIO) | | - remove() function |
| - Compatible strings| +---------------------+
+---------------------+
驱动和设备的匹配机制:
匹配过程是总线类型(struct bus_type
)的核心职责,由其 .match()
方法(通常是 bus_type.match
)实现。匹配发生在以下两种主要场景:
-
新设备注册: 当一个设备(
dev
)向总线注册时:- 总线会遍历其驱动链表上的所有已注册驱动(
drv
)。 - 对每个驱动
drv
,调用总线类型的.match(dev, drv)
方法。 - 如果
.match()
返回成功(非零),表示找到一个匹配的驱动。 - 总线随后会尝试将设备
dev
绑定到这个驱动drv
(通常会异步调度执行驱动的probe()
函数)。
- 总线会遍历其驱动链表上的所有已注册驱动(
-
新驱动注册: 当一个驱动(
drv
)向总线注册时:- 总线会遍历其设备链表上的所有已注册设备(
dev
)。 - 对每个设备
dev
,调用总线类型的.match(dev, drv)
方法。 - 如果
.match()
返回成功(非零),表示找到一个匹配的设备。 - 总线随后会尝试将设备
dev
绑定到这个新注册的驱动drv
(调用驱动的probe()
)。
- 总线会遍历其设备链表上的所有已注册设备(
.match()
方法如何工作?
.match()
方法的具体实现取决于总线类型。以下是常见总线匹配方式的原理:
-
基于 ID Table 的精确匹配 (PCI, USB 等):
- 驱动定义一个静态数组
id_table
(如struct pci_device_id mydrv_pci_ids[]
或struct usb_device_id mydrv_usb_ids[]
)。 - 表中每一项包含驱动支持的设备的精确标识符(Vendor ID, Device ID, Subvendor ID, Subdevice ID, Class, Class Mask 等)。
- 总线(如
pci_bus_type.match
即pci_match_device
)的工作:- 提取设备
dev
的标识符(从 PCI 配置空间读取)。 - 遍历驱动
drv
的id_table
。 - 将设备标识符与
id_table
中的每一项进行比较(通常支持掩码匹配)。 - 如果找到一项完全匹配(考虑掩码),
.match()
返回成功(匹配项的指针,非 NULL)。
- 提取设备
- 关键点: 匹配基于精确的数字 ID。
- 驱动定义一个静态数组
-
基于兼容字符串的模糊匹配 (设备树 - DT, ACPI, 平台设备):
- 设备端: 设备(通常是
platform_device
或通过 DT/ACPI 描述的设备)在其属性中声明一个或多个compatible
字符串。这些字符串描述了设备的硬件兼容性(如"vendor,chip-model"
,"generic-name"
)。 - 驱动端: 驱动定义一个
of_match_table
(用于 DT)或acpi_match_table
(用于 ACPI),其中包含一个或多个of_device_id
/acpi_device_id
结构体。每个结构体包含一个或多个该驱动支持的compatible
字符串。 - 总线(如
platform_bus_type.match
通常最终调用of_driver_match_device
或acpi_driver_match_device
)的工作:- 获取设备
dev
的compatible
字符串列表。 - 遍历驱动
drv
的of_match_table
/acpi_match_table
。 - 对于表中的每一项,将其包含的
compatible
字符串与设备的所有compatible
字符串依次比较(从最具体到最通用)。 - 如果找到完全相同的字符串,
.match()
返回成功(匹配项的指针)。
- 获取设备
- 关键点: 匹配基于字符串比较。驱动可以声明支持多个兼容字符串(按优先级),设备也可以声明多个兼容字符串(从最具体到最通用)。第一个找到的完全匹配字符串决定了匹配。这提供了很大的灵活性,允许一个驱动支持多个相似硬件,或者一个设备可以被更通用的驱动支持。
- 设备端: 设备(通常是
-
名称匹配 (较旧/简单的平台设备):
- 设备在注册时指定一个名称(
platform_device.name
)。 - 驱动在注册时也指定一个名称(
platform_driver.driver.name
)。 - 总线(
platform_bus_type.match
的简单实现)直接比较dev->name
和drv->driver.name
。如果相同,则匹配。 - 这种方式不够灵活,现在更推荐使用兼容字符串匹配。
- 设备在注册时指定一个名称(
-
ACPI ID 匹配 (ACPI):
- 类似于 ID Table 匹配,但使用 ACPI 定义的硬件标识符 (HID, CID, UID 等)。
- 驱动定义
acpi_match_table
包含支持的 ACPI 设备 ID。 - 总线匹配函数比较设备的 ACPI ID 和驱动支持的 ID。
匹配成功后的绑定 (probe
):
- 一旦总线
.match()
方法确认设备和驱动匹配,总线核心会调用驱动的probe()
函数,并将匹配的设备作为参数传递给它。 probe()
函数是驱动初始化的核心:- 它执行硬件特定的初始化(检查设备是否真的存在且功能正常 - 二次确认)。
- 申请所需的资源(IRQ, I/O 内存, DMA 缓冲区)。
- 初始化设备硬件(配置寄存器)。
- 将设备注册到相应的内核子系统(如网络栈、输入子系统、块层、字符设备框架等)。
- 通常会将一个指向驱动特定数据结构的指针存储在
dev->driver_data
中,以便后续驱动函数(如remove
,interrupt
)使用。
- 如果
probe()
成功返回 (0
),设备和驱动就被认为是绑定在一起了。 - 如果
probe()
失败(返回错误码),绑定过程失败,设备会回到未绑定状态,总线可能会继续尝试匹配其他驱动(如果该设备支持多个驱动)。
关键特性:
- 动态性: 设备和驱动可以在系统运行的任何时候注册(热插拔、模块加载)。
- 延迟匹配/绑定: 设备和驱动可以按任意顺序注册。如果设备先注册但当时没有匹配的驱动,它会被放在总线的设备链表上等待。当匹配的驱动稍后注册时,总线会自动触发匹配和绑定。反之亦然(驱动先注册,等待设备)。
- 多对一支持: 一个驱动通常可以支持多个设备(只要它们符合驱动
id_table
或compatible
字符串的定义)。 - 抽象层: 总线层将设备发现的细节(如扫描 PCI 配置空间、解析设备树)与驱动核心逻辑隔离开。驱动只需要关心它支持什么设备以及如何操作它们。
总结:
Linux 设备模型通过总线、设备、驱动三者的分离,实现了硬件的抽象和动态管理。总线是核心协调者,它维护设备和驱动的列表,并负责实现匹配逻辑。匹配主要基于设备提供的标识信息(精确 ID 或兼容字符串)与驱动声明的支持信息(id_table
或 of_match_table
/ acpi_match_table
)之间的比较。匹配成功后,驱动的 probe()
函数被调用,完成设备的初始化和向内核子系统的注册,最终使设备可用。这套机制是 Linux 支持海量硬件并实现热插拔和模块化驱动的基石。