4. 驱动模型基础:驱动对象、设备对象、总线对象的概念与关系、驱动注册与注销流程

好,咱们今天聊聊驱动模型里最核心的三个概念:驱动对象、设备对象、总线对象。说实话,我刚入行那会儿,总觉得这三个东西就是数据结构里多出来的几个结构体,没啥大不了的。直到有一次我在一个嵌入式项目里调试USB设备枚举,死活挂不上驱动,折腾了两天才发现是总线对象里的匹配逻辑写错了。嗯,从那以后我再也不敢小看它们了。

4.1 三个核心对象:谁是谁?

先说说这三个对象分别是什么。你想想看,一个完整的驱动模型,说白了就是要把「硬件设备」和「软件驱动」给撮合到一起。谁来撮合?总线对象来干这个活。

  • 驱动对象(Driver Object):代表一个驱动程序。它里面装着驱动的入口函数、分发函数、以及它支持哪些设备的信息。我个人习惯把它理解成「一个能干活的人」,他有一身本事,但还没找到活干。
  • 设备对象(Device Object):代表一个物理或逻辑设备。它描述了设备的硬件资源(中断号、I/O地址、DMA通道等),以及设备当前的状态。说白了,它就是「一个需要被干的活」。
  • 总线对象(Bus Object):代表一种总线(比如PCI、I2C、SPI)。它的职责是扫描总线上的设备、管理设备枚举、以及负责驱动和设备的匹配。你可以把它理解成「中介」——它知道哪些活(设备)需要人干,也知道哪些人(驱动)能干活。

核心关系一句话:总线对象负责把驱动对象和设备对象撮合到一起。驱动对象通过总线对象找到它支持的设备,设备对象通过总线对象找到能驱动它的驱动。

4.2 三者的关系:一张图说清楚

我习惯用一张图来理解它们的关系。下面这个SVG图,是我在给团队做培训时常用的,你一看就明白。

总线对象 (中介:负责匹配) 驱动对象 入口函数、分发函数 支持的设备列表 设备对象 硬件资源描述 设备状态 注册 枚举 匹配(probe)

你看,总线对象在顶层,它向下连接驱动对象和设备对象。驱动对象说「我能干这些活」,设备对象说「我需要这些活」,总线对象负责对一下——匹配上了,就调用驱动的probe函数,驱动开始干活。

4.3 驱动注册流程:把「人」登记到中介那里

驱动注册,说白了就是让总线知道「我来了,我能干这些活」。我见过不少新手直接写驱动,忘了注册这一步,结果内核根本不知道有这个驱动存在。

以Linux内核为例,驱动注册的核心流程是这样的:

// 驱动对象结构体定义(简化版)
struct my_driver {
    struct device_driver driver;  // 内核通用驱动结构
    int (*probe)(struct device *dev);
    int (*remove)(struct device *dev);
};

// 驱动注册函数
static int __init my_driver_init(void)
{
    int ret;
    struct my_driver *drv;

    // 1. 分配驱动对象
    drv = kzalloc(sizeof(*drv), GFP_KERNEL);
    if (!drv)
        return -ENOMEM;

    // 2. 初始化驱动对象
    drv->driver.name = "my_device_driver";
    drv->driver.bus = &my_bus_type;  // 绑定到哪个总线
    drv->probe = my_probe;
    drv->remove = my_remove;

    // 3. 注册到总线
    ret = driver_register(&drv->driver);
    if (ret) {
        pr_err("Failed to register driver\n");
        kfree(drv);
        return ret;
    }

    pr_info("Driver registered successfully\n");
    return 0;
}
module_init(my_driver_init);

个人经验:我在一个项目里遇到过驱动注册成功但probe没被调用的情况。查了半天,发现是总线类型绑错了——驱动注册到了platform总线,但设备挂在I2C总线上。总线不对,中介根本不帮你匹配。所以注册前一定确认好总线类型。

4.4 驱动注销流程:干完活要「退房」

驱动注销和注册是对称的。你注册了,就得注销,否则内核里会留下「僵尸驱动」。我曾经在调试一个热插拔设备时,忘了写注销函数,结果设备拔掉后驱动还在,下次插入时直接内核panic。

static void __exit my_driver_exit(void)
{
    // 1. 从总线注销驱动
    driver_unregister(&my_drv->driver);

    // 2. 释放驱动对象内存
    kfree(my_drv);

    pr_info("Driver unregistered\n");
}
module_exit(my_driver_exit);

注销时,总线对象会遍历所有已匹配的设备,对每个设备调用驱动的remove函数,让驱动「优雅地」退出。如果remove函数没写好,设备可能处于半死不活的状态。

注意:驱动注销的顺序很重要。一定要先注销驱动(driver_unregister),再释放资源。我曾经反过来做,先释放了驱动对象的内存,然后注销时总线还在访问它——直接内核Oops。嗯,从那以后我写注销函数都格外小心。

4.5 设备对象与总线对象的交互

设备对象不是凭空产生的。总线对象会扫描硬件,发现设备后创建设备对象。比如PCI总线枚举时,会读取每个PCI设备的Vendor ID和Device ID,然后创建一个pci_dev结构体作为设备对象。

设备对象创建后,总线对象会把它加入自己的设备链表,然后触发匹配流程。匹配的核心逻辑是这样的:

// 总线匹配函数(伪代码)
int bus_match(struct device *dev, struct device_driver *drv)
{
    // 1. 检查驱动是否支持该设备
    if (drv->id_table) {
        // 遍历驱动的ID表,匹配设备的Vendor/Device ID
        for (i = 0; drv->id_table[i].vendor != 0; i++) {
            if (drv->id_table[i].vendor == dev->vendor &&
                drv->id_table[i].device == dev->device) {
                return 1;  // 匹配成功
            }
        }
    }

    // 2. 或者检查设备名是否匹配驱动名
    if (strcmp(dev->name, drv->name) == 0)
        return 1;

    return 0;  // 匹配失败
}

匹配成功后,总线对象调用驱动的probe函数,把设备对象传进去。驱动在probe里做硬件初始化、注册中断、创建设备文件等操作。

4.6 一张表总结三者的职责

对象类型 核心职责 关键字段/函数 生命周期
驱动对象 描述驱动能力,提供probe/remove回调 name, bus, probe(), remove(), id_table 模块加载到卸载
设备对象 描述硬件资源,提供设备状态 vendor, device, irq, iomem, name 设备枚举到移除
总线对象 管理设备枚举,驱动匹配,生命周期协调 match(), probe(), devices链表, drivers链表 系统运行期间常驻

4.7 避坑指南:我踩过的几个坑

  • 坑一:驱动注册后probe没被调用 —— 检查总线类型是否匹配,检查id_table是否写对了。我曾经把PCI设备的Vendor ID写反了(高低字节颠倒),折腾了一下午。
  • 坑二:注销时内核崩溃 —— 多半是资源释放顺序错了。先注销驱动,再释放驱动对象。如果驱动里有定时器或工作队列,记得先取消。
  • 坑三:设备对象重复创建 —— 总线扫描时如果没做好去重,同一个设备可能被创建多个设备对象。我见过一个项目里,同一个USB设备被枚举了三次,驱动被probe了三次,每次都在申请同一块I/O内存,第二次就失败了。
  • 坑四:忘记调用driver_unregister —— 模块卸载时如果不注销,驱动对象会变成野指针。下次模块加载时,内核发现同名驱动已存在,直接拒绝注册。

好了,这一章的内容就这些。驱动对象、设备对象、总线对象这三个概念,你只要记住一句话:总线是中介,驱动是工人,设备是活。理解了这句话,后面的驱动开发就会顺很多。下一章咱们聊聊具体的设备驱动编写流程,到时候我会拿一个真实的I2C驱动例子来拆解。