当前位置:首页>Linux>Linux 设备模型:设计与实现

Linux 设备模型:设计与实现

  • 2026-09-10 15:14:36
Linux 设备模型:设计与实现

阅读本文的前提:你写过一点内核驱动,但对设备模型的整体架构还比较模糊。本文不堆砌术语,目标是让你搞清楚"设备模型这套机制到底是怎么工作的",所有代码均来自 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 设备模型的核心逻辑:

概念
作用
sysfs 位置
kobject
底层基础设施,管 sysfs 映射和引用计数
每个目录对应一个
kset
一组 kobject 的集合
目录下的子目录
bus_type
负责设备和驱动的匹配(match)
/sys/bus/
device_driver
驱动的逻辑,probe/remove/suspend/resume 回调
/sys/bus/*/drivers/
device
具体硬件设备,挂在某个 bus 上
/sys/devices/
class
同一类设备的统一抽象(按功能分组)
/sys/class/

设备模型看起来复杂,但它的设计非常优雅:用分层抽象解决耦合问题,用 kobject + 引用计数解决生命周期问题,用 match 回调解决驱动兼容性问题和可扩展性问题。

理解了总线(bus)、设备(device)、驱动(driver)、类(class)这四个核心概念及其关系,再去看 pci_bus_type、usb_bus_type、i2c_adapter 这些具体子系统的实现,就会发现它们都在这个框架里——框架是通用的,细节是具体的。

真正复杂的不是设备模型本身,而是它要解决的那些硬件层面的历史遗留问题。模型本身是干净的。

最新文章

随机文章