Linux内核platform抽象、数据结构、内核匹配机制

什么是LDM

LDM用于描述和管理内核对象(需要引用计数的文件、设备、总线、驱动程序),包括struct device、struct device driver和struct bus_type。

struct device表示要驱动的设备,struct device driver表示驱动设备的软件实体,struct bus_type表示设备和CPU之间的通道

device结构体

设备建立在device结构体之上,device结构体定义在include/linux/device.h文件中:

parent

设备的父设备,即该设备连接到的设备,在大多数情况下父设备是某种总线或主机控制器,如果父设备是NULL代表该设备是顶层设备。

kobj

最低级别的数据结构,用于跟踪内核对象(总线、驱动程序、设备等),是LDM的核心

bus

指定设备所在的总线类型,它是设备和CPU之间的通道。

driver

指定哪个驱动程序分配这个设备

每个设备需要对应的驱动程序才能工作,就像户籍卡上记录 "对接的社区管理员"。

核心:内核通过 bus 匹配 "设备" 和 "驱动"(比如 I2C 总线上的传感器,匹配对应的 I2C 传感器驱动),driver 字段就是设备绑定的那个 "办事员"。

platform_data

提供特定于设备的平台数据。当从开发板文件中声明设备时,此字段将被自动设置。换句话说,它指向特定于开发板的结构,该结构位于开发板的设置文件中,设置文件描述了设备及其接线方式。它有助于将#ifdef最小化到设备驱动程序代码中。它包含诸如芯片变体、GPIO引脚角色和中断线等资源。

是 "开发板专属的硬件配置"(比如传感器的 GPIO 引脚、中断号、芯片型号),从老的板级 C 文件(你之前看到的 i2c1_resources 所在的文件)自动填充。

核心:避免驱动代码里写死不同开发板的配置(减少 #ifdef),但现在被 of_node 替代了。

driver_data

一个指向驱动程序特定信息的私有指针。总线控制器驱动程序负责提供辅助函数,即访问器,用于获取/设置此字段。

是驱动程序给设备记录的 "私有信息"(比如设备当前的运行状态、缓存数据、校准参数),只有对应的 driver 能访问和修改。

核心:驱动用它存储设备的运行时数据,和 platform_data(硬件配置)的区别是 ------ 前者是 "运行时动态数据",后者是 "静态硬件配置"。

pm_domain

指定电源管理特定的回调函数,在系统电源状态(挂起、休眠、系统恢复以及运行时电源管理转换)更改期间以及子系统级和驱动程序级回调函数中执行该回调函数。

负责设备的 "电源作息管理"(比如休眠时断电、唤醒时上电、低功耗模式切换),就像户籍卡上记录 "负责该居民用电管理的电工"。

核心:内核在系统休眠 / 唤醒时,通过 pm_domain 的回调函数控制设备的电源状态。

of_node

与设备相关联的设备树节点。当从设备树中声明设备时,该字段会由OF核心自动填充。你可以通过检查platform_data或of_node是否已设置,来确定设备已在何处声明。

是设备树节点的指针,从设备树(DTS)读取硬件配置(比如你之前问的 interrupts、reg 参数),替代了老的 platform_data。

核心:现在主流的硬件配置方式,内核通过 of_node 能拿到设备的中断、地址、自定义参数等所有硬件信息;判断设备是 "老版板级文件定义" 还是 "新版设备树定义",只需看 platform_data 或 of_node 哪个有值。

id

设备实例

设备很少由裸设备结构体表示,因为大多数子系统会跟踪其托管的设备的额外信息。相反,该结构体通常被嵌入设备的更高级别表示中。i2c_client、spi_device、usb_device和platform_device结构体就是这种情况,它们都在自己的成员中嵌入了一个device结构体元素(spi_device->dev、i2c_client->dev、usb_device->dev和platform_device->dev)​。

比如系统里有 2 个相同的 UART 串口,id=0 标识第一个,id=1 标识第二个。

核心:区分同一类型的多个设备实例,内核靠 id 给每个实例分配独立的资源和驱动上下文。

假设你的开发板上有一个 I2C 温度传感器,它的 "户籍卡" 信息如下:

parent:指向 "I2C1 控制器"(传感器插在 I2C1 总线上);

kobj:内核分配的唯一标识(比如 0xffff880021a00000);

bus:I2C 总线(传感器通过 I2C 和 CPU 通信);

driver:指向 "tmp102_temp_sensor_driver"(这个传感器的专属驱动);

platform_data:老版本里记录 "传感器中断号 50、GPIO 引脚 PA1"(从板级文件来);

driver_data:驱动记录的 "当前温度 25℃、采样频率 1Hz"(驱动私有数据);

pm_domain:指向电源管理回调(休眠时关闭传感器电源,唤醒时重新初始化);

of_node:新版本里指向设备树中tmp102@48节点(从设备树读取中断、地址等配置);

id:0(如果有 2 个同款传感器,另一个 id=1)。

device_driver结构体

device_driver结构体是任何设备驱动程序的基本元素。在面向对象编程语言中,该结构体将作为基类,由每个设备驱动程序继承。

该结构体定义在include/linux/device/driver.h文件中:

name:

设备驱动程序的名称。当没有匹配成功的名称时,可将它作为后备选项(也就是将该名称与设备名称匹配)​。

字面含义:驱动的字符串名称(比如"i2c1-tmp102")。

实际作用:

主要作为后备匹配方式:当驱动的专用匹配表(如 of_match_table)匹配失败时,内核会尝试将这个名称与设备的名称直接比对,匹配成功就绑定。

辅助作用:内核日志中会显示这个名称,方便调试(比如 123.456 i2c1-tmp102: probe success)。

场景举例:如果你的驱动 name 是"my-i2c-sensor",而某个设备的 name 也配置为这个值,即使没有设备树匹配表,内核也能通过名称匹配。

bus:

此字段是必填项,表示设备驱动程序所属的总线。如果未设置此字段,设备驱动程序将注册失败,因为它的探测方法负责将驱动程序与设备匹配。

字面含义:指向驱动所属的总线结构体(如&i2c_bus_type/&platform_bus_type)。

实际作用:

内核管理驱动的核心依据:不同总线有不同的 "设备 - 驱动匹配规则",驱动必须归属某条总线,否则内核不知道该 "交给谁去匹配设备",注册时直接失败。

总线是驱动和设备的 "中间人":比如 I2C 驱动必须归属 I2C 总线,总线控制器会负责调用驱动的 probe 函数。

关键提醒:嵌入式中最常见的是platform_bus_type(平台总线),几乎所有片上外设(I2C / 串口 / GPIO)的驱动都归属平台总线。

owner:

此字段指定模块所有者。

字面含义:指向驱动所属的内核模块。

实际作用:

内核用于模块引用计数管理:防止驱动模块在被使用时被意外卸载(比如设备还在工作,就不能卸载驱动)。

几乎所有驱动都会设置为THIS_MODULE(宏,指向当前模块自身),这是固定写法。

代码示例:owner = THIS_MODULE;

of_match_table:

这是OF表,表示用于设备树匹配的of_device_id结构体数组。

字面含义:of_device_id结构体数组,存储驱动能匹配的设备树节点特征(主要是compatible属性)。

实际作用:

嵌入式系统中最核心的匹配方式(替代老版本的 name 匹配):驱动通过这个表告诉内核 "我能支持哪些设备树里的设备"。

匹配逻辑:内核遍历设备树节点,对比节点的compatible属性和驱动of_match_table里的compatible字符串,一致则匹配成功。

C 复制代码
// 定义匹配表:驱动支持compatible为"my,tmp102-sensor"的设备树节点
static const struct of_device_id tmp102_of_match[] = {
    { .compatible = "my,tmp102-sensor" },
    { /* 终止项,必须有 */ },
};
MODULE_DEVICE_TABLE(of, tmp102_of_match); // 向内核声明匹配表

// 驱动结构体中引用
struct platform_driver tmp102_driver = {
    .driver = {
        .name = "tmp102-sensor",
        .bus = &platform_bus_type,
        .owner = THIS_MODULE,
        .of_match_table = tmp102_of_match, // 关联设备树匹配表
    },
    .probe = tmp102_probe,
    .remove = tmp102_remove,
};

acpi_match_table:

这是ACPI(Advanced Configuration and Power Interface,高级配置和电源接口)匹配表。它与of_match_table相同,但用于ACPI匹配。

字面含义:和of_match_table逻辑完全一致,但用于 ACPI 协议的系统(主要是 x86 / 服务器,而非嵌入式)。

实际作用:

嵌入式开发中几乎用不到(嵌入式主流是设备树,而非 ACPI);

x86 系统中,驱动通过这个表匹配 ACPI 表中的设备信息,替代设备树的作用。

probe:

此函数用于查询特定设备的存在性,以及判断是否可以使用这个驱动程序并将这个驱动程序绑定到特定设备。总线驱动程序负责在给定时刻调用此函数。

字面含义:驱动的核心函数,原型通常是int xxx_probe(struct device *dev, struct device_driver *drv)(不同总线略有差异)。

实际作用(驱动最核心的函数,没有之一):

检测设备是否存在:读取硬件寄存器 / 设备树配置,确认设备真的在系统中;

绑定驱动与设备:为设备分配资源(申请中断、映射物理地址、创建字符设备节点等);

初始化设备:配置设备的工作模式(比如传感器的采样率、串口的波特率);

返回值:成功返回 0,失败返回负数(内核会认为匹配失败,尝试下一个驱动)。

调用时机:内核在驱动与设备匹配成功后,由总线驱动自动调用。

remove:

当从系统中移除设备时,调用此函数可以将设备与驱动程序解绑。

字面含义:原型通常是void xxx_remove(struct device *dev)。

实际作用:

当设备被移除(或驱动被卸载)时,内核调用此函数,完成 "解绑清理":

释放 probe 函数中申请的资源(释放中断、解除地址映射、删除字符设备节点);

恢复设备到初始状态,防止资源泄漏。

调用时机:设备从系统中移除、驱动模块被卸载时。

shutdown:

当设备即将关闭时,就会发出此命令。

字面含义:设备关闭(系统关机 / 重启)时调用的回调函数。

实际作用:

做 "紧急收尾":比如将设备寄存器恢复默认值、保存关键数据到闪存、切断设备电源等;

区别于 remove:shutdown 是 "系统级关机" 触发,remove 是 "设备单独移除" 触发。

suspend:

这是一个回调函数,它允许你将设备置于睡眠模式。

字面含义:设备进入休眠(低功耗)模式前调用的回调。

实际作用:

休眠前的 "节能准备":比如关闭设备的时钟、保存设备当前的配置参数、禁用中断、将设备置于低功耗状态;

确保设备休眠时不耗电,且唤醒后能恢复原有状态。

resume:

该函数由驱动程序核心调用,用于唤醒处于睡眠模式的设备。

字面含义:设备从休眠模式唤醒时调用的回调。

实际作用:

恢复 suspend 中保存的配置:重新启用时钟、恢复寄存器参数、重新注册中断、让设备回到休眠前的工作状态;

比如传感器休眠后唤醒,resume 函数会重新配置采样率,恢复数据采集。

pm:

表示一组电源管理回调函数。在上述数据结构中,shutdown、suspend、resume和pm是可选的,它们用于电源管理。是否提供它们取决于底层设备的功能(是否可以关闭、挂起或执行其他与电源管理相关的功能)​。

字面含义:指向struct dev_pm_ops结构体,该结构体封装了 suspend/resume/shutdown 等电源管理回调。

实际作用:

把分散的 suspend/resume/shutdown 等函数 "整合管理":如果设备的电源管理逻辑复杂(比如不同休眠深度有不同处理),可以通过 pm 字段统一配置,替代单独的 suspend/resume 字段;

可选字段:如果设备不需要复杂的电源管理,直接定义单独的 suspend/resume 即可,无需设置 pm。

C 复制代码
// 定义电源管理回调集合
static const struct dev_pm_ops tmp102_pm_ops = {
    .suspend = tmp102_suspend,
    .resume = tmp102_resume,
    .shutdown = tmp102_shutdown,
};

// 驱动结构体中引用
struct platform_driver tmp102_driver = {
    .driver = {
        .name = "tmp102-sensor",
        .bus = &platform_bus_type,
        .owner = THIS_MODULE,
        .of_match_table = tmp102_of_match,
        .pm = &tmp102_pm_ops, // 关联电源管理回调
    },
    .probe = tmp102_probe,
    .remove = tmp102_remove,
};

struct dev_pm_ops远不止 suspend/resume/shutdown 这三个成员,它是 Linux 内核统一的电源管理接口框架,包含了更多细分的电源管理场景回调。你看到的只是 "冰山一角",完整的 dev_pm_ops 结构体(简化版)长这样:

C 复制代码
struct dev_pm_ops {
    // 1. 系统级休眠/唤醒(对应你用到的基础场景)
    int (*suspend)(struct device *dev);
    int (*resume)(struct device *dev);
    void (*shutdown)(struct device *dev);
    
    // 2. 细分的休眠状态(比如冻结/解冻、挂起到内存/磁盘)
    int (*freeze)(struct device *dev);    // 冻结(比如系统准备休眠前的预冻结)
    int (*thaw)(struct device *dev);      // 解冻
    int (*suspend_late)(struct device *dev); // 休眠后期(核心已休眠,仅外设处理)
    int (*resume_early)(struct device *dev); // 唤醒前期(核心未唤醒,先恢复外设)
    
    // 3. 运行时电源管理(最核心的扩展能力!单独设置做不到)
    int (*runtime_suspend)(struct device *dev); // 运行时休眠(设备闲置时自动低功耗)
    int (*runtime_resume)(struct device *dev);   // 运行时唤醒(设备被访问时恢复)
    int (*runtime_idle)(struct device *dev);     // 运行时闲置检测(判断是否该休眠)
};

注册驱动程序

注册设备包括将设备插入其总线驱动程序维护的设备列表中。注册驱动程序包括将驱动程序插入由其所在的总线驱动程序维护的驱动程序列表中。

注册USB设备驱动程序就是将USB设备驱动程序插入由USB控制器驱动程序维护的驱动程序列表中。

注册SPI设备驱动程序就是将SPI设备驱动程序插入由SPI控制器驱动程序维护的驱动程序列表中。

driver_register()是用于将驱动程序注册到总线的低级函数,它会将驱动程序添加到总线驱动程序维护的驱动程序列表中。

总线(Bus):相当于 "硬件中介平台"(比如 USB 总线、platform 总线),所有接入该总线的设备和驱动都要在这个平台上登记。

设备(Device):就是实际的硬件(比如一个 LED 灯、一个 I2C 传感器),它会在总线上 "登记" 自己的身份信息(比如 compatible 属性、设备 ID)。

驱动(Driver):就是操作硬件的 "手册",也会在总线上 "登记" 自己能处理的硬件类型(比如支持的 compatible 值、设备 ID)。

match () 回调:就是中介的 "核对规则"------ 拿着驱动的 "适配清单" 去比对设备的 "身份信息",判断这本手册是否适配这个硬件。

绑定(Binding):核对成功后,把 "设备" 和 "驱动" 关联起来,相当于 "把手册发给对应硬件",之后硬件就能被驱动控制了。

一般来说使用特定于总线的注册函数代替driver_register()函数,到目前为止,总线特定的注册函数始终匹配{bus_name}_register_driver()模式。例如,USB、SPI、I2C和PCI设备驱动程序的注册函数分别是usb_register_driver()、spi_register_driver()、i2c_register_driver()和pci_register_driver()。

建议在模块的init/exit函数中注册/注销驱动程序,它们分别在模块加载/卸载阶段执行。在许多情况下,注册/注销驱动程序是你想在init/exit函数中执行的唯一操作。在这种情况下,每个总线核心将提供特定的辅助宏,该辅助宏可以扩展为模块的init/exit函数,并在内部调用总线特定的注册/注销函数。这些总线宏遵循module_{bus_name}driver(*{bus_name}driver)的模式,其中* {bus_name}_driver是相应总线的驱动程序结构体。表6.1给出了Linux系统支持的总线非详尽列表以及它们的宏。

总线控制器代码负责提供这些宏,但情况并非总是如此。例如,MDIO(ManagementData Input/Output,管理数据输入/输出)总线(一种用于控制网络设备的2线串行总线)驱动程序不提供module_mdio_driver()宏。因此在使用它之前,应该检查设备所在的总线是否提供此宏。下面展示了两个不同总线的示例:一个使用总线提供的注册/注销宏,另一个则不使用。让我们先看看不使用宏时的代码是什么样子:

C 复制代码
#include <linux/platform_device.h>

// 1. 定义platform驱动结构体(你之前看到的tmp102_driver)
static struct platform_driver tmp102_driver = {
    .driver = {
        .name = "tmp102-sensor",
        .bus = &platform_bus_type,
        .owner = THIS_MODULE,
        .of_match_table = tmp102_of_match,
        .pm = &tmp102_pm_ops,
    },
    .probe = tmp102_probe,
    .remove = tmp102_remove,
};

// 2. 手动写init函数:模块加载时注册驱动
static int __init tmp102_driver_init(void)
{
    // 调用platform总线的注册函数
    return platform_driver_register(&tmp102_driver);
}

// 3. 手动写exit函数:模块卸载时注销驱动
static void __exit tmp102_driver_exit(void)
{
    // 调用platform总线的注销函数
    platform_driver_unregister(&tmp102_driver);
}

// 4. 关联init/exit函数(内核宏)
module_init(tmp102_driver_init);
module_exit(tmp102_driver_exit);

MODULE_LICENSE("GPL");

上面的示例不使用宏。下面让我们看看使用宏的代码是什么样子:

C 复制代码
#include <linux/platform_device.h>

// 1. 定义platform驱动结构体(和之前完全一样)
static struct platform_driver tmp102_driver = {
    .driver = {
        .name = "tmp102-sensor",
        .bus = &platform_bus_type,
        .owner = THIS_MODULE,
        .of_match_table = tmp102_of_match,
        .pm = &tmp102_pm_ops,
    },
    .probe = tmp102_probe,
    .remove = tmp102_remove,
};

// 2. 一行宏替代所有init/exit代码!
module_platform_driver(tmp102_driver);

MODULE_LICENSE("GPL");

在驱动程序中公开支持的设备

内核必须知道给定的驱动程序支持哪些设备,如果查看每个特定于总线的设备驱动程序结构体比如platform_driver, i2c_driver, spi_driver, pci_driver和usb_driver,可以看到一个id_table字段,其类型取决于总线类型,此字段被赋予一个设备ID数组,设备ID对应驱动程序支持的设备。

有两种特殊情况我们故意省略了:设备树和ACPI。它们可以通过使用driver.of_match_table或driver.acpi_match_table字段,在设备树或ACPI中公开设备的硬件信息,这些字段不是总线特定驱动程序结构的直接元素。简单来说,设备树和ACPI可以让驱动程序声明设备,而不用直接在总线特定驱动程序结构中对它们进行声明

使用spi_device_id和of_device_id公开SPI设备的例子

C 复制代码
static const struct spi_device_id mcp23s08_ids[] = {
	{"mcp23s08", MCP_TYPE_S08},
	{"mcp23s17", MCP_TYPE_S17},
	{"mcp23s18", MCP_TYPE_S18},
	{},
};

static const struct of_device_id mcp23s08_spi_of_match[] = {
	{
		.compatible = "microchip,mcp23s08",
		.data = (void *)MCP_TYPE_S08,
	},
	{
		.compatible = "microchip,mcp23s17",
		.data = (void*)MCP_TYPE_S17,
	},
	{
		.compatible = "microchip,mcp23s18",
		.data = (void *)MCP_TYPE_S18,
	},
	{},
};

static struct spi_driver mcp23s08_driver = {
	.probe = mcp23s08_probe,
	.remove = mcp23s08_remove,
	.id_table = mcp23s08_ids,
	.driver = {
		.name = "mcp23s08",
		.of_match_table = of_match_ptr(mcp23s08_spi_of_match),
	},
}

上面的例子展示了一个驱动程序如何声明它所支持的设备。这个示例是一个SPI驱动程序,所涉及的结构体是spi_device_id。结构体of_device_id则用于在驱动程序中根据兼容字符串匹配设备的任何驱动程序。

of_match_ptr是 Linux 内核的条件编译宏,核心作用是:根据内核是否启用了设备树(CONFIG_OF),决定给of_match_table赋值为传入的匹配表,还是 NULL,避免空指针错误。

设备/驱动程序匹配和模块(自动)加载

总线是设备驱动程序和设备依赖的基本元素。硬件上体现为设备和CPU之间的链接,软件上是设备和驱动程序之间的链接。每当设备或者驱动程序被添加/注册到系统中时,他都会被自动添加到他所在的总线驱动程序维护的驱动程序列表中。

注册一组I2C设备->由I2C总线驱动程序管理->排队到一个全局列表

每个设备驱动程序都应该公开其支持的设备列表,并使该设备列表可访问驱动程序核心(特别是总线驱动程序)​。这个设备列表称为id_table,可以在驱动程序代码中对其进行声明和填充。id_table是一个设备ID数组,其中每个设备ID的类型取决于设备的类型(如I2C、SPI、USB等)​。这样,每当设备出现在总线上时,总线驱动程序就会遍历其设备驱动程序列表,并查看每个id_table以找到对应于这个新设备的条目。在其id_table中包含设备ID的每个驱动程序都将运行它们的probe()函数,并将新设备作为参数传递。这个过程称为匹配循环。驱动程序的工作方式与此类似。每当把新的驱动程序注册到总线上时,总线驱动程序就会遍历其设备列表,并查找出现在注册的驱动程序的id_table中的设备ID。对于每个匹配项,相应的设备将作为参数传递给驱动程序的probe()函数,并且probe()函数将根据命中数运行多次。

MODULE_DEVICE_TABLE

内核的匹配循环只有在驱动模块加载(insmod/modprobe)后才会生效。但如果设备是热插拔动态出现的(比如 USB/SPI/PCI 设备),我们不可能提前手动加载所有驱动 ------ 这就需要 "设备出现时,用户空间自动找到并加载对应的驱动模块。

内核知道驱动支持哪些设备(比如你的 SPI 驱动里的spi_device_id/of_match_table);

用户空间(udev/modprobe 等工具)不知道这些信息,因为驱动的设备匹配表是内核态的代码,用户空间无法直接读取。

MODULE_DEVICE_TABLE的核心作用就是:把驱动里的设备匹配表(比如 spi_device_id、of_device_id)"导出" 到模块的 "元数据区域",让用户空间工具能通过modinfo等命令读取这些信息,从而知道 "哪个模块对应哪个设备"。

这个宏不执行任何代码,而是给内核编译器 / 链接器一个 "标记":"把这个驱动支持的设备列表,写到模块的 ELF 文件的特殊段(.modinfo)里"。

C 复制代码
// 1. 定义SPI设备ID表(你已有的)
static const struct spi_device_id mcp23s08_ids[] = {
	{"mcp23s08", MCP_TYPE_S08},
	{"mcp23s17", MCP_TYPE_S17},
	{"mcp23s18", MCP_TYPE_S18},
	{},
};
// 关键:用MODULE_DEVICE_TABLE导出SPI设备ID表
MODULE_DEVICE_TABLE(spi, mcp23s08_ids);

// 2. 定义设备树匹配表(你已有的)
static const struct of_device_id mcp23s08_spi_of_match[] = {
	{.compatible = "microchip,mcp23s08", .data = (void *)MCP_TYPE_S08},
	{.compatible = "microchip,mcp23s17", .data = (void*)MCP_TYPE_S17},
	{.compatible = "microchip,mcp23s18", .data = (void *)MCP_TYPE_S18},
	{},
};
// 补充:设备树匹配表也可导出(部分总线需要)
MODULE_DEVICE_TABLE(of, mcp23s08_spi_of_match);

// 3. 驱动结构体(你已有的)
static struct spi_driver mcp23s08_driver = {
	.probe = mcp23s08_probe,
	.remove = mcp23s08_remove,
	.id_table = mcp23s08_ids,
	.driver = {
		.name = "mcp23s08",
		.of_match_table = of_match_ptr(mcp23s08_spi_of_match),
	},
};

编译模块时,内核会:

读取mcp23s08_ids里的设备名("mcp23s08"/"mcp23s17"/"mcp23s18");

把这些信息写入模块文件(.ko)的.modinfo段(ELF 文件的元数据区域);

用户空间工具(如modinfo)可以直接读取这个区域的信息。

在编译时,构建过程从驱动程序中提取出这些信息,并构建一个可读的表格,称为modules.alias,它位于/lib/modules/kernel_version/目录下。

<bus_type_name>参数应该是总线的通用名称,你需要为其添加模块的自动加载支持。对于SPI总线,它应该是"spi"​;对于设备树,它应该是"of"​;对于I2C总线,它应该是"i2c"​,等等。换句话说,它应该是设备列表(请注意,并非所有总线都会列出)中第一列(总线类型)的元素之一。下面让我们为之前使用过的同一驱动程序(gpio-mcp23s08)添加模块的自动加载支持:

驱动自动加载完整链路

从设备出现到内核发通知、用户空间 udev 找到驱动、加载驱动并触发 probe 的全过程 ------ 而 MODULE_DEVICE_TABLE 是这条链路的 "源头",netlink/uevent 是 "通信管道",udev 和 module.alias 是 "执行环节"。

以 "插入 PCI 网卡(设备)→自动加载 e1000 驱动" 为例,对应pci:v00008086d000015B8svsd bcsci*别名和 e1000 模块,完整步骤如下:

步骤 1:设备出现,内核检测到并生成 "设备别名"

PCI 总线驱动(内核里的 PCI 子系统)检测到新设备,读取设备的硬件标识:

厂商 ID(v):8086(Intel)、设备 ID(d):15B8(e1000 网卡);

内核按 PCI 总线的规则,把这些标识转换成标准化别名:pci:v00008086d000015B8svsd bcsci*(v=vendor, d=device, sv = 子厂商,* 表示通配)。

内核通过「netlink 套接字」(内核和用户空间的专用通信通道),向用户空间发送一个uevent 事件;

这个事件里包含关键信息:设备的总线类型(PCI)、标准化别名(上面的 pci:v00008086...)、设备路径(如 /sys/devices/pci0000:00/0000:00:19.0)。

为什么用 netlink?因为内核态和用户空间不能直接通信,netlink 是内核设计的 "安全通信管道",专门传这类事件。

步骤 3:udev 捕获 uevent,查 module.alias 找驱动

udev(系统的热插拔管理器,运行在用户空间)实时监听 netlink 的 uevent,捕获到 "PCI 网卡出现" 的事件;

udev 会去读取系统的/lib/modules/$(uname -r)/modules.alias文件(就是你说的 module.alias),这个文件里记录了 "设备别名→驱动模块名" 的映射,比如:

plaintext 复制代码
# modules.alias里的条目(由MODULE_DEVICE_TABLE生成)
alias pci:v00008086d000015B8sv*sd*bc*sc*i* e1000

udev 匹配到 "pci:v00008086d000015B8..." 对应的模块名是 e1000。

步骤 4:udev 加载驱动模块,触发 probe

udev 执行modprobe e1000命令,加载 e1000 驱动模块;

驱动模块加载后,内核触发 SPI/PCI 总线的 "匹配循环":驱动的id_table/of_match_table和设备标识匹配;

匹配成功后,调用驱动的 probe 函数,初始化网卡 ------ 设备就可以正常使用了。

设备声明

板级文件声明

比如声明一个 SPI 设备(MCP23S08),早期会在板级文件里写:

C 复制代码
// 板级文件里硬编码声明SPI设备(旧方法)
static struct spi_board_info mcp23s08_board_info = {
    .modalias = "mcp23s08",  // 设备名(对应驱动的spi_device_id)
    .max_speed_hz = 1000000, // SPI速率
    .bus_num = 0,            // SPI总线号
    .chip_select = 0,        // CS引脚
    .platform_data = &mcp23s08_data, // 板级数据
};

// 系统启动时注册这个设备(告诉内核"有这个SPI设备")
spi_register_board_info(&mcp23s08_board_info, 1);

设备树声明

dts 复制代码
spi0: spi@021a0000 {
	compatible = "arm,p1022";
	reg = <0x021a0000 0x4000>;
	#address-cells = <1>;
	#size-cells = <0>;
	
	// 声明MCP23S08设备(告诉内核"这个SPI总线上有mcp23s08设备")
    mcp23s08@0 {
        compatible = "microchip,mcp23s08"; // 匹配驱动的of_match_table
        reg = <0>; // CS0引脚
        spi-max-frequency = <1000000>;
    };
};

声明的设备必须有对应的驱动模块设备表,否则被忽略

文档里说 "任何已声明的设备为了能让驱动程序进行处理,都应至少存在于一个模块设备表中",用例子解释就是:

假设你在设备树里声明了 MCP23S08 设备(告诉内核 "有这个设备"),但:

系统里没有加载支持 MCP23S08 的驱动;

或者加载的驱动里没有用MODULE_DEVICE_TABLE导出包含 "mcp23s08" 的 spi_device_id,也没有 of_match_table 匹配 "microchip,mcp23s08";

总线结构体bus_type

bus_type是内核用来表示总线(无论是物理总线还是虚拟总线)的结构体。总线控制器是任何层次结构的根元素。从物理上讲,总线是处理器与一个或多个设备之间的通道。从软件角度看,总线(struct bus_type)是设备(struct device)和驱动程序(structdevice_driver)之间的链接。如果没有该链接,系统中什么也不会添加,因为总线负责匹配设备和驱动程序:

C 复制代码
struct bus_type
{
	const char *name;
	struct device *dev_root;
	int (*match)(struct device *dev, struct device_driver *drv);
	int (*probe)(struct device *dev);
	int (*remove)(struct device *dev);
	[...]
};

name

总线名称,出现在/sys/bus/目录中。

match

这是一个回调函数,每当新的设备或驱动程序被添加到总线上时就会调用该函数。回调函数必须足够智能,并在设备和驱动程序之间存在匹配时返回非零值。它们都将作为参数给出,匹配回调函数的主要目的是允许总线确定是否可以由给定的驱动程序处理特定设备;或者如果给定的驱动程序支持特定设备,则允许总线执行其他逻辑。大多数情况下,验证过程是通过进行简单的字符串比较(设备和驱动程序名称,或表格和设备树兼容属性)完成的。对于枚举的设备(如PCI和USB)​,验证过程是通过比较驱动程序支持的设备ID和给定设备的设备ID完成的,而不会损失总线特定的功能。

probe

这也是一个回调函数,当新的设备或驱动程序被添加到总线上,并且已经发生匹配时就会调用这个函数。该函数负责分配特定的总线设备结构,并调用给定驱动程序的探测函数,探测函数被设计用于管理设备(之前已经分配了探测函数)​。

remove

一个函数,当你从总线上移除设备时就会调用这个函数。

当你为设备编写驱动程序时,如果这个设备连接在所谓的总线控制器(bus controller)的物理总线上,则该设备必须依赖这个总线的驱动程序(也就是控制器驱动程序)​,该驱动程序负责在设备之间对总线进行共享访问。控制器驱动程序会在设备和总线之间提供一个抽象层。例如,每当你在I2C或USB总线上执行读取或写入操作时,I2C/USB总线控制器就会在后台透明地管理时钟、移动数据等。每个总线控制器驱动程序都会导出一组函数,以便更轻松地为连接到该总线的设备开发驱动程序,这种方式适用于很多总线(包括I2C、SPI、USB、PCI、SDIO总线等)​。

设备与驱动程序匹配机制

设备驱动程序和设备总是注册在总线上。当涉及将驱动程序支持的设备导出(也就是将设备导出添加到设备树或总线的设备列表中)时,可以使用driver.of_match_table、driver.of_match_table或_driver.id_table(具体取决于特定的设备类型,例如i2c_device.id_ table或platform_device.id_table)​。每个总线驱动程序都有责任提供匹配的函数,当有新的设备或设备驱动程序在该总线上注册时,内核就会运行其提供的匹配函数。也就是说,平台设备有3种匹配机制,其中每一种匹配机制都涉及字符串比较。这3种匹配机制都基于设备树表、ACPI表、设备和驱动程序名称。下面让我们看看伪平台(pseudo-platform)和I2C总线是如何使用这些匹配机制实现匹配函数的:

C 复制代码
static int platform_match(struct device *dev, struct device_driver *drv)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct platform_driver *pdrv = to_platform_driver(drv);
	//第一优先级:设备强制指定驱动(driver_override)
	/*
	pdev->driver_override:platform 设备的 "强制驱动名" 字段(手动指定该设备必须用某个驱动);
	逻辑:如果设备设置了driver_override(比如手动指定要使用 "mcp23s08" 驱动),
	就比对这个字段和驱动的.name是否一致;
	!strcmp(a,b):strcmp 返回 0 表示字符串相等,取反后返回 1(匹配成功);
	优先级:最高优先级(手动强制匹配,忽略其他规则);
	适用场景:调试 / 特殊定制(比如强制让设备用某个测试驱动),日常开发很少用。
	*/
	if (pdev->driver_override)
		return !strcmp(pdev->driver_override, drv->name);
	//第二优先级:设备树匹配(嵌入式主流)
	if (of_driver_match_device(dev, drv))
		return 1;
	// 第三优先级:ACPI 匹配(x86 系统)
	if (acpi_driver_match_device(dev, drv))
		return 1;
	// 第四优先级:platform_device_id 表匹配
	if (pdrv->id_table)
		return platform_match_id(pdrv->id_table, pdev) != NULL;
	//最后优先级:设备名↔驱动名比对(兜底)
	return (strcmp(pdev->name, drv->name) == 0);
}

上面的代码给出了伪平台总线的匹配函数,该函数定义在drivers/base/platform.c文件中。下面的代码给出了I2C总线的匹配函数,该函数定义在drivers/i2c/i2c-core.c文件中:

OF匹配机制

在设备树中,每个设备都由一个节点表示,并声明为总线节点的子节点。在启动时,内核(OF核心)解析设备树中的每个总线节点(以及它们的子节点,即连接到总线节点的设备)​。对于每个设备节点,内核将执行以下操作。

· 标识设备节点所属的总线。· 根据设备节点中包含的属性,使用of_device_alloc()函数分配平台设备,并对其进行初始化,built_pdev->dev.of_ node将设置为当前设备树节点。

· 使用bus_for_each_drv()函数遍历与先前确定的总线关联(即由其维护)的设备驱动程序列表。

· 对于设备驱动程序列表中的每个设备驱动程序,内核将执行以下操作。

❏ 调用总线匹配函数,将找到的设备驱动程序和先前构建的设备结构作为参数传递给该函数,即bus_found->match(cur_drv, cur_dev)。

❏ 如果总线驱动程序支持设备树匹配机制,则总线匹配函数将调用of_driver_match_device(),给定的参数与先前提到的相同,即of_driver_match_device(cur_drv,cur_dev)。

❏ of_driver_match_device()将遍历与当前驱动程序相关联的of_match_table,这是一个数组,其中的每个元素都是名为of_device_id的结构体。对于该数组中的每个of_device_id元素,内核将比较当前of_device_id元素和built_ pdev-> dev.of_node的compatible属性是否相同。如果相同(假设它们匹配)​,则运行当前驱动程序的探测函数。

· 如果没有找到支持该设备的驱动程序,该设备仍将被注册到总线上。然后,探测机制将 被延迟到以后的某个时间,以便每当新的驱动程序被注册到总线上时,内核就遍历由总线维护的设备列表;任一设备如果不与任一驱动程序相关联,就将再次被探测。对于每个新注册的驱动程序,比较与之关联的of_node的compatible属性和与of_match_table相关联的每个of_device_id的compatible属性是否兼容。

步骤 1:内核 OF 核心解析设备树,识别设备所属总线

系统启动后,内核的 OF(Open Firmware)核心模块先解析设备树文件(.dtb);

遍历设备树中的每个节点:

先识别「总线节点」(比如i2c@021a0000、spi@021b0000);

再识别总线节点下的「设备节点」(比如 i2c 节点下的tmp102@48、spi 节点下的mcp23s08@0);

明确每个设备节点属于哪条总线(比如 tmp102 属于 I2C 总线,mcp23s08 属于 SPI 总线)。

步骤 2:为设备节点创建并初始化平台设备

内核调用of_device_alloc()函数,为每个设备节点创建struct platform_device(平台设备结构体);

关键操作:把设备树节点的指针(of_node)赋值给平台设备的dev.of_node字段 ------ 相当于给《商铺备案证》贴上联机的 "规划图位置标签",让内核能随时查设备的 compatible 属性。

步骤 3:遍历总线上已注册的所有驱动

内核调用bus_for_each_drv()函数,遍历当前设备所属总线(比如 I2C)上已经注册的所有驱动;

比如:I2C 总线上已加载了 tmp102 驱动、ads1115 驱动,内核就逐个拿这些驱动和新创建的 tmp102 设备匹配。

步骤 4:调用总线匹配函数,触发 OF 匹配逻辑

内核调用该总线的匹配回调函数(比如 platform 总线的platform_match、I2C 总线的i2c_device_match);

如果该总线支持设备树匹配(几乎所有嵌入式总线都支持),匹配函数会调用of_driver_match_device()(就是你之前在platform_match里看到的那个函数);

入参是 "当前驱动" 和 "当前设备"------ 相当于物业拿着 "商户意向清单" 和 "商铺备案证" 去核对。

步骤 5:比对 compatible 属性,匹配成功则调用 probe

of_driver_match_device()会遍历驱动的of_match_table数组(每个元素是of_device_id结构体);

对数组里的每个of_device_id,内核会做两件事:

读取设备节点的compatible属性(比如microchip,mcp23s08);

比对该属性和of_device_id里的compatible字符串是否完全一致;

只要有一个元素匹配成功:

总线匹配函数返回 1(匹配成功);

内核立即调用该驱动的 probe 函数(商户 "装修开业",初始化设备);

若所有元素都不匹配:进入下一步 "延迟匹配"。

步骤 6:匹配失败→设备注册到总线,等待后续匹配

即使没找到匹配的驱动,内核也会把这个平台设备 "注册到总线上"(相当于物业把商铺备案证存档,保留铺位);

设备处于 "已注册但未绑定驱动" 的状态,probe 函数暂时不执行。

第二部分:后续的延迟匹配流程(补充逻辑)

"驱动后加载时的匹配",对应之前的 "insmod / 热插拔" 场景:

当新的驱动被加载(比如 insmod mcp23s08.ko),内核会触发 "反向匹配":

遍历该总线已注册但未绑定的所有设备;

对每个设备,再次调用of_driver_match_device(),比对驱动的of_match_table和设备的compatible属性;

匹配成功则立即调用 probe 函数;

这个机制保证:即使设备启动时没找到驱动,后续加载驱动也能让设备 "生效"(比如热插拔设备、手动 insmod 驱动)。

相关推荐
承渊政道13 分钟前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
D e re k ZH19 分钟前
C++ 标准库算法全景指南
数据结构·c++·算法
冻柠檬飞冰走茶1 小时前
字母移动游戏题解:贪心求最长可保留连续子串(附 C 实现)
c语言·开发语言·数据结构·算法·动态规划
讯捷蓝达2 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
且从容.3 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落5 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨5 小时前
WSL2的安装与使用
linux·wsl
微小的鱼5 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng5 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮6 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频