kobject 与 kset:设备模型的最小单元与组织架构

好,咱们今天来聊聊内核设备模型里最基础的两个概念——kobjectkset

说实话,我刚接触内核时,看到这两个名字就觉得头大。什么 kobject、kset、ktype……一堆 k 开头的结构体,看着就晕。但后来我慢慢发现,这些东西其实没那么玄乎。说白了,kobject 就是内核里最小的“对象单元”,而 kset 就是把这些对象组织起来的“容器”。

你想想看,Linux 内核要管理成千上万的设备、驱动、总线、类……如果没有一套统一的对象模型,那代码得乱成什么样?所以内核搞出了这套 设备模型,而 kobject 和 kset 就是它的基石。

kobject:内核里的“最小公民”

kobject 是什么?我习惯把它理解成“内核对象的最小单元”。每个 kobject 代表一个可以被引用计数、可以被命名、可以被放入 sysfs 文件系统的实体。

来看它的定义(我简化了一下):

struct kobject {
    const char      *name;        // 对象的名字
    struct list_head entry;       // 用于加入 kset 的链表节点
    struct kobject  *parent;      // 父对象
    struct kset     *kset;        // 所属的 kset
    struct kobj_type *ktype;      // 对象类型
    struct kernfs_node *sd;       // sysfs 目录项
    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;
    unsigned int uevent_suppress:1;
};

嗯,字段不少。但核心就几个:名字、父对象、所属的 kset、引用计数

我在项目中遇到过一个问题:有个驱动卸载时总是崩溃,查了半天发现是 kobject 的引用计数没处理好,导致对象被释放后还有人用。从那以后,我对 kref 就特别敏感。

核心要点:kobject 本身不单独使用,它总是嵌入到更大的结构体中。比如 struct device 里就嵌了一个 kobject。

kobject 的生命周期

kobject 的生命周期管理,说白了就是引用计数的加减。

  • kobject_init():初始化 kobject,设置引用计数为 1
  • kobject_add():将 kobject 注册到 sysfs,并关联到父对象
  • kobject_get():增加引用计数
  • kobject_put():减少引用计数,减到 0 时自动释放

我曾经踩过一个坑:在中断上下文里调用了 kobject_put(),结果触发了内存释放,而释放函数里又调了可能睡眠的函数……嗯,内核直接 panic 了。所以记住:kobject_put() 不要在原子上下文里调用

kset:把 kobject 组织起来

kset 是什么?它就是一组同类型 kobject 的集合。你可以把它理解成一个“文件夹”,里面放着一堆“文件”(kobject)。

struct kset {
    struct list_head list;        // 链表头,挂载所有 kobject
    spinlock_t      list_lock;    // 保护链表的锁
    struct kobject  kobj;         // 内嵌的 kobject,代表这个 kset 自己
    const struct kset_uevent_ops *uevent_ops; // 事件通知操作
};

注意看,kset 自己内部也嵌了一个 kobject。这意味着 kset 本身也是一个“对象”,可以被挂到别的 kset 下面。这就形成了层次结构。

我个人的理解是:kobject 是节点,kset 是容器。容器本身也是一个节点,所以可以嵌套。

ktype:定义对象的行为

每个 kobject 都有一个 ktype,它定义了这类对象的通用操作:

struct kobj_type {
    void (*release)(struct kobject *kobj);  // 释放函数
    const struct sysfs_ops *sysfs_ops;      // sysfs 文件操作
    struct attribute **default_attrs;       // 默认属性
};

release 函数特别重要。当引用计数归零时,内核会调用这个函数来释放对象。如果你忘了实现 release,或者实现得不对,那内存泄漏就找上门了。

警告:每个 kobject 必须实现 release 函数!这是内核的硬性要求。我见过有人把 release 设为 NULL,结果内核直接 oops。

它们之间的关系

来,我画个图帮你理清思路:

kset (bus_kset) kobject (kset自身) name = "bus" kobject (pci) parent = bus_kset kobject (platform) parent = bus_kset kobject (spi) parent = bus_kset list list list 每个 kobject 通过 entry 链表挂到 kset 的 list 上

这张图展示的是内核中 bus_kset 的典型结构。bus_kset 本身是一个 kset,它内嵌了一个 kobject(名字叫 "bus")。然后 pci、platform、spi 这些总线各自是一个 kobject,都挂在这个 kset 下面。

你可能会问:为什么要搞这么复杂?直接用一个链表不就行了?

嗯,原因在于这套机制提供了引用计数、sysfs 导出、uevent 通知等一系列基础设施。你写驱动时,只需要把 kobject 嵌入到自己的结构体里,然后调用几个 API,就能自动获得这些能力。

实际使用示例

来看一个简单的例子,感受一下怎么用:

// 定义一个包含 kobject 的结构体
struct my_device {
    int data;
    struct kobject kobj;  // 必须放在最后?不,其实放哪都行
};

// release 函数
static void my_release(struct kobject *kobj)
{
    struct my_device *dev = container_of(kobj, struct my_device, kobj);
    printk("Releasing my_device\n");
    kfree(dev);
}

// 定义 ktype
static struct kobj_type my_ktype = {
    .release = my_release,
    .sysfs_ops = NULL,  // 这里可以定义 sysfs 操作
};

// 创建并注册
struct my_device *create_my_device(const char *name)
{
    struct my_device *dev;
    int ret;

    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return NULL;

    // 初始化 kobject
    ret = kobject_init_and_add(&dev->kobj, &my_ktype, NULL, "%s", name);
    if (ret) {
        kobject_put(&dev->kobj);  // 失败时释放
        return NULL;
    }

    return dev;
}

// 销毁
void destroy_my_device(struct my_device *dev)
{
    kobject_put(&dev->kobj);  // 引用计数减1,到0时触发 release
}

小技巧:kobject_init_and_add() 这个函数把 init 和 add 合二为一,省事。但我个人习惯分开调,因为 init 失败和 add 失败的处理方式不一样,分开调更容易控制错误路径。

避坑指南

最后,分享几个我踩过的坑:

  • 命名冲突:kobject 的名字在同一个父对象下必须唯一。我曾经在两个驱动里用了相同的名字,结果第二个注册时直接失败。
  • 引用计数泄漏:每次 kobject_get() 必须对应一次 kobject_put()。我见过有人 get 了三次只 put 了两次,对象永远释放不了。
  • sysfs 路径:kobject 的 sysfs 路径由 parent 链决定。如果你忘了设 parent,那它就会出现在 /sys/ 根目录下,很乱。
  • uevent 抑制:如果你不想让 kobject 的创建/删除触发用户空间事件,可以设置 uevent_suppress = 1。我在做热插拔模拟时用过这个。

好了,kobject 和 kset 就聊到这儿。这套机制虽然看着啰嗦,但它是整个设备模型的根基。理解了它,后面看 device、driver、bus 这些高层抽象就会轻松很多。


专注资料整理