Skip to content

LV055-kobject简介

一、kobject简介

1. 背景

前面知道 inux 设备模型的核心是使用 bus、class、device、driver 四个核心数据结构,将大量不同功能的硬件设备及其驱动,以树状结构的形式,进行归纳、抽象,从而方便 kernel 的统一管理。而硬件设备的数量、种类是非常多的,这就决定了 kernel 中将会有大量的有关设备模型的数据结构。这些数据结构一定有一些共同的功能,需要抽象出来统一实现,否则就会不可避免的产生冗余代码。这就是 kobject 诞生的背景。

2. kobject 是什么?

kobject(内核对象)是内核中抽象出来的通用对象模型, 用于表示内核中的各种实体。 kobject 是一个结构体, 其中包含了一些描述该对象的属性和方法。 它提供了一种统一的接口和机制,用于管理和操作内核对象。

kobject 结构体定义在 kobject.h - include/linux/kobject.h -struct kobject

c
struct kobject {
	const char		*name;
	struct list_head	entry;
	struct kobject		*parent;
	struct kset		*kset;
	struct kobj_type	*ktype;
	struct kernfs_node	*sd; /* sysfs directory entry */
	struct kref		kref;
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
	struct delayed_work	release;
#endif
	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;
};
  • name:该 kobject 的名称,通常用于在/sys 目录下创建对应的目录。由于 kobject 添加到 kernel 时,需要根据名字注册到 sysfs 中,之后就不能再直接修改该字段。如果需要修改 kobject 的名字,需要调用 kobject_rename() 接口,该接口会主动处理 sysfs 的相关事宜。
  • entry:用于将 kobject 加入到 kset 中的 list_head,也就是将 kobject 链接到父 kobject 的子对象列表中, 以建立层次关系。
  • parent:指向 parent kobject,以此形成层次结构(在 sysfs 就表现为目录结构)。
  • kset:指向包含该 kobject 的 kset(可以为 NULL), 用于进一步组织和管理 kobject。如果没有指定 parent,则会把 kset 作为 parent(kset 是一个特殊的 kobject)。
  • ktype:该 kobject 属于的 kobj_type,描述 kobject 的属性和操作。每个 kobject 必须有一个 ktype,否则 kernel 会提示错误。
  • sd:kernfs_node 类型,该 kobject 在 sysfs 中的层次结构。指向 sysfs 目录中对应的 kernfs_node, 用于访问和操作 sysfs 目录项。
  • kref:struct kref 类型的变量,是一个可用于原子操作的引用计数。它用于对 kobject 进行引用计数, 确保在不再使用时能够正确释放资源。
  • state_initialized:指示该 kobject 是否已经初始化,以在 kobject 的 Init,Put,Add 等操作时进行异常校验。
  • state_in_sysfs:指示该 kobject 是否已在 sysfs 中呈现,以便在自动注销时从 sysfs 中移除。
  • state_add_uevent_sent:记录是否已经向用户空间发送 ADD uevent。
  • state_remove_uevent_sent:记录是否已经向用户空间发送 REMOVE uevent,
  • uevent_suppress:如果该字段为 1,则表示忽略所有上报的 uevent 事件。

uevent 提供了“用户空间通知”的功能实现,通过该功能,当内核中有 kobject 的增删改等动作时,会通知用户空间。

3. kobject 能做什么?

kobject 主要提供如下功能:

(1)通过 parent 指针,可以将所有 kobject 以层次结构的形式组合起来。

(2)使用一个引用计数(reference count),来记录 kobject 被引用的次数,并在引用次数变为 0 时把它释放(这是 kobject 诞生时的唯一功能)。

(3)和 sysfs 虚拟文件系统配合,将每一个 kobject 及其特性,以文件的形式显示到用户空间(/sys 目录下的那些东西)。

Tips

(1)在 Linux 中,kobject 几乎不会单独存在。它的主要功能,就是内嵌在一个大型的数据结构中,为这个数据结构提供一些底层的功能实现。

(2)Linux driver 开发者,很少会直接使用 kobject 以及它提供的接口,而是使用构建在 kobject 之上的设备模型接口。

二、在系统中的表现?

每一个 kobject 都会对应系统/sys/下的一个目录:

image-20250105142410897

目录又是有多个层次, 所以对应 kobject 的树状关系如下图所示:

image-20250105151716578

在 kobject 结构体中, parent 指针用于表示父 kobject, 从而建立了 kobject 之间的层次关系, 类似于目录结构中的父目录和子目录的关系。 一个 kobject 可以有一个父 kobject 和多个子 kobject, 通过 parent 指针可以将它们连接起来形成一个层次化的结构,类似于目录结构中, 一个目录可以有一个父目录和多个子目录, 通过目录的路径可以表示目录之间的层次关系。 这种层次化的关系可以方便地进行遍历, 查找和管理, 使得内核对象能够按照层次关系进行组织和管理。 这种设计使得 kobject 的树状结构在内核中具有很高的灵活性和可扩展性。

三、kobject 机制

kobject 的核心功能是:保持一个引用计数,当该计数减为 0 时,自动释放 kobject 所占用的 meomry 空间。这就决定了 kobject 必须是动态分配的,因为只有这样才能动态释放。

kobject 大多数的使用场景,是内嵌在大型的数据结构中(如 kset、device_driver 等),因此这些大型的数据结构,也必须是动态分配、动态释放的。

那么释放的时机是什么呢?是内嵌的 kobject 释放时。但是 kobject 的释放是由 kobject 模块自动完成的(在引用计数为 0 时),那么怎么一并释放包含自己的大型数据结构呢?

这时 kobj_type 就派上用场了。因为 kobj_type 中的 release 回调函数负责释放 kobject(甚至是包含 kobject 的数据结构)的内存空间。

那么 kobj_type 及其内部函数,是由谁实现呢?是由上层数据结构所在的模块!因为只有它才清楚 kobject 嵌在哪个数据结构中,并通过 kobject 指针以及自身的数据结构类型,利用函数宏 containerof 找到需要释放的上层数据结构的指针,然后释放它。

所以,每一个内嵌 kobject 的数据结构,例如 kset、device、device_driver 等等,都要实现一个 kobj_type,并定义其中的回调函数。同理,sysfs 相关的操作也一样,必须经过 kobj_type 的中转,因为 sysfs 看到的是 kobject,而真正的文件操作的主体,是内嵌 kobject 的上层数据结构!

kobject 是面向对象的思想在 Linux kernel 中的极致体现,但 C 语言的优势却不在这里,所以 Linux kernel 需要用比较巧妙的手段去实现它。

这里出现的 kobj_type、kset、引用计数等相关概念后面都会了解到的。

四、相关 API

1. kobject 使用流程

  • kobject 大多数情况下(有一种例外,下面会提到)会嵌在其它数据结构中使用,其使用流程如下:

(1)定义一个 struct kset 类型的指针,并在初始化时为它分配空间,添加到内核中;

(2)根据实际情况,定义内嵌有 kobject 的自己所需的数据结构原型;

(3)定义一个适合自己的 kobj_type,并实现其中回调函数 release;

(4)在需要使用到包含 kobject 的数据结构时,动态分配该数据结构,并分配 kobject 空间,添加到内核中;

(5)每一次引用数据结构时,调用 kobject_get() 接口增加引用计数;引用结束时,调用 kobject_put() 接口,减少引用计数;

(6)当引用计数为 0 时,kobject 模块会自动调用 kobj_type 所提供的 release 接口,释放上层数据结构以及 kobject 的内存空间。

  • 有一种例外就是:开发者只需要在 sysfs 中创建一个目录,而不需要其它的 kset、kobj_type 的操作。这时可以直接调用 kobject_create_and_add() 接口,分配一个 kobject 结构并把它添加到内核中。

2. kobject 的分配和释放

前面提到 kobject 必须动态分配,而不能静态定义或者位于堆栈之上,它的分配方法有两种。

2.1 通过 kmalloc 自行分配

第一种分配方法就是 通过 kmalloc 自行分配(一般是跟随上层数据结构分配),并在初始化后添加到 kernel。

2.1.1 kobject_init()

kobject_init() 函数定义如下:

c
/**
 * kobject_init - initialize a kobject structure
 * @kobj: pointer to the kobject to initialize
 * @ktype: pointer to the ktype for this kobject.
 *
 * This function will properly initialize a kobject such that it can then
 * be passed to the kobject_add() call.
 *
 * After this function is called, the kobject MUST be cleaned up by a call
 * to kobject_put(), not by a call to kfree directly to ensure that all of
 * the memory is cleaned up properly.
 */
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
{
	char *err_str;

	if (!kobj) {
		err_str = "invalid kobject pointer!";
		goto error;
	}
	if (!ktype) {
		err_str = "must have a ktype to be initialized properly!\n";
		goto error;
	}
	if (kobj->state_initialized) {
		/* do not error out as sometimes we can recover */
		pr_err("kobject (%p): tried to init an initialized object, something is seriously wrong.\n",
		       kobj);
		dump_stack();
	}

	kobject_init_internal(kobj);
	kobj->ktype = ktype;
	return;

error:
	pr_err("kobject (%p): %s\n", kobj, err_str);
	dump_stack();
}
EXPORT_SYMBOL(kobject_init);

kobject_init:初始化通过 kmalloc 等内存分配函数获得的 struct kobject 指针。主要执行逻辑为:

(1)第 17 - 24 行:确认 kobj 和 ktype 不为空;

(2)第 25 行:如果该指针已经初始化过(判断 kobj→ state_initialized),打印错误提示及堆栈信息(但不是致命错误,所以还可以继续);

(3)第 32 行:初始化 kobj 内部的参数,包括引用计数 kref、list_head、各种标志等;

(4)第 33 行:将输入形参 ktype 赋予 kobj→ ktype 。

2.1.2 kobject_add()

kobject_add() 定义如下:

c
int kobject_add(struct kobject *kobj, struct kobject *parent,
		const char *fmt, ...)
{
	va_list args;
	int retval;

	if (!kobj)
		return -EINVAL;

	if (!kobj->state_initialized) {
		pr_err("kobject '%s' (%p): tried to add an uninitialized object, something is seriously wrong.\n",
		       kobject_name(kobj), kobj);
		dump_stack();
		return -EINVAL;
	}
	va_start(args, fmt);
	retval = kobject_add_varg(kobj, parent, fmt, args);
	va_end(args);

	return retval;
}
EXPORT_SYMBOL(kobject_add);

如果 koibject 需要添加到 sysfs 中,则必须要调用 kobject_add() 函数。它将初始化完成的 kobject 添加到 kernel 中,参数包括需要添加的 kobject、该 kobject 的 parent(用于形成层次结构,可以为空)、用于提供 kobject name 的格式化字符串。主要执行逻辑为:

(1)确认 kobj 不为空,确认 kobj 已经初始化,否则错误退出。

(2)调用内部接口 kobject_add_varg(),完成添加操作,具体调用逻辑如下:

image-20250105154536844

populate_dir() 函数中逐个处理内核对象所属对象类型的默认属性,对每个属性,调用 sysfs_create_file() 函数在内核对象的目录下创建以属性名为名字的文件。kobject_add() 中只会为默认属性自动创建文件。

2.1.3 kobject_init_and_add()

kobject_init_and_add() 函数定义如下:

c
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,
			 struct kobject *parent, const char *fmt, ...)
{
	va_list args;
	int retval;

	kobject_init(kobj, ktype);

	va_start(args, fmt);
	retval = kobject_add_varg(kobj, parent, fmt, args);
	va_end(args);

	return retval;
}
EXPORT_SYMBOL_GPL(kobject_init_and_add);

qis 其实就是上面两个接口的一个组合。kobject_add_varg() 解析格式化字符串,将结果赋予 kobj→ name,之后调用 kobject_add_internal() 接口,完成真正的添加操作。 kobject_add_internal() 将 kobject 添加到 kernel,主要执行的逻辑为:

(1)校验 kobj 以及 kobj→ name 的合法性,若不合法打印错误信息并退出;

(2)调用 kobject_get() 增加该 kobject 的 parent 的引用计数(如果存在 parent 的话);

(3)如果存在 kset,则调用 kobj_kset_join() 接口将本 kobject 加入到此 kset 的 kobject 链表中。同时,如果该 kobject 没有 parent,却存在 kset,则将它的 parent 设为 kset(kset 是一个特殊的 kobject),并增加 kset 的引用计数;

(4)通过 create_dir() 接口,调用 sysfs 的相关接口,在 sysfs 下创建该 kobject 对应的目录;

(5)如果创建失败,则执行后续的回滚操作,否则将 kobj→ state_in_sysfs 置为 1;

这种方式分配的 kobject,会在引用计数变为 0 时,由 kobject_put 调用其 kobj_type 的 release 接口,释放内存空间,具体可参考后面有关 kobject_put 的笔记。

2.2 使用 kobject_create()创建

kobject 模块可以使用 kobject_create() 自行分配空间,并内置了一个 ktype(dynamic_kobj_ktype),用于在计数为 0 时释放空间。

2.2.1 kobject_create()

kobject_create() 函数定义如下:

c
struct kobject *kobject_create(void)
{
	struct kobject *kobj;

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

	kobject_init(kobj, &dynamic_kobj_ktype);
	return kobj;
}

该接口为 kobj 分配内存空间,并以 dynamic_kobj_ktype 为参数,调用 kobject_init() 接口,完成后续的初始化操作。

2.2.2 kobject_add()

kobject_add() 函数在前面已经了解过了。

2.2.3 kobject_create_and_add()

kobject_create_and_add() 函数定义如下:

c
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent)
{
	struct kobject *kobj;
	int retval;

	kobj = kobject_create();
	if (!kobj)
		return NULL;

	retval = kobject_add(kobj, parent, "%s", name);
	if (retval) {
		pr_warn("%s: kobject_add error: %d\n", __func__, retval);
		kobject_put(kobj);
		kobj = NULL;
	}
	return kobj;
}
EXPORT_SYMBOL_GPL(kobject_create_and_add);

其实就是 kobject_create()kobject_add() 的组合。

3. kobject 引用计数的加减

引用计数是啥?后面会了解到的,这里主要是看一下 object 常用的 api。

通过 kobject_get()kobject_put() 可以修改 kobject 的引用计数 kref,并在 kref 为 0 时,调用对应 ktype 的 release 接口,释放占用空间。

3.1 kobject_get()

kobject_get() 函数定义如下:

c
/**
 * kobject_get - increment refcount for object.
 * @kobj: object.
 */
struct kobject *kobject_get(struct kobject *kobj)
{
	if (kobj) {
		if (!kobj->state_initialized)
			WARN(1, KERN_WARNING
				"kobject: '%s' (%p): is not initialized, yet kobject_get() is being called.\n",
			     kobject_name(kobj), kobj);
		kref_get(&kobj->kref);
	}
	return kobj;
}
EXPORT_SYMBOL(kobject_get);

3.2 kobject_put()

kobject_put() 函数定义如下:

c
/**
 * kobject_put - decrement refcount for object.
 * @kobj: object.
 *
 * Decrement the refcount, and if 0, call kobject_cleanup().
 */
void kobject_put(struct kobject *kobj)
{
	if (kobj) {
		if (!kobj->state_initialized)
			WARN(1, KERN_WARNING
				"kobject: '%s' (%p): is not initialized, yet kobject_put() is being called.\n",
			     kobject_name(kobj), kobj);
		kref_put(&kobj->kref, kobject_release);
	}
}
EXPORT_SYMBOL(kobject_put);

以内部接口 kobject_release() 为参数,调用 kref_put() 。kref 模块会在引用计数为零时,调用 kobject_release()

3.3 kobject_release()

kobject_release() 函数定义如下:

c
static void kobject_release(struct kref *kref)
{
	struct kobject *kobj = container_of(kref, struct kobject, kref);
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
	unsigned long delay = HZ + HZ * (get_random_int() & 0x3);
	pr_info("kobject: '%s' (%p): %s, parent %p (delayed %ld)\n",
		 kobject_name(kobj), kobj, __func__, kobj->parent, delay);
	INIT_DELAYED_WORK(&kobj->release, kobject_delayed_cleanup);

	schedule_delayed_work(&kobj->release, delay);
#else
	kobject_cleanup(kobj);
#endif
}

kobject_release() 函数通过 kref 结构,获取 kobject 指针,并调用 kobject_cleanup() 函数:

c
/*
 * kobject_cleanup - free kobject resources.
 * @kobj: object to cleanup
 */
static void kobject_cleanup(struct kobject *kobj)
{
	struct kobj_type *t = get_ktype(kobj);
	const char *name = kobj->name;

	pr_debug("kobject: '%s' (%p): %s, parent %p\n",
		 kobject_name(kobj), kobj, __func__, kobj->parent);

	if (t && !t->release)
		pr_debug("kobject: '%s' (%p): does not have a release() function, it is broken and must be fixed.\n",
			 kobject_name(kobj), kobj);

	/* send "remove" if the caller did not do it but sent "add" */
	if (kobj->state_add_uevent_sent && !kobj->state_remove_uevent_sent) {
		pr_debug("kobject: '%s' (%p): auto cleanup 'remove' event\n",
			 kobject_name(kobj), kobj);
		kobject_uevent(kobj, KOBJ_REMOVE);
	}

	/* remove from sysfs if the caller did not do it */
	if (kobj->state_in_sysfs) {
		pr_debug("kobject: '%s' (%p): auto cleanup kobject_del\n",
			 kobject_name(kobj), kobj);
		kobject_del(kobj);
	}

	if (t && t->release) {
		pr_debug("kobject: '%s' (%p): calling ktype release\n",
			 kobject_name(kobj), kobj);
		t->release(kobj);
	}

	/* free name if we allocated it */
	if (name) {
		pr_debug("kobject: '%s': free name\n", name);
		kfree_const(name);
	}
}

kobject_cleanup() 负责释放 kobject 占用的空间,主要执行逻辑如下:

(1)检查该 kobject 是否有 ktype,如果没有,打印警告信息;

(2)如果该 kobject 向用户空间发送了 ADD uevent 但没有发送 REMOVE uevent,补发 REMOVE uevent;

(3)如果该 kobject 有在 sysfs 文件系统注册,调用 kobject_del 接口,删除它在 sysfs 中的注册;

(4)调用该 kobject 的 ktype 的 release 接口,释放内存空间;

(5)释放该 kobject 的 name 所占用的内存空间;

五、创建 kobject demo

1. demo 源码

可以直接看这里:05_device_model/02_kobject_create

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/kobject.h>
#include <linux/slab.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

// 定义三个 kobject 指针变量:skobject1、skobject2、skobject3
struct kobject *skobject1 = NULL;
struct kobject *skobject2 = NULL;
struct kobject *skobject3 = NULL;

struct kobj_type stype = {0}; // 定义一个 kobj_type 结构体变量 stype,用于描述 kobject 的类型。

/**
 * @brief  sdriver_demo_init()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __init int sdriver_demo_init(void)
{
    int ret = 0;
	printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
    PRT("sdriver_demo module init!\n");
    
    // 创建 kobject 的第一种方法
    // 创建并添加一个名为 "skobject1" 的 kobject 对象,父 kobject 为 NULL
    skobject1 = kobject_create_and_add("skobject1", NULL);
    // 创建并添加一个名为 "skobject2" 的 kobject 对象,父 kobject 为 skobject1。
    skobject2 = kobject_create_and_add("skobject2", skobject1);

    // 创建 kobject 的第二种方法
    // 1.使用 kzalloc 函数分配了一个 kobject 对象的内存
    skobject3 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
    // 2.初始化并添加到内核中,名为 "skobject3"。
    ret = kobject_init_and_add(skobject3, &stype, NULL, "%s", "skobject3");
    if(ret < 0)
    {
        PRTE("kobject_init_and_add fail!ret=%d\n", ret);
        goto err_kobject_init_and_add;
    }
    return 0;

err_kobject_init_and_add:
	return ret;
}

/**
 * @brief  sdriver_demo_exit()
 * @note   
 * @param [in]
 * @param [out]
 * @retval 
 */
static __exit void sdriver_demo_exit(void)
{
	
	// 释放之前创建的 kobject 对象
    kobject_put(skobject1);
    kobject_put(skobject2);
    kobject_put(skobject3);
    PRT("sdriver_demo module exit!\n");
}


module_init(sdriver_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sdriver_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_led_demo 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

我们执行 make 编译,最后就会得到 sdriver_demo.ko 文件。

2. 开发板验证

我们拷贝到开发板,然后加载驱动:

image-20250105161224342

然后我们就可以在/sys 目录下看到我们创建的 skobject1 和 skobject3,skobject2 位于 /sys/skobject1 目录下。卸载驱动模块的时候这几个目录都会被删除。

参考资料

【1】linux 驱动开发—— 6、linux 设备驱动模型_什么是 kobject-CSDN 博客

【2】Linux 设备模型剖析系列一(基本概念、kobject、kset、kobj_type)_device 下的 kobj-CSDN 博客

【3】Linux 设备模型(1)_基本概念

【4】一张图掌握 Linux platform 平台设备驱动框架!【建议收藏】-CSDN 博客

【5】关于 kobjects、ksets 和 ktypes 的一切你没想过需要了解的东西 — The Linux Kernel documentation

【6】【原创】linux 设备模型之 kset/kobj/ktype 分析 - LoyenWang - 博客园

【7】kobject / kset / ktype(linux kernel 中的面向对象) - 知乎