4. 驱动分离核心机制

说实话,很多做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 暴露。

我的经验: 写驱动时,你很少直接操作 kobject,但一定要理解它的存在。比如你在 sysfs 里看到 /sys/bus/i2c/devices/0-0040,这个路径的每一级目录背后,都是一个 kobject。

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 收到后,就可以动态加载驱动、创建设备节点。

核心流程:
  1. 设备插入 → 内核创建 kobject
  2. kset 调用 uevent_ops 生成事件字符串
  3. 事件通过 netlink 或 /sys/kernel/uevent_helper 发送到用户空间
  4. 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");
运行效果:
  1. 先加载 vbus.ko → 注册 vbus 总线
  2. 再加载 vbus_device.ko → 注册设备,触发 vbus_match,但此时无驱动,匹配失败
  3. 加载 vbus_driver.ko → 注册驱动,再次触发 vbus_match,名称匹配成功 → 调用 probe
  4. 卸载驱动 → 调用 remove

你会在 dmesg 中看到:vbus: driver matched with device vbus_test_dev

4.4 知识结构图

下面这张图,把总线、设备、驱动三者的关系,以及 kobject/kset/uevent 的协作流程画了出来。我建议你多看几遍,把这张图印在脑子里。

Linux 总线-设备-驱动模型结构图 bus_type (vbus) match() device device_driver match 成功 → probe 每个 device / driver / bus 内部都有一个 kobject kset 组织同类 kobject,管理 uevent 生成 uevent → 用户空间 udev 设备插入/拔出、驱动加载/卸载 → 触发 uevent → 用户空间动态响应
我的建议: 你可以在自己的开发板上编译加载这个虚拟总线驱动,然后观察 /sys/bus/vbus/ 目录下的变化。亲眼看到设备和驱动在总线上“相遇”,比看一百遍代码都管用。

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 总线、设备树、电源管理等,就会轻松很多。记住那张结构图,它就是你的“武功秘籍”。