Linux Device Driver Part I. kobject/ktype/kset & sysfs

文中代码基于Linux 5.1 rc6版本

Introduction to kobject

Linux Device Model的基本构成元素是kobject,通常每个kobject对应于sysfs(/sys)中的一个目录,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct kobject {
const char *name; /* sysfs中的目录名 */
struct list_head entry; /* 同一kset下的kobject构成一个链表 */
struct kobject *parent; /* 父目录的kobject对象 */
struct kset *kset; /* 所属的kobject集合(kset),可以不设置 */
struct kobj_type *ktype; /* 提供目录中的文件、namespace、ownership等 */
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; /* 是否在sysfs中注册完毕 */
unsigned int state_add_uevent_sent:1; /* 是否发送过KOBJ_ADD uevent消息 */
unsigned int state_remove_uevent_sent:1; /* 是否发送过KOBJ_REMOVE uevent消息 */
unsigned int uevent_suppress:1; /* 是否禁止uevent的发送 */
};

首先,kobject提供引用计数功能(通过kobj->kref),用户应使用kobject_get/kobject_put获取、释放kobject的引用,当引用计数降到0时,则会调用kobj->ktype->release进行析构工作(注意析构函数定义在ktype中,因此kobject必须设置其ktype)。

使用kobject,实际上就是将其初始化并加入sysfs的过程:

  1. 通过kobject_init(kobj, ktype)初始化kobj对象,此时其引用计数为1,kobj->ktype设置为传入的ktype
  2. 通过kobjct_add(kobj, parent, fmt, ...),将kobject加入sysfs
    • 其在sysfs中的目录名称为kobj->name,通过类似于printk(fmt, ...)的方式设置
    • 其父目录如下决定:
      • 若传入的parent非空,则令kobj->parent = parent,此时其父目录为parent对应的目录
      • 否则,若kobj->kset非空,则令kobj->parent = kobj->kset->kobj,此时其父目录为kobj->kset对应的目录
      • 两者都不满足,则令kobj->parent = NULL,并将其置于/sys根目录下

也可以通过kobject_init_and_add(kobj, ktype, parent, fmt, ...)一次性完成上述两步操作。

反之,可以通过kobject_del将kobject目录从sysfs中移除,不过一般不需要手动调用,当引用计数降到0时系统会自动调用kobject_del

ktype

上面介绍了如何将kobject初始化并注册为sysfs中的目录,但没有解释kobject目录下的文件从何而来。实际上,它们来自ktype中的属性(attribute),每一个属性就相当于kobject目录下的一个文件。

ktype(struct kobj_type)的定义如下:

1
2
3
4
5
6
7
8
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);
void (*get_ownership)(struct kobject *kobj, kuid_t *uid, kgid_t *gid);
};

其中default_attrs是一个struct attribute*的数组,代表kobject目录下的文件,每个属性有其文件名和权限。此外,我们还可以使用sysfs_create_file/sysfs_remove_file动态地添加、删除新的属性:

1
2
3
4
5
6
7
8
9
struct attribute {
const char *name;
umode_t mode;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
bool ignore_lockdep:1;
struct lock_class_key *key;
struct lock_class_key skey;
#endif
};

只读属性通常权限设置为S_IRUGO即0444,可读写属性通常权限设置为S_IRUGO | S_IWUSR即0644。

我们可以通过ktype的sysfs_ops提供对属性的访问,只要实现相应的回调函数即可,sysfs会自动为我们实现open、read、write等系统调用:

1
2
3
4
struct sysfs_ops {
ssize_t (*show)(struct kobject *, struct attribute *, char *);
ssize_t (*store)(struct kobject *, struct attribute *, const char *, size_t);
};

为了在showstore中区分一个kobject下的不同属性,通常会将struct attribute嵌入在其他struct中(例如struct device_attribute),然后在该struct中提供showstore函数。这样,我们就可以直接在每个属性上定义回调,而不必在ktype的回调中用一个大switch来实现每个属性。

sysfs的发明就是为了改善procfs中混乱的文件结构和内容,因此每个属性的内容应当只有一行,通常只是一个数值或一个字符串(理论上你也可以不这么做,但并不推荐违反Linux社区的约定)。对于要传输二进制内容的特殊情况,sysfs提供了struct bin_attribute

1
2
3
4
5
6
7
8
9
10
11
struct bin_attribute {
struct attribute attr;
size_t size;
void *private;
ssize_t (*read)(struct file *, struct kobject *, struct bin_attribute *,
char *, loff_t, size_t);
ssize_t (*write)(struct file *, struct kobject *, struct bin_attribute *,
char *, loff_t, size_t);
int (*mmap)(struct file *, struct kobject *, struct bin_attribute *attr,
struct vm_area_struct *vma);
};

我们可以通过sysfs_create_bin_file/sysfs_remove_bin_file动态地添加或删除二进制属性,但不能静态地设置,因为静态设置的属性实际上在kobject_add内部是通过sysfs_create_file注册的。

这里的show, store, read, write传入的buffer都只有4K大小,故普通属性最大不能超过4K,二进制属性一次只能读写4K,若文件很大要分多次读写。此外对于二进制属性,上层的驱动要自己设法判断文件已经写入完毕,不会再有下一次4K写入,sysfs层不会给予任何提示。

另外,我们还可以通过sysfs_create_link(kobj, target, name),在kobj目录下创建一个symbolic link,指向target这个kobject的目录。

sysfs还提供了attribute group功能,实际上就是将一组属性打包成一个struct attribute_group一次性注册及注销:

1
2
3
4
5
6
7
8
9
struct attribute_group {
const char *name;
umode_t (*is_visible)(struct kobject *,
struct attribute *, int);
umode_t (*is_bin_visible)(struct kobject *,
struct bin_attribute *, int);
struct attribute **attrs;
struct bin_attribute **bin_attrs;
};

attribute group的特殊之处在于,若为其设置了name,则该group中的属性都会出现在名为name的子目录下,而不是作为kobject目录下的文件出现。sysfs提供了sysfs_create_groupsysfs_update_groupsysfs_remove_groupsysfs_add_file_to_group等函数用于操作attribute group,有兴趣可以查阅include/linux/sysfs.h查看完整列表。

根据Greg Kroah-Hartman的Presentation,我们应该尽量使用Attribute Group,不要使用单独的Attribute。

kset & uevent

通常kobject都不单独使用,而是嵌入在其他struct中使用,这是一种面向对象的思想,kobject相当于基类的作用。kset就是一种特殊的kobject,它负责容纳一组kobject,以链表的形式组织,其定义如下:

1
2
3
4
5
6
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
const struct kset_uevent_ops *uevent_ops;
} __randomize_layout;

处于同一个kset中的kobject,其kset成员都会指向该kset,从而在kobject_add注册sysfs目录时,kobject的父目录会设置为kset的内嵌kobject(kset->kobj)。当然,也可以为kobject同时设置parentkset,从而使其父目录不是kset的目录。

我们可以用kset_init初始化kset,然后用kset_register将kset注册到sysfs,用kset_unregister则可以将kset从sysfs中移除。若要将kobject加入kset,或自kset中移除,则分别可以调用kobject_add和kobject_del

kset的作用不仅仅是提供一个kobject的容器,它的根本用途是管理向用户态发送的uevent通知,用户态程序根据uevent可以动态地在/dev下挂载或移除(即插即用)设备,或是进行其他更复杂的操作。实际上,所有kobject产生的uevent都要经过kset汇总才能发给用户态,如果kobject的祖先节点中没有kset则无法发送uevent。

uevent消息的格式形如NAME1=value1, NAME2=value2, ...。内核中定义了8种uevent类型,不同的类型对应于不同的ACTION,例如KOBJ_ADD对应于ACTION=add

1
2
3
4
5
6
7
8
9
10
11
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_BIND,
KOBJ_UNBIND,
KOBJ_MAX
};

我们可以使用kobject_uevent(kobj, action)或者kobject_uevent_env(kobj, action, envp)来发送一个uevent,前者实际上是用后者实现的。例如,在将某设备的kobject注册进sysfs后,就应该调用kobject_uevent(kobj, KOBJ_ADD)通知用户态该设备已经注册。envp是一个环境变量数组,用于提供NAME=value数组,这会被加入到uevent消息中。

kset中的uevent_ops定义如下:

1
2
3
4
5
6
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);
};

考察kobject_uevent_env函数,可以发现uevent_ops->filter的用途是将部分kobject发送的uevent过滤掉:

1
2
3
4
5
6
7
8
/* skip the event, if the filter returns zero. */
if (uevent_ops && uevent_ops->filter)
if (!uevent_ops->filter(kset, kobj)) {
pr_debug("kobject: '%s' (%p): %s: filter function "
"caused the event to drop!\n",
kobject_name(kobj), kobj, __func__);
return 0;
}

uevent_ops->name的用途是将SUBSYSTEM=$name加入uevent消息,其中name = uevent_ops->name(kset, kobj)

1
2
3
4
5
6
7
/* originating subsystem */
if (uevent_ops && uevent_ops->name)
subsystem = uevent_ops->name(kset, kobj);
else
subsystem = kobject_name(&kset->kobj);

retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);

最后uevent_ops->uevent的用途是直接修改构造到一半的uevent消息:

1
2
3
4
5
6
7
8
9
10
11
struct kobj_uevent_env *env;

if (uevent_ops && uevent_ops->uevent) {
retval = uevent_ops->uevent(kset, kobj, env);
if (retval) {
pr_debug("kobject: '%s' (%p): %s: uevent() returned "
"%d\n", kobject_name(kobj), kobj,
__func__, retval);
goto exit;
}
}

构造完uevent消息后,有两种方式通知用户态,一种是通过netlink socket将uevent消息广播,另一种是通过直接调用一个用户态程序,将uevent消息作为环境变量传给该程序。

现在主流采用的是netlink方式,对应的用户态程序为udev,它目前已经成为了systemd的一部分。内核的调用链为kobject_uevent_env --> kobject_uevent_net_broadcast --> uevent_net_broadcast_untagged/uevent_net_broadcast_tagged --> netlink_broadcast,最终使用netlink_broadcast进行全局广播。内核使用的socket采用NETLINK_KOBJECT_UEVENT协议,用户态只需要通过socket(AF_NETLINK, *, NETLINK_KOBJECT_UEVENT)创建socket,即可用此socket接收到内核发出的uevent广播。

另一种方式是通过kobject_uevent_env --> call_usermodehelper_exec调用用户态程序uevent_helper,其默认值为CONFIG_UEVENT_HELPER_PATH,在编译时指定,一般为/sbin/hotplug,即udev的上一代解决方案hotplug。在嵌入式系统中,常使用mdev取代udev,也采用这种方式传递uevent,其路径为/sbin/mdev。在kernel运行时,还可以通过sys/kernel/uevent_helper动态修改uevent_helper的取值。

Internals

Dir Creation

我们知道namespace是Linux用于实现容器的功能,可以提供名字空间的隔离,例如使两个进程看到不同的进程空间。sysfs必须是namespace aware的,这样才能确保/sys/class/net/eth0这样的目录对于两个处在不同的network namespace中的进程,会呈现不同的内容。因此,在sysfs中创建目录、文件时,实际上要带上namespace作为参数,这样我们可以为同一个路径注册多次,每次使用不同的namespace。

考察kobject_add --> kobject_add_varg --> kobject_add_internal --> create_dir,这是创建目录及属性文件的核心函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static int create_dir(struct kobject *kobj)
{
const struct kobj_ns_type_operations *ops;
int error;

error = sysfs_create_dir_ns(kobj, kobject_namespace(kobj));
if (error)
return error;

error = populate_dir(kobj);
if (error) {
sysfs_remove_dir(kobj);
return error;
}

/*
- @kobj->sd may be deleted by an ancestor going away. Hold an
- extra reference so that it stays until @kobj is gone.
*/
sysfs_get(kobj->sd);

/*
- If @kobj has ns_ops, its children need to be filtered based on
- their namespace tags. Enable namespace support on @kobj->sd.
*/
ops = kobj_child_ns_ops(kobj);
if (ops) {
BUG_ON(ops->type <= KOBJ_NS_TYPE_NONE);
BUG_ON(ops->type >= KOBJ_NS_TYPES);
BUG_ON(!kobj_ns_type_registered(ops->type));

sysfs_enable_ns(kobj->sd);
}

return 0;
}

我们首先试图找到kobj对应的namespace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* kobject_namespace - return @kobj's namespace tag
* @kobj: kobject in question
*
* Returns namespace tag of @kobj if its parent has namespace ops enabled
* and thus @kobj should have a namespace tag associated with it. Returns
* %NULL otherwise.
*/
const void *kobject_namespace(struct kobject *kobj)
{
const struct kobj_ns_type_operations *ns_ops = kobj_ns_ops(kobj);

if (!ns_ops || ns_ops->type == KOBJ_NS_TYPE_NONE)
return NULL;

return kobj->ktype->namespace(kobj);
}

通常,kobj_ns_ops(kobj)会返回NULL,于是kobj没有对应的namespace。若kobj->parent->ktype->child_ns_type非空,则kobj_ns_ops(kobj)的返回值为kobj->parent->ktype->child_ns_type(kobj->parent),这种情况代表父目录有对应的namespace,自然子目录也应该和同一个namespace绑定。

然后,我们调用sysfs_create_dir_ns创建kobject对应的目录:

1
2
3
4
5
6
7
8
9
10
11
12
struct kernfs_node *parent, *kn;
/* ...... */
if (kobj->parent)
parent = kobj->parent->sd;
else
parent = sysfs_root_kn;
/* ...... */
kn = kernfs_create_dir_ns(parent, kobject_name(kobj),
S_IRWXU | S_IRUGO | S_IXUGO, uid, gid,
kobj, ns);
/* ...... */
kobj->sd = kn;

可以发现,真正的工作实际上是在kernfs这一层完成,而sysfs只是kernfs的一个wrapper,它负责的逻辑仅仅是在kobj没有父节点时,将其注册在/sys根目录。实际上此前sysfs是一个完整的文件系统实现,到内核3.14版本时,将其核心逻辑抽取出来形成了kernfs,以便复用代码,今后要实现类似的虚拟文件系统时就可以直接利用kernfs快速方便地实现。

create_dir的下一步工作是调用populate_dir,将默认属性注册为sysfs中的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* populate_dir - populate directory with attributes.
* @kobj: object we're working on.
*
* Most subsystems have a set of default attributes that are associated
* with an object that registers with them. This is a helper called during
* object registration that loops through the default attributes of the
* subsystem and creates attributes files for them in sysfs.
*/
static int populate_dir(struct kobject *kobj)
{
struct kobj_type *t = get_ktype(kobj);
struct attribute *attr;
int error = 0;
int i;

if (t && t->default_attrs) {
for (i = 0; (attr = t->default_attrs[i]) != NULL; i++) {
error = sysfs_create_file(kobj, attr);
if (error)
break;
}
}
return error;
}

 最后,我们检查kobj是否支持namespace,即kobj->ktype->child_ns_type(kobj)是否返回有效值,若支持则为该目录启用sysfs的namespace支持。

File Creation

创建目录的过程并不复杂,下面继续考察创建文件的过程,即sysfs_create_filesysfs_create_bin_file,它们最终都会调用到sysfs_add_file_mode_ns,这个函数实现了创建文件的核心功能:

1
2
3
int sysfs_add_file_mode_ns(struct kernfs_node *parent,
const struct attribute *attr, bool is_bin,
umode_t mode, kuid_t uid, kgid_t gid, const void *ns)

它最终又是调用了__kernfs_create_file来创建文件。

1
2
kn = __kernfs_create_file(parent, attr->name, mode & 0777, uid, gid,
size, ops, (void *)attr, ns, key);

注意这里传入的ops是kernfs层的struct kernfs_ops,根据是否是二进制属性以及读写权限等,会使用不同的kernfs_ops

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
if (!is_bin) {
struct kobject *kobj = parent->priv;
const struct sysfs_ops *sysfs_ops = kobj->ktype->sysfs_ops;

/* every kobject with an attribute needs a ktype assigned */
if (WARN(!sysfs_ops, KERN_ERR
"missing sysfs attribute operations for kobject: %s\n",
kobject_name(kobj)))
return -EINVAL;

if (sysfs_ops->show && sysfs_ops->store) {
if (mode & SYSFS_PREALLOC)
ops = &sysfs_prealloc_kfops_rw;
else
ops = &sysfs_file_kfops_rw;
} else if (sysfs_ops->show) {
if (mode & SYSFS_PREALLOC)
ops = &sysfs_prealloc_kfops_ro;
else
ops = &sysfs_file_kfops_ro;
} else if (sysfs_ops->store) {
if (mode & SYSFS_PREALLOC)
ops = &sysfs_prealloc_kfops_wo;
else
ops = &sysfs_file_kfops_wo;
} else
ops = &sysfs_file_kfops_empty;

size = PAGE_SIZE;
} else {
struct bin_attribute *battr = (void *)attr;

if (battr->mmap)
ops = &sysfs_bin_kfops_mmap;
else if (battr->read && battr->write)
ops = &sysfs_bin_kfops_rw;
else if (battr->read)
ops = &sysfs_bin_kfops_ro;
else if (battr->write)
ops = &sysfs_bin_kfops_wo;
else
ops = &sysfs_file_kfops_empty;

size = battr->size;
}

这些回调最终就会调用ktype中的sysfs_ops回调,以对普通属性的写入为例:

1
2
3
4
5
6
7
8
9
10
11
12
/* kernfs write callback for regular sysfs files */
static ssize_t sysfs_kf_write(struct kernfs_open_file *of, char *buf,
size_t count, loff_t pos)
{
const struct sysfs_ops *ops = sysfs_file_ops(of->kn);
struct kobject *kobj = of->kn->parent->priv;

if (!count)
return 0;

return ops->store(kobj, of->kn->priv, buf, count);
}

目光回到kernfs,在__kernfs_create_file中,传入的ops最终被设置到了kn->attr.ops

1
2
3
4
kn->attr.ops = ops;
kn->attr.size = size;
kn->ns = ns;
kn->priv = priv;

考察kernfs注册的file_operations

1
2
3
4
5
6
7
8
9
10
const struct file_operations kernfs_file_fops = {
.read = kernfs_fop_read,
.write = kernfs_fop_write,
.llseek = generic_file_llseek,
.mmap = kernfs_fop_mmap,
.open = kernfs_fop_open,
.release = kernfs_fop_release,
.poll = kernfs_fop_poll,
.fsync = noop_fsync,
};

我们还是以写入为例,kernfs_fop_write中有如下片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mutex_lock(&of->mutex);
if (!kernfs_get_active(of->kn)) {
mutex_unlock(&of->mutex);
len = -ENODEV;
goto out_free;
}

ops = kernfs_ops(of->kn);
if (ops->write)
len = ops->write(of, buf, len, *ppos);
else
len = -EINVAL;

kernfs_put_active(of->kn);
mutex_unlock(&of->mutex);

其中kernfs_ops定义如下:

1
2
3
4
5
6
static const struct kernfs_ops *kernfs_ops(struct kernfs_node *kn)
{
if (kn->flags & KERNFS_LOCKDEP)
lockdep_assert_held(kn);
return kn->attr.ops;
}

至此整个调用链已经很清晰了,用户程序访问sysfs时,首先进入kernfs层的file_operations回调,然后通过kn->attr.ops调用到sysfs层提供的回调,最终调用到ktypebin_attribute提供的回调。