[翻译]你不会想知道的kobject,kset,和ktypes

时间:2022-12-04 08:27:58

-------------------------------------------------------------------------------------------------------------------------------

阿辉翻译自Linux内核文档:\linux-3.4.75\Documentation\kobject.txt

原创翻译,欢迎转载,转载请注明出处

-------------------------------------------------------------------------------------------------------------------------------

  理解kobject抽象和以此来建立的设备驱动模型的部分困难在于没有一个明显的切入点。

  而正确使用kobjects需要了解一些不同的类型,而这些类型是相互引用。为了更容易理解,我们将从一些模糊的概念开始,并逐步增加细节。为此,先来介绍几个我们会使用到术语的快速定义。

1、kobject是struct kobject类型的对象,它包含一个名字和引用计数,并且拥有一个父指针(这可以让对象组织成层次结构),并且kobject通常是sysfs虚拟文件系统中的表述。

2、 一般不会对kobject本身感兴趣,相反,kobject结构通常是被嵌入到其他结构中,而这些结构才包含了真正感兴趣的数据。

3、没有一个结构会嵌入多于一个kobject结构,如果这么做了,关于这个对象的引用计数肯定会一团糟,你的code也会充满bug,所以千万不要这么做。

4、ktype是内嵌kobject结构的对象的类型,每个内嵌kboject的结构都需要一个特定的ktype结构,ktype决定了kobject被创建和销毁时所采取的动作。

5、kset包含了一组kobject结构,这些kobject可以有相同或者不同的ktype。kset是一个基础的容器类型,它包含了kobject的集合。kset自身也内嵌了一个kobject结构,不过你可以不用关注这个实现的细节,因为kernel中的kset核心程序已经做好这些事情了。

6、当你看到某个sysfs目录下有很多其他目录,这些目录都对应着同样kset里包含的具体kobject

  我们会以自下到上的角度开始研究如何创建和操作这些类型;因此,我们将会从kobject开始

嵌入的kobject

  极少会需要kernel代码区创建一个单独的kboject结构,除了后面所述的一个显著例外。相反,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结构体,通过指向kobj成员就可以找到嵌入的kobject结构。然而,包含kobject的代码通常会有相反的困扰,如何根据kobject结构指针找到包含它的指针呢?不要特意取巧(比如假设kobject在该结构的开头),而应该使用<linux/kernel.h>中的contain_of()宏:

container_of(pointer, type, member)

对应参数的含义如下:

pointer:指向内嵌kobject的指针

type:包含kobject结构的结构体的指针

member:”pointer”指针的名字(比如前面uio_map结构中的kobj)

  container_of()宏的返回值是该容器类型的指针。举个例子,指针kp是kobject结构指针,内嵌于uio_map结构体,可以通过如下的代码得到得到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”是指向kobject结构的指针,随后宏就变成如下形式的调用:

struct uio_map *map = to_map(kobj);

kobject初始化

  创建kobject当然必须初始化kobject对象,kobject的一些内部成员需要(强制)通过kobject_init()初始化:

void kobject_init(struct kobject *kobj, struct kobj_type *ktype);

  ktyp对于kobject的正确创建是必须的,因为每个kobject必须关联一个kobj_type。

  在调用kobject_init将kobject注册到sysfs之后,必须调用kobject_add()添加:

  int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...);

  它正确设置了kobject的名称和父节点,如果这个kobject和一个特定的kset关联,则kobj->kset必须在调用kobject_add之前被指定。若kobject已经跟一个kset关联,在调用kobject_add时可以将这个kobject的父节点设置为NULL,这样它的父节点就会被设置为这个kset本身。

  在将kobject添加到kernel时,它的名称就已经被设定好了,代码中不应该直接操作kobject的名称。如果你必须为kobject改名,则调用kobject_rename()函数实现:

  int kobject_rename(struct kobject *kobj, const char *new_name);

  kobject_rename内部并不执行任何锁定操作,也没用何时名称是有效的概念。因此调用者必须自己实现完整性检查和保证序列性。有一个函数叫kobject_set_name(),但是它遗留了一些缺陷,目前正在被移除。如果你的代码需要调用这个函数,它是不正确的,需要被修正。可以通过kobject_name()函数来正确获取kobject的名称:

const char *kobject_name(const struct kobject * kobj);

  有一个辅助函数用来在同一时间初始化和添加kobject,它被称作令人惊讶的是kobject_init_and_add():

  int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,

struct kobject *parent, const char *fmt, ...);

  它的参数和前面介绍的单独使用kobject_init()、kobject_add()这两个函数时一样

Uevents

  在注册完kobject结构之后,你需要向世界宣布,kobject已经被创建。这可以通过kobject_uevent()函数来实现:

  int kobject_uevent(struct kobject *kobj, enum kobject_action action);

  若这个kobject是第一次被加到kernel里面,action参数应该设置为KOBJ_ADD。

  kobject_uevent只能在kobject的所有属性或者子节点正确初始化完成之后调用,因为用户空间会在这个函数调用时马上区查找它们。从kernel中移除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只是希望为你的结构提供一个引用计数,那么请使用kref来代替,kobject有点大材小用了。对于如何使用kref结构的信息,请参考Linux内核源码树中的Documentation/kref.txt这个文件。

创建简单的kobject

  有些时候,开发者希望的只是在sysfs中创建一个目录,并且不会被kset、show、store函数等细节的复杂概念所迷惑。这里有一个可以单独创建kobject的例外,为了创建这样一个入口,可以通过如下的函数来实现:

  struct kobject *kobject_create_and_add(char *name, struct kobject *parent);

  这个函数会创建一个kobject,并把它的目录放到指定父节点在sysfs中目录里面。可以通过如下的函数简单创建kobject相关联的属性:

  int sysfs_create_file(struct kobject *kobj, struct attribute *attr);

或者

  int sysfs_create_group(struct kobject *kobj, struct attribute_group *grp);

  这两种方法都包含了属性类型,和通过kobject_create_and_add()创建的kobject类型;属性类型可以直接使用kobj_attribute类型,所以不需要特别创建自定义属性。读者可以通过samples/kobject/kobject-example.c中的模块例程,学习kobject和属性的实现。

kytpes和释放方法

  前面的讨论中仍然缺失的一个重要部分是当一个kobject的引用计数为0时会发生什么。创建kobject的代码通常不知道什么时候会发生这件事,如果它知道,那在第一次使用kobject时使用引用计数就没有意义了。甚至,连生命周期可预测的对象也在sysfs被引入时变得更加复杂,因为内核的其他部分可能会引用任何在系统中已注册的kobject。

  结论就是:kobject所保护的结构体在它的引用计数没到0之前不可以被释放。引用计数并不由创建kobject的代码直接控制,因此这些代码必须在kobject的最后一个引用被释放时被异步的通知到。千万不能通过kfree直接去释放你通过kobject_add注册的kobject结构体,kobject_put才是唯一安全的方法。在kobject_init调用之后使用kobject_put来避免错误发生总是一个好的方法。

  通知的机制通过kobject的释放函数法实现。通常这种函数有如下的实现形式:

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必须有一个释放函数,并且这个kobject必须保持直到这个释放函数被调用到。如果这个条件不能被满足,则这个代码是有缺陷的。注意,假如你忘了提供释放函数,内核会提出警告的;不要尝试提供一个空的释放函数来消除这个警告,你会收到kobject维护者的无情嘲笑。

  注意,kobject的名称在释放函数里还是有效的,但是千万不要在释放函数里面修改kobject的名称,否则会导致kobject核心的内存泄漏,这会让人非常不高兴的。

有趣的是,释放函数并没有保存在kobject结构自身里面。相反, 它被关联到ktype里,让我们介绍下kobj_type这个结构体

struct kobj_type {

void (*release)(struct kobject *);

const struct sysfs_ops *sysfs_ops;

struct attribute        **default_attrs;

};

  这个结构体用于描述特定类型的kobject(或者,更准确的说是容器对象)。每个对象都必须关联一个kobj_type结构体,当你调用kobject_init和kobject_init_and_add时必须传入一个kobj_type的指针。

kobj_type结构里的release域当然是针对这个kobject类型的释放函数指针。其他两个成员(sysfs_ops和default_attrs)控制对象在sysfs中时如何表述的,这部分内容这篇文章不会介绍到。

当kobject使用ktype这个属性类型注册到系统时,default_attrs指针包含的默认属性列表会自动为该kobject创建出来。

kset

  kset仅仅是一系列相互关联的kobject的集合。并没有要求这些kobject有同样的ktype,但是如果没有同样的ktype,还是要非常小心才行

  kset的主要作用如下:

  作为一个包含一组对象的容器,kset可以被内核用来追踪”所有块设备”或者”所有PCI设备驱动”。

  kset也是sysfs中的子目录,所有跟kset相关联的kobject都会出现在这个目录里。每个kset都内嵌了一个kobject结构,它可以是其他kobject的父节点,sysfs中的*目录结构都是这么组织的。

  kset可以用于支持kobject的热插拔,并且对uevent事件如何上报到用户空间造成影响。

  若用面向对象思想来看,kset是*的容器类;kset包含一个自身的kobject,但是这个kobject由kset代码来管理,不应该被其他用户操作到。

  kset通过标准的内核链表来管理它的所有子节点,kobject通过kset成员变量指向它的kset容器。大多数情况下,kobject都是它所属kset(或者严格点,kset内嵌的kobject)的子节点。

  因为kset内嵌了一个kobject结构,它应该动态的被创建,而永远不要静态或者在栈上声明,可以通过如下的函数来创建一个新的kset:

   struct kset *kset_create_and_add(const char *name,

struct kset_uevent_ops *u,

struct kobject *parent);

  当你结束kset的使用时,通过下面的函数来销毁它:

  void kset_unregister(struct kset *kset);

  kset的一个例子可以在内核源码树的samples/kobject/kset-example.c文件中找到。

如果一个kset希望控制它所关联kobject的uevent操作,它可以使用kset_uevent_ops结构来处理:

struct kset_uevent_ops {

int (*filter)(struct kset *kset, struct kobject *kobj);

const char *(*name)(struct kset *kset, struct kobject *kobj);

int (*uevent)(struct kset *kset, struct kobject *kobj,

struct kobj_uevent_env *env);

};

  filter函数用于让kset决定一个特定kobject的uevent是否被传递到用户空间,若该函数返回0,则该uevent将会被阻止。

  name函数用于替代发送uevent到用户空间的kset的默认名字。默认情况下,该名称会和kset一致,但这个函数(name)存在的话,可以覆盖这个名字。

  当uevent要被发送到用户空间时,uevent函数会被调用到,该函数允许将更多环境变量添加到uevent中。

  你可能会问,一个kobject是如何添加到kset中去的,鉴于没有看到一个确切实现该功能的函数,答案是通过kobject_add函数来实现。当调用kobject_add来添加一个kobject时,需要传递一个指向该kobject所属的kset的kset参数,kobject_add会处理剩下的事情。

  如果kobject没有设置父节点,它将会被添加到kset的目录底下。并不是kset的所有成员都会在kset目录底下。如果在kobject添加之前就确认了父节点,在注册到kset时会添加到父节点的目录底下。

删除kobject

  在kobject核心成功注册之后,必须在代码完成时清理掉这个kobject,可以通过kobject_put来完成。通过这个函数,kobject核心会自动清除为该kobject分配的所有内存。若已经为该对象发送过KOBJ_ADD这个uevent,对应的KOBJ_REMOVE uevent也在清理时被发送。另外,sysfs的其他清理工作也会为这个调用者启动。

如果你需要完成kobject的二级删除(表示销毁对象时不允许睡眠),可以尝试通过kobject_del从sysfs中注销kobject。这个函数会让kobject在sysfs中不可见,但此时它并没有被清理掉,引用计数还是不变。可以在后续的代码中调用kobject_put来完成清理kobject相关的内存。

若创建了循环引用,可以通过kobject_del函数来去掉对父节点的引用。这种方法在某些情况下是合法的,比如父节点引用到了子节点。循环引用必须通过一个明确的kobject_del调用来去除,这样释放函数将会被调用到,并且之前的循环引用将会释放掉。

kobject/kset例程

可以通过samples/kobject/{kobject-example.c,kset-example.c}中的例程学习如何正确的使用kset和kobject。这些例程可以通过选择kernel的配置选项CONFIG_SAMPLE_KOBJECT来把它编译成模块实现。