3. 内核对象与kobject

Linux 设备模型并不是凭空存在的抽象层,它由一组基础数据结构支撑,其中最关键的就是 kobject。可以把 kobject 理解为内核中“可被引用、可被挂接、可被导出到用户空间”的最小对象单元。kset 负责把同类 kobject 组织成集合,ktype 则描述这一类对象共有的行为。三者配合,构成了设备、驱动、总线、类等对象的统一生命周期与 sysfs 映射基础。

一、kobject、kset、ktype 的职责分工

kobject 本身并不直接代表某个设备,它更像一个“管理骨架”,内嵌在 struct device、struct device_driver、struct class 等更大的结构体中。它提供引用计数、父子层级关系、名称以及 sysfs 目录项等能力。kset 是 kobject 的集合,通常对应 sysfs 中的一个目录层级,例如 /sys/devices 下按总线或设备类型划分的集合。ktype 则定义了一类 kobject 的释放函数和 sysfs 属性操作,保证同类对象在生命周期结束时执行正确的清理逻辑。

三者的关系可以概括为:kobject 是对象实例,kset 是对象的组织容器,ktype 是对象的行为模板。一个 kobject 可以属于某个 kset,也可以拥有自己的 ktype;kset 自身也内嵌一个 kobject,因此集合本身同样参与引用计数和层级管理。

kobject / kset / ktype 与 sysfs 映射 kobject 引用计数 kref 父/子层级 parent 名称 name sysfs 目录项 kobj kset 同类 kobject 集合 内嵌 kobject 链表 list 管理成员 对应 sysfs 目录层级 ktype release 释放回调 sysfs_ops 属性操作 default_attrs 默认属性 同类对象行为模板 sysfs 映射结果 /sys/devices/.../deviceX/ ← kobject 目录 /sys/bus/pci/devices/ ← kset 集合目录 生命周期核心:kref 归零 → release() → 释放内存并移除 sysfs 目录 引用计数保护对象在并发访问中不被提前释放

二、引用计数与生命周期管理

kobject 的生命周期由引用计数控制,底层使用 struct kref。每当有新的使用者需要访问该对象时,调用 kobject_get() 增加计数;使用完毕后调用 kobject_put() 减少计数。当计数归零时,内核不会立即释放内存,而是调用该 kobject 所属 ktype 的 release() 回调,由它完成最终的内存释放和资源清理。这种“延迟释放”机制是内核对象安全并发访问的基石。

struct kobject {
    const char      *name;
    struct list_head entry;
    struct kobject  *parent;
    struct kset     *kset;
    struct kobj_type *ktype;
    struct kernfs_node *sd;
    struct kref     kref;
    unsigned int    state_initialized:1;
    unsigned int    state_in_sysfs:1;
    unsigned int    state_add_uevent_sent:1;
    unsigned int    state_remove_uevent_sent:1;
};

典型流程是:先初始化 kobject,再将其添加到 kset 或指定 parent,此时 sysfs 目录被创建;使用期间通过 get/put 维持引用;当不再需要时调用 kobject_del() 从 sysfs 移除,再调用 kobject_put() 释放最后一个引用。注意,kobject_del() 只负责断开与 sysfs 和父对象的联系,并不释放内存,真正的释放发生在引用计数归零之后。

常见误区:很多驱动开发者误以为调用 kobject_put() 就一定会立刻释放对象。实际上,如果还有其他持有者未释放引用,对象会继续存活。因此,release 回调中必须只做“最后一次清理”,不能假设它会在每次 put 时都被调用。

三、sysfs 映射机制

sysfs 是 kobject 层级在用户空间的可视化投影。每个 kobject 在 sysfs 中对应一个目录,目录名来自 kobject 的 name;目录之间的父子关系来自 kobject 的 parent 指针;目录中的属性文件则来自 ktype 的 default_attrs 或动态添加的 attribute。kset 作为集合,通常对应一个上层目录,其成员 kobject 作为子目录出现。

当 kobject 被添加到内核对象层级时,内核会调用 kobject_add(),进而通过 kernfs 创建对应的目录和属性文件。属性文件的读写最终会回调到 ktype 中定义的 sysfs_ops 的 show/store 方法。这样,用户通过 echo 或 cat 操作 sysfs 文件时,实际上是在间接调用内核对象的属性访问函数。

# 查看某个 kobject 对应的 sysfs 目录
ls -l /sys/devices/pci0000:00/0000:00:1f.2/

# 查看 kobject 的引用计数与状态(调试用)
cat /sys/kernel/debug/kobject/0000:00:1f.2/uevent

# 典型属性文件读写
cat /sys/class/net/eth0/address
echo 1 > /sys/class/net/eth0/queues/rx-0/rps_cpus

四、案例:设备注册中的 kobject 生命周期

以 PCI 设备注册为例,当内核枚举到一个 PCI 设备时,会分配 struct pci_dev,其中内嵌 struct device,而 struct device 又内嵌 struct kobject。随后调用 device_register(),该函数会初始化 kobject、设置 ktype 为 device_ktype、将 kobject 添加到设备 kset 中,并在 sysfs 中创建 /sys/devices/pci.../0000:00:1f.2/ 目录。此时引用计数为 1。

当驱动通过 pci_get_device() 获取该设备时,引用计数增加;驱动卸载或设备移除时,调用 pci_dev_put() 减少引用。最终当所有引用释放后,device_ktype.release 被调用,释放 pci_dev 内存,sysfs 目录也随之消失。整个过程体现了“引用计数保护访问、release 负责释放、sysfs 反映状态”的闭环。

对象 核心作用 生命周期关键点 sysfs 表现
kobject 引用计数、层级、名称、sysfs 目录项 kobject_get / kobject_put / release 一个目录
kset 同类 kobject 集合与上层目录 kset 自身也内嵌 kobject,参与计数 集合目录
ktype 定义 release 与属性操作 release 在计数归零时调用 属性文件行为
调试建议:当发现 sysfs 目录无法删除或设备无法释放时,优先检查 kobject 引用计数是否泄漏。可通过 /sys/kernel/debug/kobject/ 下的调试接口观察对象状态,或使用 kobject_put() 的调用路径进行排查。常见泄漏点包括:获取设备后未 put、在中断上下文中错误持有引用、以及 release 回调中再次访问已释放的父对象。

理解 kobject、kset、ktype 的分工,掌握引用计数与 release 的配合方式,是读懂 Linux 设备模型和 sysfs 行为的关键。它们把“对象存在多久”“对象如何组织”“对象如何暴露给用户”三个问题统一到一套机制中,为设备、驱动、总线、类等上层概念提供了稳定的底座。