本文会围绕kobject、ktype和kset三个概念进行介绍,我们先大概了解一下相关概念以及它们之间的关系:
kobject在内核中应用最多的就是设备驱动模型————总线、设备、驱动、类的管理都使用了kobject,但是kobject并不只为设备驱动模型服务,它是内核中的通用对象模型,用来为内核中各部分的对象管理提供统一视图,其实现在内核的lib/目录下。
kobject一般都不会单独使用,这样是没有意义的,它总是内嵌到其他结构体中,例如字符设备的定义:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
我们说kobject只是通用对象的表示,其自身不包含任何特定于某个模块的属性,因此我们更关心的是kobject的外围结构。每个外围结构体所在的模块都应该定义一个ktype,这个ktype就完成了由通用向特殊的过渡。因此kobject可看做其他所有外围结构的基类,它抽象出一些通用的属性(如引用计数)以及通用接口,而每个外围结构各自完成这些接口的内部实现,这里使用内嵌的方式来体现这种继承关系。
使用内嵌而不用指针也是为了方便通过kobject来反向定位其外围结构的实例,通过我们熟知的container_of()宏便可以做到。由于kobject是系统统一管理的,因此先找到kobject对象进而跟踪到其代表的具体对象是很常见的做法。
kset是一个基本的容器类,它是一组kobject的集合。当我们想统一管理某些有类似属性的kobjects时,可以将它们加入到一个集合中,这个集合的作用是,当一个事件发生时,可以同时通知到集合中的所有kobjects。
本文基于Linux内核版本3.10.27(kobject在2.6内核中就被引入了)。
kobject
kobject在内核中不会单独出现,并且散落在内核的各个角落,因此很难追踪到每一个kobject对象。好在 Linux内核将所有的kobjects与虚拟文件系统sysfs紧密结合起来,这样就让所有kobjects可视化和层次化。kobject描述了sysfs虚拟文件系统中的层级结构,一个kobject对象就对应了sysfs中的一个目录,而sysfs中的目录结构也体现在各个kobjects之间的父子关系。
kobject由struct kobject定义:
#include <linux/kobject.h>
struct kobject {
const char *name; /* kobject对象的名字,对应sysfs中的目录名 */
struct list_head entry; /* 在kset中的链表节点 */
struct kobject *parent; /* 用于构建sysfs中kobjects的层次结构,指向父目录 */
struct kset *kset; /* 所属kset */
struct kobj_type *ktype; /* 特定对象类型相关,用于跟踪object及其属性 */
struct sysfs_dirent *sd; /* 指向该目录的dentry私有数据 */
struct kref kref; /* kobject的引用计数,初始值为1 */
unsigned int state_initialized:1; /* kobject是否初始化,由kobject_init()设置 */
unsigned int state_in_sysfs:1; /* 是否已添加到sysfs层次结构中 */
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1; /* 是否忽略uevent事件 */
};
新建一个kobject对象需要两步:初始化struct kobject结构,添加到sysfs目录结构中。
1) 初始化一个kobject对象:
void kobject_init(struct kobject *kobj, struct kobj_type *ktype);
这个函数需要传入两个参数,kobj和ktype必须不为NULL,由于kobj都是嵌入到其他结构体,所以一般传kobj参数的方式形如&pcdev->kobj。
该函数即完成对kobj的初始化:
初始化kobj->kref引用计数为初始值1;
初始化kobj->entry空链表头;
kobj->ktype = ktype;
然后将kobj->state_initialized置为1,表示该kobject已初始化。
2) 初始化之后,通过kobject_add()将kobj添加到系统中:
int kobject_add(struct kobject *kobj, struct kobject *parent,
const char *fmt, ...);
这个函数给kobj指定一个名字,这个名字也就是其在sysfs中的目录名,parent用来指明kobj的父节点,即指定了kobj的目录在sysfs中创建的位置。如果这个kobj要加入到一个特定的kset中,则在kobject_add()必须给kobj->kset赋值,此时parent可以设置为NULL,这样kobj会自动将kobj->kset对应的对象作为自己的parent。如果parent设置为NULL,且没有加入到一个kset中,kobject会被创建到/sys顶层目录下。
设置对象的名字是通过下面的接口完成的:
int kobject_set_name(struct kobject *kobj, const char *fmt, ...);
这个函数给kobj的name成员赋值。注意这里面是通过kmalloc给name分配内存的。相应的获取一个kobject对象的名字的接口为:
const char *kobject_name(const struct kobject * kobj);
如果在添加kobj之后要给kobj改名字,则使用kobject_rename()接口,而不要直接去操作kobj->name,因为涉及到内存重新分配以及sysfs中目录的改变。
也可以通过下面的接口来一次性完成kobject_init()和kobject_add()过程:
int kobject_init_and_add(struct kobject *kobj,
struct kobj_type *ktype, struct kobject *parent,
const char *fmt, ...);
参数含义和上述接口相同。
ktype
ktype是由struct kobj_type来定义的,
#include <linux/kobject.h>
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
};
kobj_type都必须实现release方法,该方法由kobject的外层结构来定义,用来释放特定模块相关的kobject资源。
default_attrs定义了一系列默认属性,default_attrs是一个二级指针,可以对每个kobject设置多个默认属性(最后一个属性用NULL填充)。
struct attribute {
const char *name; /* 属性名字 */
umode_t mode; /* 用户访问模式,在<linux/stat.h>中定义 */
};
例如:
static struct attribute *i2c_adapter_attrs[] = {
&dev_attr_name.attr,
&dev_attr_new_device.attr,
&dev_attr_delete_device.attr,
NULL
};
可见,kobj_type是由具体模块定义的,每一个属性都对应着kobject目录下的一个文件,这样可以在用户态通过读写属性文件,来完成对该属性值的读取和更改。
而sysfs_ops定义了属性的操作:
struct sysfs_ops {
ssize_t (*show)(struct kobject *, struct attribute *,char *);
ssize_t (*store)(struct kobject *,struct attribute *,const char *, size_t);
const void *(*namespace)(struct kobject *, const struct attribute *);
};
也就是说,default_attrs数组中所有属性的操作都是由sysfs_ops来完成的。
在kobject的容器(外围结构)中,一般会定义xxx_add_attrs()和xxx_remove_attrs()提供增加和删除额外属性的接口。例如对于bus_ktype,默认属性是空的,因此我们看到/sys/bus/目录下是没有属性文件的,而每个子目录的属性可能不同,因此都需要动态地去定义属性列表并据此创建属性文件。动态属性一般通过__ATTR()宏来定义。
创建属性文件最终都是通过sysfs_create_file()或sysfs_create_group()来完成的。其中sysfs_create_group(kobj, grp)用来创建一组属性文件,需要定义struct attribute_group结构的属性集,这里需要说明的是,其中name成员如果为NULL,则直接在kobj目录下创建各个属性文件,如果name不为NULL,则会创建一个名为name的目录,然后在该目录下创建各个属性文件。
struct attribute_group {
const char *name;
umode_t (*is_visible)(struct kobject *,
struct attribute *, int);
struct attribute **attrs;
};
引用计数 —— kref
kref成员是object对象的引用计数,初始值为1,通过kref_get()和kref_put()可对该计数进行增减操作。kref_get()和kref_put()是内核中通用的引用计数的操作,针对kobject,使用下面两个封装函数:
struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);
为方便跟踪一个kobject,在使用kobject之前,都要使用kobject_get()将参数kobj的kref加1,该函数返回kobj自身的指针。kobject_put()则将kref减1,当kref减到0时,则kobject_release()被调用来释放kobject资源。
kobject_release()会调用ktype的release方法做实际的释放操作,并释放为name分配的内存。如果有必要,还会向用户态发送KOBJ_REMOVE事件,并通过kobject_del()将kobject从sysfs中删除。
与sysfs关联 —— sysfs_dirent
读写sysfs中的文件(即每个attribute)的操作函数集为sysfs_file_operations:
const struct file_operations sysfs_file_operations = {
.read = sysfs_read_file,
.write = sysfs_write_file,
.llseek = generic_file_llseek,
.open = sysfs_open_file,
.release = sysfs_release,
.poll = sysfs_poll,
};
在读写sysfs的文件时,是通过文件的dentry找到和sysfs关联的struct sysfs_dirent结构,sysfs中每一个目录或文件都有其对应的一个struct sysfs_dirent结构的实例,而sysfs_dirent的s_parent成员用于构建sysfs中的层次结构。
那么,对于一个属性文件的sysfs_dirent结构,可通过其s_parent找到其所在目录的kobject实例,进而找到文件操作对应的kobj->ktype->sysfs_ops方法集合。相应的,kobject的sd成员即指向该目录对应的sysfs_dirent实例。
在读写一个sysfs里的属性文件时,其中read操作会去调用其所在目录对应的kobject的kobj->ktype->sysfs_ops的show方法完成属性读取,write操作会去调用kobj->ktype->sysfs_ops的store方法完成属性写入并使其生效。open/release方法用于打开/关闭文件。poll用来探测文件内容是否改变(如果相应kobject实现了poll/select的事件通知),poll方法返回POLLERR|POLLPRI事件,如果发现文件内容有变则需要重新打开文件或seek到文件头并重新读取才能读到新的内容。
实际上,上述sysfs_ops的show和store方法也是简单的包裹函数,实际调用了具体模块的xxx_attribute的show和store方法(在实现一个属性的操作时,如果是很简单的属性读取和配置,可以复用struct kobj_attribute,这种属性一般只是一个整型或字符串,如果是比较复杂的模块和属性,则可自定义形如xxx_attribute的结构及其show/store方法)。
Uevents
在一个kobject的状态变化时(新注册、注销、重命名等),都会广播出一个对应的事件通知(通常用户态会去接收并处理),通知事件的接口为:
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
或
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp_ext[]);
第二个接口可以传递额外的环境变量(“额外”的意思是说,即使envp_ext为NULL,也会传递基本的”ACTION=%s”、”DEVPATH=%s”、”SUBSYSTEM=%s”、”SEQNUM=%llu”)。action的可取值如下定义:
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_MAX
};
这些动作在发送到用户态时是通过字符串来表达的,其对应关系为:
static const char *kobject_actions[] = {
[KOBJ_ADD] = "add",
[KOBJ_REMOVE] = "remove",
[KOBJ_CHANGE] = "change",
[KOBJ_MOVE] = "move",
[KOBJ_ONLINE] = "online",
[KOBJ_OFFLINE] = "offline",
};
kset
除了sysfs中的目录层次结构之外,还有一个集合的概念,内核将相互关联的kobjects放到一个kset中统一管理(一个kset中并没有要求其中每个kobject的ktype必须相同,但正常情况下总是相同的)。
struct kset {
struct list_head list; /* 其成员列表 */
spinlock_t list_lock;
struct kobject kobj;
const struct kset_uevent_ops *uevent_ops; /* 扩展的事件处理 */
};
使用kset可以统一管理某些kobjects,方便查找和遍历,kobject的entry成员将所有的同一集合中的成员连接起来。
另外我们看到,一个kset自身在内核中也是一个kobject对象,因此,一个kset在sysfs中也对应着一个目录,这个kset的kobject可以作为其子目录的parent,sysfs顶层目录的bus/、devices/等目录就是这样创建的。通常,一个目录下的所有子目录都是属于同一个kset的,例如/sys/bus/目录下的所有子目录都属于全局的bus_kset。
kset的作用还在于可以对kobject的uevent事件的默认动作做一些扩展,上面讲到的kobject_uevent_env()函数中,如果发现kobj属于某个kset,则还会继续调用其kset的uevent_ops,
struct kset_uevent_ops {
int (* const filter)(struct kset *kset, struct kobject *kobj);
const char *(* const name)(struct kset *kset, struct kobject *kobj);
int (* const uevent)(struct kset *kset, struct kobject *kobj,
struct kobj_uevent_env *env);
};
这个结构体中,filter方法是一个过滤规则,用于判断是否将uevent发出去,可使用户态忽略某些事件。name方法用于获得特定子系统的名字传递给用户态,可以用来覆盖默认的名字(默认为kset的名字)。uevent方法即用于完成扩展的事件通知动作,例如对于”设备”的kset,事件中除了携带通用的环境变量,还需要携带MAJOR、MIMOR、DEVNAME等变量发往用户态。
创建一个新的kset的接口如下:
struct kset *kset_create_and_add(const char *name,
const struct kset_uevent_ops *uevent_ops,
struct kobject *parent_kobj)
{
struct kset *kset;
int error;
/* 创建一个kset */
kset = kset_create(name, uevent_ops, parent_kobj);
if (!kset)
return NULL;
/* 将kset->obj注册到sysfs中 */
error = kset_register(kset);
if (error) {
kfree(kset);
return NULL;
}
return kset;
}
其过程有两步,首先通过kset_create()创建kset对象,为其成员赋初始值,此时这个kset的kobj未加入任何kset,kobj的ktype也是默认的kset_ktype,值得一提的是,kset的名字也是其kobject对象的名字。然后通过kset_register()将kset->kobj注册到sysfs中(创建目录和属性文件),并广播KOBJ_ADD消息。
如果要使新创建的kset加入一个存在的kset,或使用自定义的ktype,则需要在外层模块为kset->kobj初始化好之后,直接调用kset_register()。
相应的,销毁一个kset的函数为kset_unregister(struct kset *kset);
如何将一个kobject加入到一个kset中呢?kobj_kset_join()和kobj_kset_leave()完成加入和移除的操作,但这两个接口不对外开放,实际上,kobj从一个kset中加入和移除的操作包含在kobject_add()和kobject_del()中,因此在创建一个kobject对象时,如果想让其加入某个kset,就要在kobject_add()之前指定。
还有两点需要注意:
1) 如果一个kobject的parent为NULL,那么,如果其加入了某个kset,则指定其父对象为kset的object,此时,这个kobject创建在kset的object目录下。
2) 如果一个kobject的parent不为NULL,又加入了某个kset,此时,这个kobject创建在parent的目录下。这样一来,就可能出现一个对象加入了某个kset,但不在这个kset的目录(或子目录)下。
下图展示了在sysfs视图中,kset、kobject、attribute的关系。图中绿色线条构成了sysfs中的目录结构,可见图中有5层目录。黑色线条体现集合的包含关系,图中有两个kset。橙色实体是一个目录下的属性文件。
kobject和kset的demo可参考内核源码中的samples/kobject/。