4. 驱动模型基础:驱动对象、设备对象、总线对象的概念与关系、驱动注册与注销流程
好,咱们今天聊聊驱动模型里最核心的三个概念:驱动对象、设备对象、总线对象。说实话,我刚入行那会儿,总觉得这三个东西就是数据结构里多出来的几个结构体,没啥大不了的。直到有一次我在一个嵌入式项目里调试USB设备枚举,死活挂不上驱动,折腾了两天才发现是总线对象里的匹配逻辑写错了。嗯,从那以后我再也不敢小看它们了。
4.1 三个核心对象:谁是谁?
先说说这三个对象分别是什么。你想想看,一个完整的驱动模型,说白了就是要把「硬件设备」和「软件驱动」给撮合到一起。谁来撮合?总线对象来干这个活。
- 驱动对象(Driver Object):代表一个驱动程序。它里面装着驱动的入口函数、分发函数、以及它支持哪些设备的信息。我个人习惯把它理解成「一个能干活的人」,他有一身本事,但还没找到活干。
- 设备对象(Device Object):代表一个物理或逻辑设备。它描述了设备的硬件资源(中断号、I/O地址、DMA通道等),以及设备当前的状态。说白了,它就是「一个需要被干的活」。
- 总线对象(Bus Object):代表一种总线(比如PCI、I2C、SPI)。它的职责是扫描总线上的设备、管理设备枚举、以及负责驱动和设备的匹配。你可以把它理解成「中介」——它知道哪些活(设备)需要人干,也知道哪些人(驱动)能干活。
核心关系一句话:总线对象负责把驱动对象和设备对象撮合到一起。驱动对象通过总线对象找到它支持的设备,设备对象通过总线对象找到能驱动它的驱动。
4.2 三者的关系:一张图说清楚
我习惯用一张图来理解它们的关系。下面这个SVG图,是我在给团队做培训时常用的,你一看就明白。
你看,总线对象在顶层,它向下连接驱动对象和设备对象。驱动对象说「我能干这些活」,设备对象说「我需要这些活」,总线对象负责对一下——匹配上了,就调用驱动的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驱动例子来拆解。