kobject 与 kset:设备模型的最小单元与组织架构
好,咱们今天来聊聊内核设备模型里最基础的两个概念——kobject 和 kset。
说实话,我刚接触内核时,看到这两个名字就觉得头大。什么 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。
它们之间的关系
来,我画个图帮你理清思路:
这张图展示的是内核中 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 这些高层抽象就会轻松很多。