阅读本文的前提:你写过一点内核驱动,但对设备模型的整体架构还比较模糊。本文不堆砌术语,目标是让你搞清楚"设备模型这套机制到底是怎么工作的",所有代码均来自 Linux 5.7.8 内核源码。
一、为什么需要设备模型?
先从一个痛点说起。
Linux 早期(2.4 时代),每个子系统各管各的。PCI 驱动自己遍历 PCI 总线找设备,USB 驱动自己遍历 USB 树,platform 设备干脆没有统一管理。后果是:
- 电源管理无法统一:内核不知道系统里有多少设备,怎么统一做 suspend/resume?
- 热插拔散落各地:PCI 有 PCI 的 uevent,USB 有 USB 的 uevent,没有统一抽象
- 设备文件创建靠手写:
mknod 命令 Hardcode,插一个新设备就得改 udev 规则
2.6 内核引入了设备模型,核心目标就是用一套统一的数据结构描述所有硬件,建立一棵完整的设备树。之后电源管理、热插拔、驱动匹配、sysfs 导出全部基于这棵树。
二、设计思想:分层抽象
设备模型的设计哲学可以概括为三个层次:
用户空间 ↓ 通过 sysfs 交互内核抽象层(class / bus / device / driver) ↓底层基础设施(kobject / kset / kref)
越往上越面向用户,越往下越面向内核。这个分层非常重要——你写的驱动只需要接触上层的 device/device_driver/bus_type,不需要直接跟 kobject 打交道。
三、底层基础设施:kobject
3.1 kobject 是什么?
kobject 是设备模型的最底层构件。它本身不复杂,本质上是一个带有引用计数的对象,嵌在各种结构体里使用:
// 路径:D:\work\linux-5.7.8\linux-5.7.8\include\linux\kobject.hstructkobject {constchar *name; // sysfs 目录名structlist_headentry;// 挂到父 kset 的链表节点structkobject *parent;// 父 kobject,决定 sysfs 目录层级structkset *kset;// 所属 ksetstructkobj_type *ktype;// 描述该对象的操作(析构函数等)structkrefkref;// 引用计数unsignedint state_initialized:1; // 是否已初始化unsignedint state_in_sysfs:1; // 是否已在 sysfs 中表现unsignedint state_add_uevent_sent:1;unsignedint state_remove_uevent_sent:1;unsignedint uevent_suppress:1; // 是否压制 uevent};
每个 kobject 在 /sys/ 下对应一个目录。parent 指针决定了这个目录的层级,name 就是目录名。
3.2 引用计数怎么工作的?
// 路径:D:\work\linux-5.7.8\linux-5.7.8\lib\kobject.cstaticvoidkobject_get(struct kobject *kobj){if (kobj) kref_get(&kobj->kref);}staticvoidkobject_put(struct kobject *kobj){if (kobj) kref_put(&kobj->kref, kobject_release);}staticvoidkobject_release(struct kref *kref){structkobject *kobj = container_of(kref, structkobject, kref);conststructkobj_type *t = get_ktype(kobj); pr_debug("kobject: '%s' (refcount: %d): %s\n", kobj->name, kref_read(&kobj->kref), __func__);if (t && t->release) t->release(kobj); // 调用具体的释放函数else pr_debug("kobject: '%s' (end): %s\n", kobj->name, __func__);}
引用计数归零时,自动调用 ktype->release() 来释放内存。这个机制保证了对象在所有引用都释放之前不会被销毁,避免了 use-after-free。
3.3 kset 是什么?
kset 是一组 kobject 的集合,可以理解为一个可以包含子 kobject 的容器:
// 路径:D:\work\linux-5.7.8\linux-5.7.8\include\linux\kobject.hstructkset {structlist_headlist;// 该 kset 下所有 kobject 的链表spinlock_t list_lock;structkobjectkobj;// kset 自身也是一个 kobjectconststructkset_uevent_ops *uevent_ops;};
kobject 通过 entry 挂入 kset->list,通过 parent 建立目录树结构。一个典型的 kset 下的目录结构:
/sys/bus/pci/ ← kset 的 kobject ├── devices/ ← 子 kset └── drivers/ ← 子 kset
四、bus_type:总线抽象
4.1 为什么需要总线抽象?
拿 USB 和 PCI 来说,它们的设备发现机制完全不同:PCI 靠配置空间读取 Vendor ID/Device ID,USB 靠描述符枚举。但有一点是相同的——都需要"设备和驱动匹配"这个逻辑。bus_type 就是把"总线"这个概念抽象出来,让匹配逻辑可以统一实现。
4.2 bus_type 的定义
// 路径:D:\work\linux-5.7.8\linux-5.7.8\include\linux\device.hstructbus_type {constchar *name; // "pci", "usb", "platform", "i2c"constchar *dev_name; // 设备命名方式,比如 USB 用 "usb-X"structdevice *dev_root;// 总线根设备// 设备与驱动的匹配——这是 bus_type 最重要的函数int (*match)(struct device *dev, struct device_driver *drv);// 总线上设备被探测前/后调用的钩子int (*probe)(struct device *dev);int (*remove)(struct device *dev);// uevent 回调,热插拔时通知用户空间int (*uevent)(struct device *dev, struct kobj_uevent_env *env);// 总线的属性,会在 /sys/bus/<name>/ 下以文件形式暴露structattribute_group **bus_groups;structattribute_group **dev_groups;structattribute_group **drv_groups;unsignedint drivers_autoprobe:1;structsubsys_private *p;// 私有数据,对外不可见};
4.3 总线的注册
以 platform 总线为例,它是 Linux 最简单也最常用的一种总线(设备树上的设备、PCI 总线上的设备等都属于 platform 设备):
// 路径:D:\work\linux-5.7.8\linux-5.7.8\drivers\base\platform.cintplatform_bus_type_init(void){ platform_bus_type.probe = platform_probe; platform_bus_type.remove = platform_remove; platform_bus_type.uevent = platform_uevent;return bus_register(&platform_bus_type); // 关键函数}
bus_register() 做了什么?它创建了 /sys/bus/platform/ 目录,并在里面初始化了 devices 和 drivers 两个子目录:
// 路径:D:\work\linux-5.7.8\linux-5.7.8\drivers\base\bus.cintbus_register(struct bus_type *bus){int retval;structsubsys_private *p;structlock_class_key *key = &bus->p->subsys_key;// 创建 /sys/bus/<name>/ retval = kset_register(&bus->p->devices_kset->kset); retval = kset_register(&bus->p->drivers_kset->kset);// 在 bus->p->devices_kset 和 bus->p->drivers_kset 上注册// 之后 device 和 driver 注册时自动放入对应子目录 ...}
4.4 平台总线的 match 实现
platform 总线的匹配规则非常直接——名字相同就匹配:
// 路径:D:\work\linux-5.7.8\linux-5.7.8\drivers\base\platform.cstaticintplatform_match(struct device *dev, struct device_driver *drv){structplatform_device *pdev = to_platform_device(dev);structplatform_driver *pdrv = to_platform_driver(drv);// 匹配方式一:名字完全匹配(最常用)if (of_driver_match_device(dev, drv))return1;if (pdrv->id_table)return platform_match_id(pdrv->id_table, pdev) != NULL;// 匹配方式三:设备名 == 驱动名(fallback)if (strcmp(pdev->name, drv->name) == 0)return1;return0;}
这个函数告诉我们一个重要事实:设备模型提供了 match 机制,但 match 的具体策略由各总线自己决定。PCI 总线按 Vendor/Device ID 匹配,USB 总线按接口类匹配,而 platform 总线按名字匹配——策略各异,框架统一。
五、device:设备抽象
5.1 device 结构
// 路径:D:\work\linux-5.7.8\linux-5.7.8\include\linux\device.hstructdevice {structdevice *parent;// 父设备,比如 USB 控制器是 USB 设备的父设备structdevice_private *p;structkobjectkobj;// 嵌入 kobject,决定它在 sysfs 中的位置constchar *init_name; // 设备初始名字,如 "eth0"conststructdevice_type *type;structbus_type *bus;// 设备所属的总线structdevice_driver *driver;// 已经绑定的驱动void *platform_data; // platform 设备的私有数据void *driver_data; // 驱动私有的设备数据structdev_links_infolinks;// 设备与其他设备之间的依赖关系structdev_pm_infopower;// 电源管理信息structdev_msi_infomsi; ... device_node *of_node; // 设备树节点(Device Tree) fwnode_handle *fwnode; // 固件节点抽象};
注意 parent 字段——这使得设备之间形成了一棵树。系统启动时,parent 指向控制器或总线,设备层层嵌套,最终根设备的 parent 为 NULL,整棵树以根设备为根。
5.2 设备的注册
// 路径:D:\work\linux-5.7.8\linux-5.7.8\drivers\base\core.cintdevice_register(struct device *dev){ device_initialize(dev);return device_add(dev);}voiddevice_initialize(struct device *dev){ dev->kobj.kset = devices_kset; // 挂入 /sys/devices/ kobject_init(&dev->kobj, &device_ktype); INIT_LIST_HEAD(&dev->p->knode_class); ...}intdevice_add(struct device *dev){ ...// 1. 将 device 的 kobject 加入 sysfs kobject_add(&dev->kobj, dev->kobj.parent, "%s", dev->init_name);// 2. 在 /sys/devices/<bus>/ 下创建符号链接 bus_add_device(dev);// 3. 将设备加入所属 class devices_kset->p->default_groups[0] = dev->type->groups;// 4. 发送 uevent 通知用户空间 kobject_uevent(&dev->kobj, KOBJ_ADD);// 5. 尝试与已有驱动匹配 bus_probe_device(dev); ...}
这个函数是设备注册的核心路径。bus_probe_device(dev) 的实现很关键:
// 路径:D:\work\linux-5.7.8\linux-5.7.8\drivers\base\bus.cvoidbus_probe_device(struct device *dev){structbus_type *bus = dev->bus;structdevice_driver *drv;int ret;if (!bus)return;if (bus->p->drivers_autoprobe) bus_for_each_drv(bus, NULL, &dev->state_initialized, __device_attach);}
它遍历总线上所有驱动,对每个驱动调用 __device_attach,后者调用 bus->match() 来判断是否匹配。
六、device_driver:驱动抽象
6.1 driver 结构
// 路径:D:\work\linux-5.7.8\linux-5.7.8\include\linux\device.hstructdevice_driver {constchar *name; // 驱动名,用于 matchstructbus_type *bus;// 驱动所属的总线structmodule *owner;int (*probe)(struct device *dev); // 匹配成功后调用int (*remove)(struct device *dev); // 设备移除时调用void (*shutdown)(struct device *dev); // 关机时调用int (*suspend)(struct device *dev, pm_message_t state);int (*resume)(struct device *dev);structdriver_private *p;};
6.2 驱动的注册与匹配
// 路径:D:\work\linux-5.7.8\linux-5.7.8\drivers\base\driver.cintdriver_register(struct device_driver *drv){ ... bus_add_driver(drv); // 把驱动加入总线的驱动列表 kobject_add(&drv->p->kobj, &bus->p->devices_kset->kobj, "%s", drv->name); driver_add_groups(drv, bus->drv_groups);// 尝试去匹配当前总线上所有已注册的设备 driver_attach(drv);return0;}intdriver_attach(struct device_driver *drv){return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);}staticint __driver_attach(struct device *dev, void *data){int ret; ret = bus->match(dev, drv); // ← 关键:调用总线的 matchif (ret == 0)return0; // 不匹配,跳过if (ret < 0)return ret; ret = driver_probe_device(drv, dev); // 匹配则执行 probe ...}
6.3 probe 的完整流程
// 路径:D:\work\linux-5.7.8\linux-5.7.8\drivers\base\dd.cstaticintdriver_probe_device(struct device_driver *drv, struct device *dev){ ...// 1. 调用总线的 probe 钩子(如果定义了的话)if (dev->bus->probe) ret = dev->bus->probe(dev);elseif (drv->probe) ret = drv->probe(dev); // ← 最终调用驱动的 probe// 2. 绑定成功后,建立双向链接 driver_bound(dev, drv);// 3. 发送 uevent 通知用户空间 kobject_uevent(&dev->kobj, KOBJ_BIND);return ret;}staticvoiddriver_bound(struct device *dev, struct device_driver *drv){ dev->driver = drv; // 设备记住驱动 dev->p->driver = drv; // 私有数据也记一份// 在 sysfs 中建立符号链接// /sys/devices/.../my-device/driver -> /sys/bus/.../drivers/my-driver/if (dev->bus && dev->bus->p && dev->bus->p->drivers_autoprobe) sysfs_create_link(&dev->kobj, &drv->p->kobj, "driver");}
绑定完成后,device->driver 和 driver->p->kobj 之间互相指向对方,sysfs 里也建立了符号链接——用户空间可以随时查到哪个设备对应哪个驱动。
七、class:设备类抽象
7.1 为什么需要 class?
想象一下你要写一个管理网络设备的管理工具。你需要知道系统里有哪些网卡,但网卡可能是 PCI 的、USB 的、虚拟的——物理路径完全不同。class 解决了这个问题:它按功能(而非物理位置)组织设备。
7.2 class 的注册
// 路径:D:\work\linux-5.7.8\linux-5.7.8\drivers\base\class.cint __class_register(struct class *cls, struct lock_class_key *key){structsubsys_private *p;int retval; p = kzalloc(sizeof(*p), GFP_KERNEL);if (!p)return -ENOMEM; p->class = cls; cls->p = p;// 创建 /sys/class/<name>/ retval = kset_register(&cls->p->class_kset); ...}intclass_register(struct class *cls){return __class_register(cls, &__key);}
7.3 设备加入 class 时发生什么?
当一个设备注册并属于某个 class 时,内核在 /sys/class/<name>/ 下创建一个符号链接指向设备的真实路径:
// 路径:D:\work\linux-5.7.8\linux-5.7.8\drivers\base\class.cintclass_add_device(struct class *class, struct device *device){structdevice *parent;structsubsys_private *cls_p = class->p;// 在 /sys/class/<classname>/ 下创建符号链接if (cls_p) sysfs_create_link(&cls_p->class_dir->kobj, &device->kobj, device->init_name);}
这样用户空间程序只需要去 /sys/class/net/ 下就能找到所有网卡,而不需要关心它们在 /sys/devices/ 下的具体路径。
八、电源管理:设备模型的应用
设备模型对电源管理的支持是它被引入内核的重要原因之一。每个 device 都有一个 power 字段:
// 路径:D:\work\linux-5.7.8\linux-5.7.8\include\linux\device.hstructdev_pm_info {pm_message_t state; // 当前电源状态unsignedint can_wakeup:1; // 设备能否唤醒系统unsignedint async_suspend:1;bool is_prepared:1; // 是否已准备好挂起bool is_suspended:1; // 是否已挂起bool may_skip_resume:1;structwakeup_source *wakeup;structtimer_listsuspend_timer;unsignedlong timer_expires; ...};
系统 suspend 时,内核从设备树的叶子节点开始,依次调用每个设备的 ->pm->runtime_suspend() 或 ->pm->suspend();resume 时反向遍历。这保证了子设备先挂起、后恢复,父设备后挂起、先恢复——和树的后序遍历顺序完全一致。
九、全景:设备模型的 sysfs 视图
/sys/├── devices/│ └── platform/ ← 设备树的根,所有设备的物理位置│ └── my-device/│ ├── driver -> ../../../bus/platform/drivers/my-driver/│ ├── subsystem -> ../../../class/myclass/│ └── uevent│├── bus/ ← 所有总线│ └── platform/│ ├── devices/ ← 挂在此总线上的设备(符号链接)│ │ └── my-device -> ../../../devices/platform/my-device/│ └── drivers/│ └── my-driver/│ ├── bind│ ├── unbind│ ├── uevent│ └── new_id│├── class/ ← 所有设备类│ └── myclass/│ └── my-device -> ../../devices/platform/my-device/│└── drivers/ ← 全局驱动视图(已被 bus 替代)
uevent 文件特别有用:往里面写东西会触发 uevent,可以用来模拟设备插拔,方便调试。
十、核心流程总结
10.1 设备注册 → 匹配驱动
platform_driver_register() → bus_add_driver() 在 /sys/bus/platform/drivers/ 注册 → driver_attach() → bus_for_each_dev() 遍历平台上所有设备 → __driver_attach() → platform_match() 比较驱动名和设备名 → driver_probe_device() → drv->probe() 匹配成功,调用驱动probe → driver_bound() → sysfs_create_link() 建立 driver 符号链接 → kobject_uevent() 发送 KOBJ_BIND uevent
10.2 驱动注册 → 匹配设备(设备先于驱动存在的情况)
platform_device_register() → device_add() → kobject_add() 在 /sys/devices/ 创建目录 → bus_add_device() 在 /sys/bus/platform/devices/ 创建链接 → kobject_uevent() 发送 KOBJ_ADD uevent → bus_probe_device() → bus_for_each_drv() → __device_attach() → platform_match() → driver_probe_device() → drv->probe() → driver_bound()
这两条路径是对称的——谁先注册并不重要,设备模型保证最终总能匹配上。
十一、总结
Linux 设备模型的核心逻辑:
| | |
|---|
| kobject | | |
| kset | | |
| bus_type | | /sys/bus/ |
| device_driver | 驱动的逻辑,probe/remove/suspend/resume 回调 | /sys/bus/*/drivers/ |
| device | | /sys/devices/ |
| class | | /sys/class/ |
设备模型看起来复杂,但它的设计非常优雅:用分层抽象解决耦合问题,用 kobject + 引用计数解决生命周期问题,用 match 回调解决驱动兼容性问题和可扩展性问题。
理解了总线(bus)、设备(device)、驱动(driver)、类(class)这四个核心概念及其关系,再去看 pci_bus_type、usb_bus_type、i2c_adapter 这些具体子系统的实现,就会发现它们都在这个框架里——框架是通用的,细节是具体的。
真正复杂的不是设备模型本身,而是它要解决的那些硬件层面的历史遗留问题。模型本身是干净的。