在上一篇关于 sysfs 的总体层次拆解中,我们提到 sysfs 完美实现了物理拓扑(devices)与功能分类(class)的分离。在 Linux 内核设备模型中,有一句经典的名言:“设备(Device)找驱动(Driver),驱动找设备,总线牵线搭桥,这里的搭线桥就是/sys/bus/。
本文将从宏观目录结构、微观 C 数据结构以及底层匹配(Match & Probe)机制三个维度,深度拆解 /sys/bus/ 的设计。
在终端中执行 ls /sys/bus/,你会看到系统中当前注册的所有总线类型:
/sys/bus/
├── pci/ # 高速 PCI/PCIe 总线
├── usb/ # USB 总线
├── i2c/ # I2C 串行总线
├── spi/ # SPI 总线
├── platform/ # 虚拟平台总线(板载 SoC 外设的基石)
└── cpu/ # CPU 总线
以最典型的物理总线(如 i2c)或虚拟平台总线(platform)为例,进入其内部目录,会发现每一个总线目录下都具备高度统一的骨架结构.
/sys/bus/platform/
├── devices/ # 软链接:挂载在该总线上的所有设备
├── drivers/ # 子目录:在该总线上注册的所有驱动程序
├── drivers_probe # 接口:触发手动 Probe 的属性文件
├── drivers_autoprobe # 接口:控制是否开启自动匹配的开关 (1/0)
└── uevent # 接口:总线级别的热插拔事件广播
1. devices/ 目录
该目录下包含了所有绑定在当前总线上的设备。这里的每一个条目都不是实体文件,而是指向 /sys/devices/ 中真实物理节点(kobject)的符号链接(Symbolic Link)。
2. drivers/ 目录
该目录下包含了所有注册到该总线上的驱动程序。每个驱动目录下通常会包含以下关键节点:
在内核 C 语言层面,每一个 /sys/bus/ 下的子目录,底层都对应一个 struct bus_type 结构体(定义于 <linux/device.h>)。
structbus_type {
constchar *name; /* 总线名称,决定 /sys/bus/ 下的目录名 */
constchar *dev_name;
structdevice *dev_root;
/* 属性文件管理 */
conststructattribute_group **bus_groups;
conststructattribute_group **dev_groups;
conststructattribute_group **drv_groups;
/* 核心回调函数:匹配与初始化 */
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
/* 底层私有数据容器 (维护链表与 kset) */
structsubsys_private *p;
};
1.match() 回调函数(核心)
2.subsys_private指针P
subsys_private 内部包含了两个重要的 kset:

总线子系统最精彩的设计,在于其异步与双向的动态匹配机制。
无论系统中是先有硬件设备(如内核启动解析设备树 DTS),还是先有驱动程序(如 insmod my_driver.ko),总线都能保证两者正确结合。
当调用 device_register(dev) 时:
当调用 driver_register(drv) 或 module_platform_driver() 时:

了解了 /sys/bus/ 的机制后,在实际调试或热替换驱动时,可以利用 sysfs 暴露的 bind / unbind 接口,在不卸载驱动模块(不执行 rmmod)且重启系统的前提下,动态更换设备的驱动:
1.手动解绑驱动(Unbind)
假设想让 i2c-0 总线上的 0-0048 设备脱离当前驱动:
echo "0-0048" > /sys/bus/i2c/drivers/tmp102/unbind
执行后,内核会调用驱动的 remove() 函数释放资源,并在 /sys/bus/i2c/drivers/tmp102/ 目录下移除指向该设备的软链接。
2. 手动重新绑定驱动(Bind)
将设备重新绑定给指定驱动:
echo "0-0048" > /sys/bus/i2c/drivers/tmp102/bind
内核会重新调用 tmp102 驱动的 probe() 函数进行初始化。
下面提供一个完整的内核模块 C 语言代码示例,演示如何从零手写注册一个自定义总线,并在该总线上注册自定义设备与驱动,随后详细分析内核内部的注册与匹配流程。
#include<linux/module.h>
#include<linux/init.h>
#include<linux/device.h>
#include<linux/string.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Developer");
MODULE_DESCRIPTION("Custom Bus, Device, and Driver Demo");
/* 1. 总线 Match 回调函数:比较设备名与驱动名是否一致 */
staticintmy_bus_match(struct device *dev, struct device_driver *drv)
{
pr_info("my_bus: Matching dev '%s' with drv '%s'\n", dev_name(dev), drv->name);
/* 简单的字符串匹配规则:名字相同即匹配成功 */
return (strcmp(dev_name(dev), drv->name) == 0);
}
/* 2. 定义并注册自定义总线 my_bus */
staticstructbus_typemy_bus_type = {
.name = "my_bus",
.match = my_bus_match,
};
/* 3. 驱动 Probe & Remove 回调函数 */
staticintmy_drv_probe(struct device *dev)
{
pr_info("my_bus: Driver probe called for device '%s'!\n", dev_name(dev));
return0;
}
staticintmy_drv_remove(struct device *dev)
{
pr_info("my_bus: Driver remove called for device '%s'!\n", dev_name(dev));
return0;
}
/* 4. 定义自定义设备 (device) 与驱动 (driver) */
staticstructdevicemy_dev;
staticstructdevice_drivermy_drv = {
.name = "my_dev_001", /* 驱动名称,须与设备名称一致以完成匹配 */
.bus = &my_bus_type, /* 归属于 my_bus 总线 */
.probe = my_drv_probe,
.remove = my_drv_remove,
};
/* 5. 设备 release 回调 (必填,防止内核警告) */
staticvoidmy_dev_release(struct device *dev)
{
pr_info("my_bus: Device '%s' released\n", dev_name(dev));
}
/* 6. 模块初始化 */
staticint __init my_bus_demo_init(void)
{
int ret;
pr_info("my_bus: Initializing custom bus demo...\n");
// 第一步:注册总线 -> 生成 /sys/bus/my_bus/
ret = bus_register(&my_bus_type);
if (ret) {
pr_err("my_bus: Failed to register bus\n");
return ret;
}
// 第二步:初始化并注册设备 -> 生成 /sys/bus/my_bus/devices/my_dev_001
dev_set_name(&my_dev, "my_dev_001");
my_dev.bus = &my_bus_type; /* 关联到 my_bus */
my_dev.release = my_dev_release;
ret = device_register(&my_dev);
if (ret) {
pr_err("my_bus: Failed to register device\n");
goto err_unregister_bus;
}
// 第三步:注册驱动 -> 生成 /sys/bus/my_bus/drivers/my_dev_001
ret = driver_register(&my_drv);
if (ret) {
pr_err("my_bus: Failed to register driver\n");
goto err_unregister_dev;
}
pr_info("my_bus: Demo module loaded successfully\n");
return0;
err_unregister_dev:
device_unregister(&my_dev);
err_unregister_bus:
bus_unregister(&my_bus_type);
return ret;
}
/* 7. 模块注销 */
staticvoid __exit my_bus_demo_exit(void)
{
pr_info("my_bus: Cleaning up custom bus demo...\n");
driver_unregister(&my_drv);
device_unregister(&my_dev);
bus_unregister(&my_bus_type);
}
module_init(my_bus_demo_init);
module_exit(my_bus_demo_exit);
加载模块后执行 dmesg | tail -n 10,内核日志输出如下:
my_bus: Initializing custom bus demo...
my_bus: Matching dev 'my_dev_001' with drv 'my_dev_001'
my_bus: Driver probe called for device 'my_dev_001'!
my_bus: Demo module loaded successfully
同时系统 sysfs 树状结构中生成对应节点:
/sys/bus/my_bus/
├── devices
│ └── my_dev_001 -> ../../../devices/my_dev_001
└── drivers
└── my_dev_001
bus_register(&my_bus_type) 时:
调用device_register(&my_dev)
driver_register(&my_drv)
设备与驱动是如何“对应(Match & Probe)”上的?
核心机制:双向异步触发表(Two-way Triggering)。不管是先注册设备,还是先注册驱动,总线都能确保两者绑定。
【情况 A: 先注册 Device,后注册 Driver】
注册 Device ────────> 加入 devices 链表 ────────> 尝试匹配失败 (drivers 链表为空)
│
注册 Driver ────────> 加入 drivers 链表 ────────> 遍历 devices 链表 ──> 匹配成功! ──> 执行 Probe()
【情况 B: 先注册 Driver,后注册 Device】
注册 Driver ────────> 加入 drivers 链表 ────────> 尝试匹配失败 (devices 链表为空)
│
注册 Device ────────> 加入 devices 链表 ────────> 遍历 drivers 链表 ──> 匹配成功! ──> 执行 Probe()
当任意一方(例如调用 driver_register)触发匹配时,内核执行流程如下:
1.bus_add_driver(drv)
2.driver_attach(drv)
调用 bus_for_each_dev(drv->bus, NULL, drv, __driver_attach)。
内核会遍历 my_bus 设备链表中的每一个 struct device,并对每个设备调用 __driver_attach。
3.__driver_attach(dev, drv)
在此调用总线提供的匹配回调函数:
if (!driver_match_device(drv, dev))
return0; /* 匹配失败,继续遍历下一个设备 */
4.执行匹配函数 (my_bus_match)
如果 my_bus_match 返回 0(不匹配):驱动继续查找下一个设备。
如果 my_bus_match 返回 1(匹配成功):进入 driver_probe_device(drv, dev)。
5.执行 Probe 函数
内核在完成设备与驱动绑定(建立 sysfs 软链接)后,会优先调用 bus->probe(如果定义了的话),否则直接调用驱动定义的 drv->probe(dev)(即 my_drv_probe)。
至此,设备与驱动完成绑定,设备正式进入就绪状态。
在 Linux 内核设备模型中,PCI、USB、I2C、SPI 等物理总线都有明确的物理接口与总线协议规范。然而在嵌入式 SoC 芯片内部,集成了大量直接通过 CPU 内存映射总线(MMIO)寻址的外设控制器(如 GPIO、UART、PWM、定时器、显示控制器等)。
这些外设既没有 PCI 系统的配置空间,也没有 USB 的端点描述符,更无法在运行时被内核自动动态枚举。
为了将这些“无物理总线”的内存映射设备(Memory-Mapped Devices)统一纳入统一设备模型(Unified Device Model)管理,Linux 内核设计者创造性地引入了一种虚拟总线——平台总线(Platform Bus)。
在平台总线出现之前(Linux 2.6 之前),嵌入式驱动开发面临着严重的架构痛点:
Platform Bus 的核心思想在于:凭空创造一条“虚拟总线”,把真实的 CPU 内存总线抽象为 platform_bus_type

设备抽象:
structplatform_device {
constchar *name; /* 设备名称,用于与驱动进行字符串匹配 */
int id; /* 设备 ID,用于区分同名设备(如 uart0, uart1);若只有一个则填 -1 */
structdevicedev;/* 嵌入的通用 device 结构体 */
u32 num_resources;/* 硬件资源数量 */
structresource *resource;/* 硬件资源数组 (寄存器地址, IRQ, DMA 等) */
conststructplatform_device_id *id_entry;/* 匹配到的 ID 表项 */
structmfd_cell *mfd_cell;/* MFD 多功能设备支持 */
};
其中最关键的字段是 resource 数组。内核定义了标准的资源类型:
驱动抽象:struct platform_driverplatform_driver 描述平台驱动程序,封装了具体的操作接口:
structplatform_driver {
int (*probe)(struct platform_device *); /* 设备匹配成功后的初始化回调 */
int (*remove)(struct platform_device *); /* 设备卸载或驱动注销时的清理回调 */
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
structdevice_driverdriver;/* 嵌入的通用 device_driver 结构体 */
conststructplatform_device_id *id_table;/* 传统 ID 匹配表 */
bool prevent_deferred_probe;
};
当有新的 platform_device 或 platform_driver 被注册到内核时,平台总线会触发匹配函数 platform_match()(位于 drivers/base/platform.c)。
Platform 总线采用了优先级降级匹配的策略,按顺序尝试以下 4 种匹配方式:
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);
/* 1. 尝试 ACPI 方式匹配 */
if (acpi_driver_match_device(dev, drv))
return 1;
/* 2. 尝试 设备树 (Device Tree) OF 匹配 (最高优先级) */
if (of_driver_match_device(dev, drv))
return 1;
/* 3. 尝试 platform_device_id 表匹配 */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* 4. 降级策略:基于设备名与驱动名的纯字符串匹配 */
return (strcmp(pdev->name, drv->name) == 0);
}
匹配链条详细拆解:
ACPI匹配:主要用于 x86 架构下的高级电源与硬件配置接口匹配。
设备树(OF)匹配:现代 ARM / ARM64 / RISC-V 架构的标准方式。匹配设备树节点中的 compatible 属性与驱动中 of_match_table 的 compatible 字符串。
ID 表匹配:驱动定义 struct platform_device_id 数组,允许一个驱动支持多个不同名称的同类设备。
字符串名称匹配:最原始的匹配方式,比较 pdev->name 与 pdrv->driver.name 是否完全一致。
在 Linux 2.6 及早期的 ARM 内核(Linux 3.x 以前)中,所有平台设备都在 arch/arm/mach-xxx/board-yyy.c 中硬编码声明:
/* 传统的板级代码 (已淘汰) */
static struct resource my_uart_resources[] = {
[0] = {
.start = 0x4000c000,
.end = 0x4000c0ff,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 54,
.end = 54,
.flags = IORESOURCE_IRQ,
},
};
static struct platform_device my_uart_device = {
.name = "my_uart",
.id = 0,
.num_resources = ARRAY_SIZE(my_uart_resources),
.resource = my_uart_resources,
};
/* 系统启动阶段手动注册 */
platform_device_register(&my_uart_device);
问题:这导致内核中充斥着巨大的垃圾代码(著名的 "ARM Linux churn"),硬件微调必须重新编译内核。在引入 Device Tree 之后,板级 C 文件被彻底清理。硬件描述移入 .dts 文件:
/* 设备树定义 */
usart1: serial@4000c000 {
compatible = "my,uart-driver";
reg = <0x4000c0000x100>;
interrupts = <GIC_SPI 54 IRQ_TYPE_LEVEL_HIGH>;
status = "okay";
};
Unflatten DTS:Bootloader 将二进制设备树(.dtb)传给内核,内核启动时将其解包为 device_node 树节点。
of_platform_populate():内核系统初始化阶段,自动遍历设备树节点。
自动生成 platform_device:只要 DTS 节点包含 compatible 属性(且不处于特定的内部总线子节点下),内核会自动为其分配 struct platform_device,提取 reg 为 IORESOURCE_MEM,提取 interrupts 为 IORESOURCE_IRQ,并自动注册到平台总线上。
在用户态,我们可以通过 sysfs 节点直观地观察平台总线的运行状态:
/sys/
├── bus/
│ └── platform/
│ ├── devices/
│ │ ├── 4000c000.serial -> ../../../devices/platform/soc/4000c000.serial
│ │ └── ...
│ └── drivers/
│ ├── my_uart_driver/
│ │ ├── bind
│ │ ├── unbind
│ │ └── 4000c000.serial -> ../../../../devices/platform/soc/4000c000.serial
│ └── ...
└── devices/
└── platform/ # 平台物理拓扑根节点
└── soc/ # 片上系统总线节点
└── 4000c000.serial
/sys/devices/platform/:存放所有平台设备的真实物理/逻辑拓扑节点。
/sys/bus/platform/devices/:包含指向上述真实节点的软链接。
/sys/bus/platform/drivers/:包含所有注册的平台驱动。
首先,在板级设备树文件(如 arch/arm/boot/dts/xxx.dts)中定义一个虚拟的硬件设备节点。
其中 compatible 属性是驱动与设备完成匹配的“暗号”。
/ {
soc {
#address-cells = <1>;
#size-cells = <1>;
/* 定义我们的自定义平台设备 */
my_device_node: my_device@4000c000 {
compatible = "mycompany,my-custom-device";
reg = <0x4000c0000x100>; /* 寄存器物理基地址与长度 */
interrupts = <0484>; /* 中断配置 (GIC SPI 中断号 48) */
status = "okay";
};
};
};
#include<linux/module.h>
#include<linux/init.h>
#include<linux/platform_device.h>
#include<linux/of.h>
#include<linux/io.h>
#include<linux/interrupt.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Developer");
MODULE_DESCRIPTION("Platform Driver with explicit module_init and module_exit");
/* 1. 中断处理函数 */
staticirqreturn_tmy_device_irq_handler(int irq, void *dev_id)
{
pr_info("my_platform: Interrupt occurred! IRQ line: %d\n", irq);
// TODO: 处理硬件中断业务逻辑
return IRQ_HANDLED;
}
/* 2. Probe 函数:设备与驱动匹配成功后由总线触发执行 */
staticintmy_driver_probe(struct platform_device *pdev)
{
structresource *mem_res;
int irq;
void __iomem *base_addr;
int ret;
pr_info("my_platform: Match success! Probe function called for %s\n", dev_name(&pdev->dev));
/* A. 从设备树中提取 MEM 寄存器资源 */
mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!mem_res) {
dev_err(&pdev->dev, "Failed to get memory resource\n");
return -EINVAL;
}
/* B. 内存映射:将物理地址转换为内核虚拟地址(使用 devm_ 托管机制自动释放) */
base_addr = devm_ioremap_resource(&pdev->dev, mem_res);
if (IS_ERR(base_addr))
return PTR_ERR(base_addr);
pr_info("my_platform: Register mapped at virtual address: %p\n", base_addr);
/* C. 从设备树中提取 IRQ 中断号 */
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "Failed to get IRQ number\n");
return irq;
}
/* D. 注册中断处理程序(使用 devm_ 托管机制自动释放) */
ret = devm_request_irq(&pdev->dev, irq, my_device_irq_handler,
IRQF_TRIGGER_HIGH, "my_custom_dev", NULL);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ %d\n", irq);
return ret;
}
pr_info("my_platform: Driver initialized successfully, IRQ: %d\n", irq);
return0;
}
/* 3. Remove 函数:设备移除或驱动注销时由总线触发执行 */
staticintmy_driver_remove(struct platform_device *pdev)
{
pr_info("my_platform: Driver remove called for %s\n", dev_name(&pdev->dev));
return0;
}
/* 4. 设备树匹配表 */
staticconststructof_device_idmy_of_match[] = {
{ .compatible = "mycompany,my-custom-device" },
{ /* Sentinel 结束标记 */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
/* 5. 定义 Platform 驱动结构体 */
staticstructplatform_drivermy_platform_driver = {
.probe = my_driver_probe,
.remove = my_driver_remove,
.driver = {
.name = "my_custom_driver",
.of_match_table = my_of_match,
},
};
/* 6. 显式展开:模块入口函数 */
staticint __init my_driver_init(void)
{
pr_info("my_platform: Registering platform driver...\n");
/* 可以在此处添加模块加载时的其他自定义初始化逻辑 */
return platform_driver_register(&my_platform_driver);
}
/* 7. 显式展开:模块出口函数 */
staticvoid __exit my_driver_exit(void)
{
pr_info("my_platform: Unregistering platform driver...\n");
/* 可以在此处添加模块卸载时的自定义资源清理逻辑 */
platform_driver_unregister(&my_platform_driver);
}
/* 8. 指定内核模块入口与出口 */
module_init(my_driver_init);
module_exit(my_driver_exit);
设备树加载:系统启动时,Bootloader 将 .dtb 传入内核,内核解析设备树,并在 /sys/devices/platform/ 下自动生成名为 4000c000.my_device@4000c000 的虚拟 platform_device。
加载驱动模块:执行 insmod my_platform_drv.ko 注册 platform_driver。
总线匹配(Match):平台总线发现驱动中的 of_match_table 包含了 "mycompany,my-custom-device",与设备树节点的 compatible 属性匹配成功。
执行探测(Probe):内核调用 my_driver_probe(),利用 platform_get_resource 安全提取出寄存器基址与中断号,完成硬件初始化。
