4、总线-设备-驱动模型:Linux设备模型基石
说实话,很多做Linux驱动开发的朋友,一开始都会被“总线-设备-驱动”这三个词绕晕。我当年刚入行时也一样,总觉得这玩意儿太抽象,不如直接写个字符设备驱动来得痛快。直到有一次,我在一个嵌入式项目里需要同时管理十几个外设,每个外设的驱动都得手动注册、手动匹配,代码写得跟意大利面条似的——那才叫一个痛苦。
后来我静下心来,把Linux设备模型捋了一遍,才发现这玩意儿其实没那么玄乎。说白了,它就是一套“解耦”的机制。你想想看,如果没有这套模型,驱动和设备就得硬绑在一起,换一个硬件就得改驱动代码,那还怎么玩?
4.1 设备模型的核心:kobject / kset / uevent
Linux设备模型的底层,是三个基础构件:kobject、kset 和 uevent。它们就像乐高积木的最小颗粒,所有设备、驱动、总线,最终都是基于它们搭起来的。
4.1.1 kobject:一切对象的基类
kobject 是 Linux 内核中“对象化”的最小单元。它不直接代表设备或驱动,而是提供引用计数、父子关系和 sysfs 表示的能力。
// include/linux/kobject.h
struct kobject {
const char *name; // 对象名称,会出现在 sysfs 中
struct list_head entry; // 挂入 kset 的链表节点
struct kobject *parent; // 父对象,形成层次结构
struct kset *kset; // 所属的 kset 集合
struct kobj_type *ktype; // 对象类型,包含 release 方法
struct kernfs_node *sd; // sysfs 目录项
struct kref kref; // 引用计数
};
我个人习惯把 kobject 理解成“内核里的身份证”。每个设备、每个驱动、每个总线,都有一个对应的 kobject。通过它,内核可以统一管理这些对象的生命周期和 sysfs 暴露。
4.1.2 kset:对象的集合
kset 是 kobject 的集合。它把同类型的 kobject 组织在一起,形成一个“族群”。比如,系统中所有的块设备、所有的网络设备,各自属于一个 kset。
struct kset {
struct list_head list; // 挂载所有 kobject 的链表头
struct kobject kobj; // kset 自身的 kobject
const struct kset_uevent_ops *uevent_ops; // uevent 操作函数
};
kset 还有一个重要作用:当集合中的 kobject 状态发生变化时(比如被创建或删除),kset 会负责生成 uevent 通知用户空间。
4.1.3 uevent:内核到用户空间的消息
uevent 是内核向用户空间发送的“事件通知”。当设备被插入、拔出、驱动被绑定或解绑时,内核会通过 uevent 把消息发出去。用户空间的 udev 或 mdev 收到后,就可以动态加载驱动、创建设备节点。
- 设备插入 → 内核创建 kobject
- kset 调用 uevent_ops 生成事件字符串
- 事件通过 netlink 或 /sys/kernel/uevent_helper 发送到用户空间
- udev 解析事件,执行匹配规则
我曾经在一个项目中,发现设备插上后 /dev 下没有节点。排查了半天,最后发现是 uevent_helper 被清空了。嗯,从那以后我每次调试设备模型问题,都会先检查 uevent 通路是否正常。
4.2 总线注册与匹配机制
总线是设备模型的“粘合剂”。它负责把设备和驱动撮合到一起。Linux 内核里有很多现成的总线:platform 总线、I2C 总线、SPI 总线、PCI 总线……但它们的底层逻辑是一样的。
4.2.1 总线结构体
struct bus_type {
const char *name; // 总线名称
struct bus_attribute *bus_attrs; // 总线属性
struct device_attribute *dev_attrs;// 设备属性
struct driver_attribute *drv_attrs;// 驱动属性
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
void (*remove)(struct device *dev);
// ... 其他回调
};
这里最关键的就是 match 函数。每次有新的设备或驱动注册到总线上时,内核都会调用 match 函数,看看它们是不是“一对”。
4.2.2 匹配流程
你想想看,当你在总线上注册一个驱动时,内核会怎么做?它会遍历总线上所有已注册的设备,对每个设备调用 match 函数。反过来,注册设备时也一样,遍历所有驱动。
常见的匹配方式有几种:
| 匹配方式 | 说明 | 典型总线 |
|---|---|---|
| 名称匹配 | 比较设备名和驱动名是否一致 | platform 总线 |
| ID 表匹配 | 驱动提供 ID 表,设备 ID 在表中即匹配 | I2C、SPI、PCI |
| ACPI/OF 匹配 | 通过设备树或 ACPI 表匹配 | platform 总线(现代) |
4.3 实战:编写虚拟总线驱动
光说不练假把式。我们来写一个虚拟总线驱动,把上面这些概念串起来。这个总线不操作任何真实硬件,只用来演示注册、匹配、探测的完整流程。
4.3.1 定义虚拟总线
// vbus.c
#include <linux/device.h>
#include <linux/module.h>
#include <linux/kernel.h>
// 匹配函数:通过名称匹配
static int vbus_match(struct device *dev, struct device_driver *drv)
{
return strcmp(dev_name(dev), drv->name) == 0;
}
// uevent 函数:发送事件到用户空间
static int vbus_uevent(struct device *dev, struct kobj_uevent_env *env)
{
add_uevent_var(env, "VBUS_NAME=%s", dev_name(dev));
return 0;
}
// 定义总线结构体
struct bus_type vbus_type = {
.name = "vbus",
.match = vbus_match,
.uevent = vbus_uevent,
};
EXPORT_SYMBOL_GPL(vbus_type);
4.3.2 注册总线
static int __init vbus_init(void)
{
int ret;
ret = bus_register(&vbus_type);
if (ret)
pr_err("vbus: bus_register failed\n");
return ret;
}
static void __exit vbus_exit(void)
{
bus_unregister(&vbus_type);
}
module_init(vbus_init);
module_exit(vbus_exit);
MODULE_LICENSE("GPL");
4.3.3 编写虚拟设备和驱动
设备和驱动是独立的模块,它们通过 vbus 总线连接。
// vbus_device.c
#include <linux/device.h>
#include <linux/module.h>
extern struct bus_type vbus_type;
void vbus_dev_release(struct device *dev)
{
pr_info("vbus: device %s released\n", dev_name(dev));
}
static struct device vbus_dev = {
.init_name = "vbus_test_dev",
.bus = &vbus_type,
.release = vbus_dev_release,
};
static int __init vbus_dev_init(void)
{
return device_register(&vbus_dev);
}
static void __exit vbus_dev_exit(void)
{
device_unregister(&vbus_dev);
}
module_init(vbus_dev_init);
module_exit(vbus_dev_exit);
MODULE_LICENSE("GPL");
// vbus_driver.c
#include <linux/device.h>
#include <linux/module.h>
extern struct bus_type vbus_type;
static int vbus_drv_probe(struct device *dev)
{
pr_info("vbus: driver matched with device %s\n", dev_name(dev));
return 0;
}
static int vbus_drv_remove(struct device *dev)
{
pr_info("vbus: driver removed from device %s\n", dev_name(dev));
return 0;
}
struct device_driver vbus_drv = {
.name = "vbus_test_dev",
.bus = &vbus_type,
.probe = vbus_drv_probe,
.remove = vbus_drv_remove,
};
static int __init vbus_drv_init(void)
{
return driver_register(&vbus_drv);
}
static void __exit vbus_drv_exit(void)
{
driver_unregister(&vbus_drv);
}
module_init(vbus_drv_init);
module_exit(vbus_drv_exit);
MODULE_LICENSE("GPL");
- 先加载 vbus.ko → 注册 vbus 总线
- 再加载 vbus_device.ko → 注册设备,触发 vbus_match,但此时无驱动,匹配失败
- 加载 vbus_driver.ko → 注册驱动,再次触发 vbus_match,名称匹配成功 → 调用 probe
- 卸载驱动 → 调用 remove
你会在 dmesg 中看到:vbus: driver matched with device vbus_test_dev
4.4 知识结构图
下面这张图,把总线、设备、驱动三者的关系,以及 kobject/kset/uevent 的协作流程画了出来。我建议你多看几遍,把这张图印在脑子里。
4.5 避坑指南
- 我曾经踩过的坑: 写 match 函数时忘了检查 dev_name 是否为空,结果内核直接 panic。从那以后,我每个 match 函数第一行都是
if (!dev || !drv) return 0; - uevent 不触发: 检查 kset 的 uevent_ops 是否赋值,以及 /sys/kernel/uevent_helper 是否指向了正确的 helper 程序。
- probe 不调用: 确认 match 返回了 1(匹配成功),并且设备没有被其他驱动占用。
- sysfs 目录不出现: 检查 kobject 的 name 是否设置,以及 parent 指针是否有效。
好了,这一章的内容就到这里。总线-设备-驱动模型是 Linux 设备管理的核心,理解了它,后面再学 platform 总线、设备树、电源管理等,就会轻松很多。记住那张结构图,它就是你的“武功秘籍”。