关于 kobject、kset 和 ktype 你从不想知道的一切¶
- 作者:
Greg Kroah-Hartman <gregkh@linuxfoundation.org>
- 最后更新:
2007年12月19日
基于 Jon Corbet 为 lwn.net 撰写的原创文章,该文章于 2003 年 10 月 1 日发布,位于 https://lwn.net/Articles/51437/
理解驱动程序模型(以及构建在其上的 kobject 抽象)的难点之一在于,没有明显的起点。处理 kobject 需要理解几种不同的类型,所有这些类型都相互引用。为了使事情更容易,我们将采取多步骤方法,从模糊的术语开始,并随着我们的进行添加细节。为此,这里有一些我们将要使用的术语的快速定义。
kobject 是 struct kobject 类型的对象。kobject 有一个名称和一个引用计数。kobject 还有一个父指针(允许将对象排列成层次结构)、一个特定类型,通常还有一个在 sysfs 虚拟文件系统中的表示。
kobject 本身通常并不有趣;相反,它们通常嵌入在一些其他结构中,这些结构包含代码真正感兴趣的内容。
任何结构都**绝对不能**在其内部嵌入多个 kobject。如果这样做,则该对象的引用计数肯定会混乱且不正确,并且您的代码将存在错误。所以不要这样做。
ktype 是嵌入 kobject 的对象的类型。每个嵌入 kobject 的结构都需要一个相应的 ktype。ktype 控制 kobject 在创建和销毁时会发生什么。
kset 是一组 kobject。这些 kobject 可以属于同一 ktype 或属于不同的 ktype。kset 是 kobject 集合的基本容器类型。kset 包含它们自己的 kobject,但是您可以安全地忽略该实现细节,因为 kset 核心代码会自动处理此 kobject。
当您看到一个 sysfs 目录中充满了其他目录时,通常每个目录都对应于同一 kset 中的一个 kobject。
我们将了解如何创建和操作所有这些类型。我们将采取自下而上的方法,因此我们将回到 kobject。
嵌入 kobject¶
内核代码很少创建独立的 kobject,下面将解释一个主要的例外情况。相反,kobject 用于控制对更大的、特定于域的对象的访问。为此,kobject 将嵌入到其他结构中。如果您习惯于以面向对象的术语思考问题,则 kobject 可以被视为顶级的抽象类,其他类都从中派生。kobject 实现了一组本身没有特别用处,但对于其他对象来说很好的功能。C 语言不允许直接表达继承,因此必须使用其他技术(例如结构嵌入)。
(顺便说一句,对于那些熟悉内核链表实现的人来说,这类似于“list_head”结构很少单独使用,而是总是嵌入在感兴趣的更大的对象中。)
因此,例如,drivers/uio/uio.c
中的 UIO 代码有一个结构定义了与 uio 设备关联的内存区域
struct uio_map {
struct kobject kobj;
struct uio_mem *mem;
};
如果您有一个 struct uio_map 结构,则查找其嵌入的 kobject 只是使用 kobj 成员的问题。但是,使用 kobject 的代码通常会遇到相反的问题:给定一个 struct kobject 指针,指向包含结构的指针是什么?您必须避免技巧(例如假设 kobject 位于结构的开头),而是使用 <linux/kernel.h>
中找到的 container_of() 宏
container_of(ptr, type, member)
其中
ptr
是指向嵌入的 kobject 的指针,
type
是包含结构的类型,并且
member
是pointer
指向的结构字段的名称。
container_of() 的返回值是指向相应容器类型的指针。因此,例如,指向嵌入在 struct uio_map **内** 的 struct kobject 的指针 kp
可以使用以下方法转换为指向 **包含的** uio_map 结构的指针
struct uio_map *u_map = container_of(kp, struct uio_map, kobj);
为了方便起见,程序员通常定义一个简单的宏来将 kobject 指针**向后转换**为包含的类型。正如您在此处看到的那样,早期的 drivers/uio/uio.c
中就是这样做的
struct uio_map {
struct kobject kobj;
struct uio_mem *mem;
};
#define to_map(map) container_of(map, struct uio_map, kobj)
其中宏参数“map”是指向相关 struct kobject 的指针。该宏随后使用以下方式调用
struct uio_map *map = to_map(kobj);
kobject 的初始化¶
创建 kobject 的代码当然必须初始化该对象。一些内部字段通过(强制)调用 kobject_init()
设置
void kobject_init(struct kobject *kobj, const struct kobj_type *ktype);
创建 kobject 需要 ktype,因为每个 kobject 都必须有一个关联的 kobj_type。在调用 kobject_init()
后,要向 sysfs 注册 kobject,必须调用函数 kobject_add()
int kobject_add(struct kobject *kobj, struct kobject *parent,
const char *fmt, ...);
这将正确设置 kobject 的父级和 kobject 的名称。如果要将 kobject 与特定的 kset 相关联,则必须在调用 kobject_add()
之前分配 kobj->kset。如果 kset 与 kobject 相关联,则可以在调用 kobject_add()
时将 kobject 的父级设置为 NULL,然后 kobject 的父级将是 kset 本身。
由于 kobject 的名称在添加到内核时设置,因此永远不应直接操作 kobject 的名称。如果必须更改 kobject 的名称,请调用 kobject_rename()
int kobject_rename(struct kobject *kobj, const char *new_name);
kobject_rename()
不会执行任何锁定,也没有关于哪些名称有效的明确概念,因此调用者必须提供他们自己的健全性检查和序列化。
有一个名为 kobject_set_name()
的函数,但这是一个遗留的垃圾,正在被删除。如果您的代码需要调用此函数,则说明它不正确,需要修复。
要正确访问 kobject 的名称,请使用函数 kobject_name()
const char *kobject_name(const struct kobject * kobj);
有一个辅助函数可以同时初始化 kobject 并将其添加到内核,这个辅助函数非常令人惊讶地称为 kobject_init_and_add()
int kobject_init_and_add(struct kobject *kobj, const struct kobj_type *ktype,
struct kobject *parent, const char *fmt, ...);
参数与上面描述的单独的 kobject_init()
和 kobject_add()
函数相同。
Uevent¶
在 kobject 注册到 kobject 核心后,您需要向外界宣布它已被创建。这可以通过调用 kobject_uevent() 来完成
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
当 kobject 首次添加到内核时,请使用 **KOBJ_ADD** 操作。只有在正确初始化 kobject 的任何属性或子级后才能执行此操作,因为当发生此调用时,用户空间将立即开始查找它们。
当 kobject 从内核中删除时(有关如何执行此操作的详细信息如下),kobject 核心将自动创建 **KOBJ_REMOVE** 的 uevent,因此调用者不必担心手动执行此操作。
引用计数¶
kobject 的关键功能之一是充当嵌入它的对象的引用计数器。只要对对象的引用存在,该对象(以及支持它的代码)就必须继续存在。用于操作 kobject 的引用计数的底层函数是
struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);
成功调用 kobject_get()
将增加 kobject 的引用计数器并返回指向 kobject 的指针。
当引用被释放时,调用 kobject_put()
将会递减引用计数,并可能释放该对象。请注意,kobject_init()
将引用计数设置为 1,因此设置 kobject 的代码最终需要执行 kobject_put()
来释放该引用。
由于 kobject 是动态的,它们不能静态声明或在堆栈上声明,而应始终动态分配。内核的未来版本将包含对静态创建的 kobject 的运行时检查,并会警告开发者这种不当用法。
如果只想使用 kobject 为您的结构体提供引用计数器,请改用 struct kref;使用 kobject 有点过度了。有关如何使用 struct kref 的更多信息,请参阅 Linux 内核源代码树中的文件 向内核对象添加引用计数器 (krefs)。
创建“简单” kobject¶
有时,开发者仅仅想要一种在 sysfs 层级中创建简单目录的方法,而不必处理 kset、show 和 store 函数以及其他细节的复杂性。这是应该创建单个 kobject 的一个例外。要创建这样的条目,请使用以下函数:
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent);
此函数将创建一个 kobject,并将其放置在 sysfs 中指定父 kobject 下面的位置。要创建与此 kobject 关联的简单属性,请使用:
int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
或者:
int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp);
这里使用的两种类型的属性(通过 kobject_create_and_add()
创建的 kobject),都可以是 kobj_attribute 类型,因此不需要创建特殊的自定义属性。
有关简单 kobject 和属性的实现,请参见示例模块 samples/kobject/kobject-example.c
。
ktype 和释放方法¶
讨论中仍然缺少的一个重要内容是,当 kobject 的引用计数达到零时会发生什么。创建 kobject 的代码通常不知道这种情况何时发生;如果知道,那么一开始使用 kobject 就没有什么意义了。当引入 sysfs 时,即使是可预测的对象生命周期也会变得更加复杂,因为内核的其他部分可以获取系统中任何已注册 kobject 的引用。
最终结果是,在引用计数变为零之前,受 kobject 保护的结构体不能被释放。引用计数不受创建 kobject 的代码的直接控制。因此,每当最后一个对其 kobject 的引用消失时,该代码必须被异步通知。
一旦您通过 kobject_add()
注册了您的 kobject,您绝不能直接使用 kfree()
来释放它。唯一安全的方法是使用 kobject_put()
。 建议在 kobject_init()
之后始终使用 kobject_put()
,以避免出现错误。
此通知是通过 kobject 的 release() 方法完成的。通常,这种方法的形式如下:
void my_object_release(struct kobject *kobj)
{
struct my_object *mine = container_of(kobj, struct my_object, kobj);
/* Perform any additional cleanup on this object, then... */
kfree(mine);
}
一个重要的点怎么强调都不过分:每个 kobject 都必须有一个 release() 方法,并且 kobject 必须持续存在(处于一致的状态),直到调用该方法为止。如果未满足这些约束,则代码有缺陷。请注意,如果您忘记提供 release() 方法,内核会警告您。请勿尝试通过提供“空”释放函数来消除此警告。
如果您的清理函数需要做的只是调用 kfree()
,那么您必须创建一个包装函数,该函数使用 container_of() 向上转型到正确的类型(如上面的示例所示),然后在整个结构体上调用 kfree()
。
请注意,kobject 的名称在 release 函数中可用,但不能在此回调中更改。否则,kobject 核心会发生内存泄漏,这会让人不愉快。
有趣的是,release() 方法本身不存储在 kobject 中;相反,它与 ktype 相关联。因此,让我们介绍一下 struct kobj_type:
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops;
const struct attribute_group **default_groups;
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
void (*get_ownership)(struct kobject *kobj, kuid_t *uid, kgid_t *gid);
};
此结构用于描述特定类型的 kobject(或更准确地说,是包含对象)。每个 kobject 都需要有一个关联的 kobj_type 结构;当您调用 kobject_init()
或 kobject_init_and_add()
时,必须指定指向该结构的指针。
struct kobj_type 中的 release 字段当然是指向此类型 kobject 的 release() 方法的指针。另外两个字段(sysfs_ops 和 default_groups)控制此类型的对象在 sysfs 中的表示方式;它们超出了本文档的范围。
default_groups 指针是一个默认属性列表,该列表将为使用此 ktype 注册的任何 kobject 自动创建。
kset¶
kset 只是想要彼此关联的 kobject 的集合。它们不一定必须是相同的 ktype,但如果不是,请非常小心。
kset 提供以下功能:
它充当一个包含一组对象的容器。内核可以使用 kset 来跟踪“所有块设备”或“所有 PCI 设备驱动程序”。
kset 也是 sysfs 中的一个子目录,与该 kset 关联的 kobject 可以在其中显示。每个 kset 都包含一个 kobject,该 kobject 可以设置为其他 kobject 的父对象;sysfs 层级的顶层目录就是以这种方式构建的。
Kset 可以支持 kobject 的“热插拔”,并影响向用户空间报告 uevent 事件的方式。
在面向对象的术语中,“kset”是顶层容器类;kset 包含自己的 kobject,但该 kobject 由 kset 代码管理,任何其他用户都不应操作它。
kset 将其子项保存在标准内核链表中。Kobject 通过其 kset 字段指回其包含的 kset。在几乎所有情况下,属于 kset 的 kobject 在其父对象中都具有该 kset(或严格来说,是其嵌入的 kobject)。
由于 kset 在其中包含一个 kobject,因此应始终动态创建,而绝不能静态声明或在堆栈上声明。要创建新的 kset,请使用:
struct kset *kset_create_and_add(const char *name,
const struct kset_uevent_ops *uevent_ops,
struct kobject *parent_kobj);
当您完成 kset 的操作后,请调用:
void kset_unregister(struct kset *k);
来销毁它。这将从 sysfs 中删除 kset 并递减其引用计数。当引用计数变为零时,将释放 kset。由于可能仍然存在对 kset 的其他引用,因此释放可能会在 kset_unregister()
返回之后发生。
在内核树中的 samples/kobject/kset-example.c
文件中可以看到使用 kset 的示例。
如果 kset 希望控制与其关联的 kobject 的 uevent 操作,则可以使用 struct kset_uevent_ops 来处理它:
struct kset_uevent_ops {
int (* const filter)(struct kobject *kobj);
const char *(* const name)(struct kobject *kobj);
int (* const uevent)(struct kobject *kobj, struct kobj_uevent_env *env);
};
filter 函数允许 kset 防止为特定 kobject 向用户空间发出 uevent。如果该函数返回 0,则不会发出 uevent。
name 函数将在 uevent 发送给用户空间时被调用,以覆盖 kset 的默认名称。默认情况下,该名称将与 kset 本身相同,但如果存在此函数,则可以覆盖该名称。
uevent 函数将在 uevent 即将发送到用户空间时调用,以允许将更多环境变量添加到 uevent 中。
有人可能会问,考虑到没有提供执行该功能的函数,如何准确地将 kobject 添加到 kset 中。答案是此任务由 kobject_add()
处理。当将 kobject 传递给 kobject_add()
时,其 kset 成员应指向该 kobject 将属于的 kset。kobject_add()
将处理其余部分。
如果属于 kset 的 kobject 没有设置父 kobject,则会将其添加到 kset 的目录中。并非 kset 的所有成员都一定存在于 kset 目录中。如果在添加 kobject 之前分配了显式父 kobject,则该 kobject 将在 kset 中注册,但会被添加到父 kobject 下。
Kobject 删除¶
在 kobject 成功注册到 kobject 核心后,当代码完成对其的操作时,必须将其清除。为此,请调用 kobject_put()
。通过这样做,kobject 核心将自动清除此 kobject 分配的所有内存。如果已为对象发送了 KOBJ_ADD
uevent,则将发送相应的 KOBJ_REMOVE
uevent,并且将为调用者正确处理任何其他 sysfs 内务处理。
如果您需要对 kobject 执行两阶段删除(例如,当您需要销毁该对象时不允许休眠),则调用 kobject_del()
,这将从 sysfs 中取消注册该 kobject。这会使 kobject“不可见”,但不会清除它,并且对象的引用计数仍然相同。稍后调用 kobject_put()
以完成与 kobject 关联的内存的清理。
如果构建了循环引用,可以使用kobject_del()
来删除对父对象的引用。 在某些情况下,父对象引用子对象是有效的。 循环引用_必须_通过显式调用kobject_del()
来打破,这样才能调用释放函数,并且之前循环中的对象才能互相释放。
可供复制的示例代码¶
有关正确使用 kset 和 kobject 的更完整示例,请参阅示例程序 samples/kobject/{kobject-example.c,kset-example.c}
,如果您选择 CONFIG_SAMPLE_KOBJECT
,它们将被构建为可加载模块。