0%

Linux 统一设备模型

为了降低设备多样性带来的Linux驱动开发的复杂度,实现设备的热插拔处理和电源管理等功能,Linux 内核提出了 device model 的概念。device model 将硬件设备归纳、分类,抽象出一套标准的数据结构和接口,驱动的开发则简化为对内核所规定的数据结构的填充和实现。

技术背景

一切皆文件,这是 Linux 的哲学之一。设备当然也不例外,它们往往被抽象成文件,存放在 /dev 目录下供用户进程进行操作。用户通过这些设备文件,可以实现对硬件进行相应的操作。而这些设备文件,需要由对应的设备文件系统来负责管理。

在 kernel 2.6 之前,完成这一使命的是 devfs。devfs 是 Linux 2.4 引入的一个虚拟的文件系统,挂载在 /dev 目录下。可以动态地为设备在 /dev 下创建或删除相应的设备文件,只生成存在设备的节点。然而它存在以下缺点:

  • 可分配的设备号数目 (major / minor) 受到限制
  • 设备映射不确定,一个设备所对应的设备文件可能发生改变
  • 设备名称在内核或模块中写死,违反了内核开发的原则
  • 缺乏热插拔机制

随着 kernel 的发展,从 Linux 2.6 起,devfs 被 sysfs + udev 所取代。sysfs + udev 在设计哲学和现实中的易用性都比 devfs 更优,自此 sysfs + udev 的组合走上 mainline ,直至目前,依然作为 Linux 的设备管理手段。

kobject

数据结构

kobject 时 Linux 设备模型的基础,它主要提供如下功能:

  • 通过 parent 指针,可以将所有 kobject 以层次结构的形式组合起来。
  • 使用 reference count,来记录 kobject被引用的次数,并在引用次数变为 0 时把它释放
  • sysfs 文件系统配合,将每一个kobject 及其特性,以文件的形式,开放到用户空间

struct kobject

在Linux中,kobject几乎不会单独存在,Linux driver开发者,很少会直接使用 kobject 以及它提供的接口,而是使用构建在 kobject 之上的设备模型接口。 kobject 的主要功能,就是内嵌在一个大型的数据结构中,为这个数据结构提供一些底层的功能实现。

include/linux/kobject.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct kobject {
const char *name; // 名称,将在 sysfs 中作为文件名
struct list_head entry; // 加入 kset 链表的结构
struct kobject *parent; // 父节点指针,构成树状结构,在sysfs就表现为目录结构
struct kset *kset; // 指向所属 kset,可以为NULL,如果存在,且没有指定parent,则会把kset作为parent
struct kobj_type *ktype; // 类型
struct kernfs_node *sd; // 指向所属 (sysfs) 目录项
struct kref kref; // 可用于原子操作的引用计数
unsigned int state_initialized:1; // 是否已经初始化
unsigned int state_in_sysfs:1; // 是否已在 sysfs 中显示
unsigned int state_add_uevent_sent:1; // 是否已经向 user space 发送 ADD uevent
unsigned int state_remove_uevent_sent:1; // 是否已经向 user space 发送 REMOVE uevent
unsigned int uevent_suppress:1; // 是否忽略上报(不上报 uevent)
};

struct kobj_type

kobject_type 类似于对 kobject 的派生,包含不同 kobj_type 的 kobject 可以看做不同的子类。通过实现相同的函数来实现多态。在这种设计下,每一个内嵌 kobject 的数据结构(如 kset、device、device_driver 等),都要实现自己的 kobj_type,并实现其中的函数。

kobj_type 结构如下:

include/linux/kobject.h
1
2
3
4
5
6
7
struct kobj_type {
void (*release)(struct kobject *kobj); // 析构函数,kobject 的引用计数为 0 时调用,可以将包含该种类型kobject的数据结构的内存空间释放掉
const struct sysfs_ops *sysfs_ops; // 操作函数,当用户读取 sysfs 属性时调用 show(),写入 sysfs 属性时调用 store()
struct attribute **default_attrs; // 默认属性,体现为该 kobject 目录下的文件
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj); // namespace 操作函数
const void *(*namespace)(struct kobject *kobj);
};

kobj_type的定义会如实地在 sysfs 中反映,其中的属性 attribute 会以 attribute.name 为文件名在该目录下创建文件。

attibute 是内核空间和用户空间进行信息交互的一种方法。例如某个driver定义了一个变量,却希望用户空间程序可以修改该变量,以控制driver的运行行为,那么就可以将该变量以 sysfs attribute 的形式开放出来。attribute 分为普通的 attribute 和二进制 attribute :

  • 使用 attribute 生成的 sysfs 文件,只能用字符串的形式读写
  • struct bin_attribute 在 attribute 的基础上,增加了 read、write 等函数,因此它所生成的 sysfs 文件可以用任何方式读写
  • 类似于 bin_attribute ,我们可以用包含 attribute 的方式对 attribute 进行扩展,定义出 device_attribute 、 class_attribute 或一些设备自定义属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
};

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);
};

attribute_group 是属性组,将一组属性打包成一个对象,其包含了以 attribute 和 bin_attribute 指针数组。

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;
};

对该文件进行读写会调用 sysfs_ops 中定义的 show()store()

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

struct kset

kset 是 kobject 的容器,用于表示某一类型的 kobject 。kset 维护了其包含的 kobject 链表,链表的最后一项执向 kset.kobj

include/linux/kobject.h
1
2
3
4
5
6
struct kset {
struct list_head list; // kobject 链表头
spinlock_t list_lock; // 自旋锁,保障操作安全
struct kobject kobj; // 自身的 kobject
const struct kset_uevent_ops *uevent_ops; // uevent 操作函数集。kobject 发送 uevent 时会调用所属 kset 的 uevent_ops
};

注意和 kobj_type的关联,kobject 会利用成员 kset 找到自已所属的 kset,设置自身的 ktype 为 kset.kobj.ktype 。当没有指定 kset 成员时,才会用 ktype 来建立关系。

此外,kobject 调用的是它所属 kset 的 uevent 操作函数来发送 uevent,如果 kobject 不属于任何 kset ,则无法发送 uevent。

功能实现

kobject大多数情况下会嵌在其它数据结构中使用,其使用流程如下:

  1. 定义一个 struct kset 类型的指针,并在初始化时为它分配空间,添加到内核中
  2. 根据实际情况,定义自己所需的数据结构原型,该数据结构中包含有 kobject
  3. 定义一个适合自己的 ktype,并实现其中回调函数
  4. 在需要使用到包含 kobject 的数据结构时,动态分配该数据结构,并分配 kobject 空间,添加到内核中
  5. 每一次引用数据结构时,调用 kobject_get 接口增加引用计数;引用结束时,调用 kobject_put 接口,减少引用计数
  6. 当引用计数减少为 0 时,kobject 模块调用 ktype 所提供的 release 接口,释放上层数据结构以及 kobject 的内存空间

上面有提过,kobject大多数情况下会嵌在其它数据结构中使用,有一种例外 kobject 单独使用的情况是:

开发者只需要在 sysfs 中创建一个目录,而不需要其它的 kset、ktype 的操作。这时可以直接调用 kobject_create_and_add 接口,分配一个kobject结构并把它添加到 kernel 中。

kobject 的分配和释放

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

  • 通过 kmalloc 自行分配(一般是跟随上层数据结构分配),并在初始化后添加到kernel。这种方式分配的 kobject,会在引用计数变为0时,由 kobject_put 调用其 ktype 的 release 接口,释放内存空间,其涉及接口如下:
1
2
3
4
5
6
7
8
// kobject_init 设置 kobject 的引用计数为1,将 ktype 赋值给 kobj->ktype 和其他初始化工作
void kobject_init(struct kobject *kobj, struct kobj_type *ktype);

// kobject_add 将初始化完成的 kobject 添加到 kernel 中,参数包括需要添加的kobject、该kobject的parent(用于形成层次结构,可以为空)、用于提供kobject name的格式化字符串。
int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...);

// kobject_init_and_add 是上面两个接口的组合
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype, struct kobject *parent, const char *fmt, ...);

kobject_add 最终会调用内部接口 kobject_add_internal,将 kobject 添加到 kernel ,其主要逻辑为:

  • 校验kobj以及kobj->name的合法性,若不合法打印错误信息并退出
  • 调用 kobject_get 增加该kobject的parent的引用计数(如果存在parent的话)
  • 如果存在kset(即kobj->kset不为空),则调用 kobj_kset_join接口加入kset。同时,如果该kobject没有parent,却存在kset,则将它的parent设为kset(kset是一个特殊的kobject),并增加kset的引用计数
  • 通过 create_dir 接口,调用 sysfs 的相关接口,在sysfs下创建该kobject对应的目录
  • 如果创建失败,执行后续的回滚操作,否则将kobj->state_in_sysfs置为1
  • 使用 kobject_create 创建

kobject 模块可以使用 kobject_create 自行分配空间,并内置了一个 ktype (dynamic_kobj_type) ,用于在计数为0是释放空间。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// kobject_create,该接口为kobj分配内存空间,并以dynamic_kobj_ktype为参数,调用kobject_init接口,完成后续的初始化操作。
struct kobject * kobject_create(void);

// kobject_create_and_add,是kobject_create和kobject_add的组合
struct kobject * kobject_create_and_add(const char *name, struct kobject *parent);

static void dynamic_kobj_release(struct kobject *kobj)
{
pr_debug("kobject: (%p): %s\n", kobj, __func__);
kfree(kobj);
}

static struct kobj_type dynamic_kobj_ktype = {
.release = dynamic_kobj_release,
.sysfs_ops = &kobj_sysfs_ops,
};

kobject引用计数的修改

通过 kobject_getkobject_put 可以修改 kobject 的引用计数,并在计数为 0 时,调用 ktyperelease 接口,释放占用空间。

include/linux/kobject.h
1
2
3
4
5
// kobject_get,调用kref_get,增加引用计数
struct kobject *kobject_get(struct kobject *kobj);

// kobject_put,以内部接口kobject_release为参数,调用kref_put,kref模块会在引用计数为零时,调用kobject_release。
void kobject_put(struct kobject *kobj);

kobject_release,通过kref结构,获取kobject指针,并调用kobject_cleanup接口继续。

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

  • 检查该kobject是否有ktype,如果没有,打印警告信息
  • 如果该kobject向用户空间发送了ADD uevent但没有发送REMOVE uevent,补发REMOVE uevent
  • 如果该kobject有在sysfs文件系统注册,调用kobject_del接口,删除它在sysfs中的注册
  • 调用该kobject的ktype的release接口,释放内存空间
  • 释放该kobject的name所占用的内存空间

kset的初始化与注册

kset 是一个特殊的 kobject,因此其初始化、注册等操作也会调用 kobject 的相关接口,除此之外,会有它特有的部分。和 kobject一样,kset的内存分配,可以由上层软件通过 kmalloc自行分配,也可以由 kobject 模块负责分配,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
// kset_init,用于初始化已分配的kset,主要包括调用kobject_init_internal初始化其kobject,然后初始化kset的链表
// 需要注意的是,如果使用此接口,上层软件必须提供该kset中的kobject的ktype
void kset_init(struct kset *kset);

// kset_register,先调用kset_init,然后调用kobject_add_internal将其kobject添加到kernel
int kset_register(struct kset *kset);

// kset_unregister,直接调用kobject_put释放其kobject。当其kobject的引用计数为0时,即调用ktype的release接口释放kset占用的空间
void kset_unregister(struct kset *kset);

// kset_create_and_add,会调用内部接口kset_create动态创建一个kset,并调用kset_register将其注册到kernel
struct kset * kset_create_and_add(const char *name, const struct kset_uevent_ops *u, struct kobject *parent_kobj);

sysfs

sysfs 是一个基于内存的虚拟文件系统,它和 kobject一起,可以将 kernel 的数据结构导出到用户空间,Linux 的设备模型在 sysfs 体现为:

  • Kernel Objects: 目录
  • Object Atrributes: 文件(文件内容为属性值)
  • Object Relationships: 链接文件

目录结构

sysfs 负责以设备树的形式向用户空间提供直观的设备和驱动信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ tree -L 1 /sys
/sys
|-- block # 历史遗留问题,存放块设备,提供以设备名 (如 sda) 到 `/sys/devices` 的符号链接
|-- bus # 按总线类型分类,在某个总线目录之下可以找到连接该总线的设备的符号链接,指向 /sys/devices
|-- class # 按设备功能分类,如输入设备在 `/sys/class/input` 之下,图形设备在 `/sys/class/graphics` 之下
|-- dev # 按设备驱动程序分层(字符设备 / 块设备),提供以 major:minor 为名到 /sys/devices 的符号链接
|-- devices # 包含所有被发现的注册在各种总线上的各种物理设备
|-- firmware # 提供对固件的查询和操作接口(关于固件有专用于固件加载的一套 API)
|-- fs # 描述当前加载的文件系统,提供文件系统和文件系统已挂载设备信息
|-- hypervisor # 如果开启了 Xen,这个目录下会提供相关属性文件
|-- kernel # 提供 kernel 所有可调整参数,但大多数可调整参数依然存放在 sysctl(/proc/sys/kernel)
|-- module # 所有加载模块 (包括内联、编译进 kernel、外部的模块) 的信息,按模块类型分类
`-- power # 电源选项,可用于控制整个机器的电源状态,如写入控制命令进行关机、重启等

以硬盘 vda 为例,既可以在块设备目录 /sys/block/ 下找到,又可以在所有设备目录 /sys/devices 下找到,

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
# ls -al /sys/block/
total 0
drwxr-xr-x 2 root root 0 Mar 28 21:43 .
dr-xr-xr-x 13 root root 0 Mar 28 21:43 ..
lrwxrwxrwx 1 root root 0 Mar 28 21:43 loop0 -> ../devices/virtual/block/loop0
lrwxrwxrwx 1 root root 0 Mar 28 21:43 sr0 -> ../devices/pci0000:00/0000:00:01.1/ata1/host0/target0:0:1/0:0:1:0/block/sr0
lrwxrwxrwx 1 root root 0 Mar 28 21:43 vda -> ../devices/pci0000:00/0000:00:06.0/virtio1/block/vda
# ls -al /sys/block/vda/
total 0
drwxr-xr-x 10 root root 0 Mar 28 21:43 .
drwxr-xr-x 3 root root 0 Mar 28 21:43 ..
-r--r--r-- 1 root root 4096 Mar 28 21:43 alignment_offset
lrwxrwxrwx 1 root root 0 Mar 28 21:43 bdi -> ../../../../../virtual/bdi/253:0
-rw-r--r-- 1 root root 4096 Mar 28 21:43 cache_type
-r--r--r-- 1 root root 4096 Mar 28 21:43 capability
-r--r--r-- 1 root root 4096 Mar 28 21:43 dev
lrwxrwxrwx 1 root root 0 Mar 28 21:43 device -> ../../../virtio1
-r--r--r-- 1 root root 4096 Mar 28 21:43 discard_alignment
-r--r--r-- 1 root root 4096 Mar 28 21:43 ext_range
-r--r--r-- 1 root root 4096 Mar 28 21:43 hidden
drwxr-xr-x 2 root root 0 Mar 28 21:43 holders
-r--r--r-- 1 root root 4096 Mar 28 21:43 inflight
drwxr-xr-x 2 root root 0 Mar 29 11:40 integrity
drwxr-xr-x 3 root root 0 Mar 29 11:40 mq
drwxr-xr-x 2 root root 0 Mar 29 11:40 power
drwxr-xr-x 3 root root 0 Mar 28 21:43 queue
-r--r--r-- 1 root root 4096 Mar 28 21:43 range
-r--r--r-- 1 root root 4096 Mar 28 21:43 removable
-r--r--r-- 1 root root 4096 Mar 28 21:43 ro
-r--r--r-- 1 root root 4096 Mar 28 21:43 serial
-r--r--r-- 1 root root 4096 Mar 28 21:43 size
drwxr-xr-x 2 root root 0 Mar 28 21:43 slaves
-r--r--r-- 1 root root 4096 Mar 28 21:43 stat
lrwxrwxrwx 1 root root 0 Mar 28 21:43 subsystem -> ../../../../../../class/block
drwxr-xr-x 2 root root 0 Mar 29 11:40 trace
-rw-r--r-- 1 root root 4096 Mar 28 21:43 uevent
drwxr-xr-x 5 root root 0 Mar 28 21:43 vda1

目录以文件的形式提供了设备的信息,比如 dev 记录了主设备号和次设备号,size 记录了分区大小,uevent 存放了 uevent 的标识符等:

1
2
3
4
5
6
7
8
9
$ cat /sys/block/vda/size
104857600
$ cat /sys/block/vda/dev
253:0
$ cat /sys/block/vda/uevent
MAJOR=253
MINOR=0
DEVNAME=vda
DEVTYPE=disk

sysfs 映射

sysfs 本质上是对 Linux 设备模型中各个数据结构的映射,它是通过 VFS 的接口去读写 kobject 的层次结构后动态建立的内存文件系统。

从代码实现上看出,当前版本的 sysfs 实际上基于 kernfs 实现。sysfs_init 通过 kernfs_create_root 创建新的 kernfs 层级,然后将其保存在静态全局变量中,供各处使用。然后通过 register_filesystem 将其注册为名为 sysfs 的文件系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __init sysfs_init(void)
{
int err;

sysfs_root = kernfs_create_root(NULL, KERNFS_ROOT_EXTRA_OPEN_PERM_CHECK, NULL);
if (IS_ERR(sysfs_root))
return PTR_ERR(sysfs_root);

sysfs_root_kn = sysfs_root->kn;

err = register_filesystem(&sysfs_fs_type);
if (err) {
kernfs_destroy_root(sysfs_root);
return err;
}

return 0;
}

目录映射

每个kobject 都在 sysfs 中对应一个目录,因此在将 kobject 添加到 Kernel 时会调用 sysfs 文件系统的创建目录接口,创建和Kobject对应的目录,相关代码调用链路如下:

1
kobject_add => kobject_add_varg => kobject_add_internal => create_dir => sysfs_create_dir

这里是详细 create_dir 的相关代码:

  • 如果 kobj 有 parent ,则它的父节点为 kobj->parent->sd ,否则为根目录节点 sysfs_root
  • 然后将其作为参数调用在父节点目录下创建一个名为 kobj->name 的目录
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
/* lib/kobject.c, line 47 */
static int create_dir(struct kobject *kobj)
{
int error = 0;
error = sysfs_create_dir(kobj);
if (!error) {
error = populate_dir(kobj);
if (error)
sysfs_remove_dir(kobj);
}
return error;
}

/* fs/sysfs/dir.c, line 736
**
* sysfs_create_dir - create a directory for an object.
* @kobj: object we're creating directory for.
*/
int sysfs_create_dir(struct kobject * kobj)
{
enum kobj_ns_type type;
struct sysfs_dirent *parent_sd, *sd;
const void *ns = NULL;
int error = 0;

if (kobj->parent)
parent_sd = kobj->parent->sd;
else
parent_sd = &sysfs_root;

if (!parent_sd)
return -ENOENT;

if (sysfs_ns_type(parent_sd))
ns = kobj->ktype->namespace(kobj);
type = sysfs_read_ns_type(kobj);

error = create_dir(kobj, parent_sd, type, ns, kobject_name(kobj), &sd);
if (!error)
kobj->sd = sd;
return error;
}

属性映射

kobject 的属性在 sysfs 中对应的是文件,在 linux 内核中,attibute文件的创建由 sysfs_create_file 接口完成的:

fs/sysfs/file.c
1
2
3
4
5
// 在 sysfs 为 kobj 的属性创建文件
int sysfs_create_file(struct kobject *kobj, struct attribute *attr);

// 在 sysfs 删除属性
int sysfs_remove_file(struct kobject *kobj, struct attribute *attr);

所有的文件系统,都会定义一个 struct file_operations变量,用于描述本文件系统的操作接口,sysfs也不例外:

fs/sysfs/file.c
1
2
3
4
5
6
7
8
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,
};

attribute 文件的 read 操作,会由 VFS 转到 sysfs_file_operationssysfs_read_file 接口上,其处理逻辑如下:

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
// sysfs_read_file从file指针中取一个私有指针,转换为一个struct sysfs_buffer类型的指针,以此为参数(buffer),调用fill_read_buffer接口
static ssize_t
sysfs_read_file(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
struct sysfs_buffer * buffer = file->private_data;
ssize_t retval = 0;

mutex_lock(&buffer->mutex);
if (buffer->needs_read_fill || *ppos == 0) {
retval = fill_read_buffer(file->f_path.dentry,buffer);
if (retval)
goto out;
}
// ...
}

// fill_read_buffer,直接从buffer指针中取出一个struct sysfs_ops指针,调用该指针的show函数,即完成了文件的read操作
static int fill_read_buffer(struct dentry * dentry, struct sysfs_buffer * buffer)
{
struct sysfs_dirent *attr_sd = dentry->d_fsdata;
struct kobject *kobj = attr_sd->s_parent->s_dir.kobj;
const struct sysfs_ops * ops = buffer->ops;
// ...
count = ops->show(kobj, attr_sd->s_attr.attr, buffer->page);
// ...
}

这里的 struct sysfs_ops 指针哪来的?好吧,我们再看看 sysfs_open_file 接口吧。

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
/* fs/sysfs/file.c, line 326 */
static int sysfs_open_file(struct inode *inode, struct file *file)
{
struct sysfs_dirent *attr_sd = file->f_path.dentry->d_fsdata;
struct kobject *kobj = attr_sd->s_parent->s_dir.kobj;
struct sysfs_buffer *buffer;
const struct sysfs_ops *ops;
int error = -EACCES;

/* need attr_sd for attr and ops, its parent for kobj */
if (!sysfs_get_active(attr_sd))
return -ENODEV;

/* every kobject with an attribute needs a ktype assigned */
if (kobj->ktype && kobj->ktype->sysfs_ops)
ops = kobj->ktype->sysfs_ops;
else {
WARN(1, KERN_ERR "missing sysfs attribute operations for "
"kobject: %s\n", kobject_name(kobj));
goto err_out;
}

...

buffer = kzalloc(sizeof(struct sysfs_buffer), GFP_KERNEL);
if (!buffer)
goto err_out;

mutex_init(&buffer->mutex);
buffer->needs_read_fill = 1;
buffer->ops = ops;
file->private_data = buffer;
...
}

可以看到,sysfs_bufferops 指针来自于 kobjectktypesysfs_ops 指针。如果从属的kobject(就是attribute文件所在的目录)没有ktype,或者没有 ktype->sysfs_ops指针,是不允许它注册任何attribute的!

以 class 为例

让我们通过设备模型 class 中有关sysfs的实现,来总结一下sysfs的应用方式。首先,在class.c中,定义了Class所需的ktype以及sysfs_ops类型的变量,如下:

drivers/base/class.c
1
2
3
4
5
6
7
8
9
10
11
static const struct sysfs_ops class_sysfs_ops = {
.show = class_attr_show,
.store = class_attr_store,
.namespace = class_attr_namespace,
};

static struct kobj_type class_ktype = {
.sysfs_ops = &class_sysfs_ops,
.release = class_release,
.child_ns_type = class_child_ns_type,
};

由前面章节的描述可知,所有 class_typekobject下面的 attribute 文件的读写操作,都会交给 class_attr_showclass_attr_store两个接口处理,以 class_attr_show 为例:

drivers/base/class.c
1
2
3
4
5
6
7
8
9
10
11
12
#define to_class_attr(_attr) container_of(_attr, struct class_attribute, attr)

static ssize_t class_attr_show(struct kobject *kobj, struct attribute *attr, char *buf)
{
struct class_attribute *class_attr = to_class_attr(attr);
struct subsys_private *cp = to_subsys_private(kobj);
ssize_t ret = -EIO;

if (class_attr->show)
ret = class_attr->show(cp->class, class_attr, buf);
return ret;
}

该接口使用 container_ofstruct attribute 类型的指针中取得一个 class 模块的自定义指针:struct class_attribute,该指针中包含了class模块自身的show和store接口。下面是 struct class_attribute 的声明:

include/linux/device.h
1
2
3
4
5
struct class_attribute {
struct attribute attr;
ssize_t (*store)(struct class *class, struct class_attribute *attr, const char *buf, size_t count);
const void *(*namespace)(struct class *class, const struct class_attribute *attr);
};

因此,所有需要使用 attribute的模块,都不会直接定义 struct attribute 变量,而是通过一个自定义的数据结构,该数据结构的一个成员是struct attribute 类型的变量,并提供show和store回调函数。然后在该模块 ktype 所对应的struct sysfs_ops变量中,实现该本模块整体的show和store函数,并在被调用时,转接到自定义数据结构 struct class_attribute中的show和store函数中。这样,每个atrribute文件,实际上对应到一个自定义数据结构变量中了。

device

数据结构

struct device

device 描述了一项设备,对应的数据结构 device

1
2
3
4
5
6
7
8
9
10
11
12
13
struct device {
struct device *parent; // 该设备的父设备,一般是该设备所属的bus,controller等设备
struct device_private *p; // 一个用于struct device的私有数据结构指针
struct kobject kobj; // 表示该设备并把它连接到结构体系中的 kobject
const char *init_name; // 该设备的名称
const struct device_type *type;// 它和device的关系,类似于 kobject 和 kobj_type 的关系
struct bus_type *bus; // 标识了该设备连接到何种类型的总线上
struct device_driver *driver; // 该device对应的 device driver
struct class *class; // 该设备属于哪个class
dev_t devt; // 设备号,由两个部分(Major和Minor)组成
void (*release)(struct device *dev);
/* 省略了部分成员 */
};

struct device_private

其中维护了类型为 device_private 的指针 p :

1
2
3
4
5
6
7
8
struct device_private {
struct klist klist_children;
struct klist_node knode_parent;
struct klist_node knode_driver;
struct klist_node knode_bus;
struct list_head deferred_probe;
struct device *device;
};

klist_node 用来作为在所属 driver 链表、所属 bus 链表等中的节点。

设备通过 device_register 来注册到系统中,通过 device_unregister 来从系统中卸载。

功能实现

在设备模型框架下,设备驱动的开发是一件很简单的事情,主要包括2个步骤:

  • 分配一个 struct device 类型的变量,填充必要的信息后,把它注册到内核中。
  • 分配一个 struct device_driver 类型的变量,填充必要的信息后,把它注册到内核中。

这两步完成后,内核会在合适的时机,调用 struct device_driver 变量中的probe、remove、suspend、resume等回调函数,从而触发或者终结设备驱动的执行。而所有的驱动程序逻辑,都会由这些回调函数实现,此时,驱动开发者眼中便不再有设备模型,转而只关心驱动本身的实现。

以上两个步骤的补充说明:

  • 一般情况下,Linux驱动开发很少直接使用device和device_driver,因为内核在它们之上又封装了一层,如soc device、platform device等等,而这些层次提供的接口更为简单、易用
  • 内核提供很多struct device结构的操作接口(具体可以参考include/linux/device.h和drivers/base/core.c的代码),主要包括初始化(device_initialize)、注册到内核(device_register)、分配存储空间+初始化+注册到内核(device_create)等等,可以根据需要使用。
  • device和device_driver必须具备相同的名称,内核才能完成匹配操作,进而调用device_driver中的相应接口。这里的同名,作用范围是同一个bus下的所有device和device_driver。
  • device和device_driver必须挂载在一个bus之下,该bus可以是实际存在的,也可以是虚拟的。
  • driver开发者可以在struct device变量中,保存描述设备特征的信息,如寻址空间、依赖的GPIOs等,因为device指针会在执行probe等接口时传入,这时driver就可以根据这些信息,执行相应的逻辑操作了。

attribute 读写

在 sysfs中我们看到,大多数时候,attribute文件的读写数据流为:

1
vfs---->sysfs---->kobject---->attibute---->kobj_type---->sysfs_ops---->xxx_attribute

其中 kobj_typesysfs_opsxxx_attribute都是由包含kobject的上层数据结构实现。Linux内核中关于该内容的例证到处都是,device也不无例外的提供了这种例子,如下:

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
46
47
48
49
50
/* driver/base/core.c, line 118 */
static ssize_t dev_attr_show(struct kobject *kobj, struct attribute *attr,
char *buf)
{
struct device_attribute *dev_attr = to_dev_attr(attr);
struct device *dev = kobj_to_dev(kobj);
ssize_t ret = -EIO;

if (dev_attr->show)
ret = dev_attr->show(dev, dev_attr, buf);
if (ret >= (ssize_t)PAGE_SIZE) {
print_symbol("dev_attr_show: %s returned bad count\n",
(unsigned long)dev_attr->show);
}
return ret;
}

static ssize_t dev_attr_store(struct kobject *kobj, struct attribute *attr,
const char *buf, size_t count)
{
struct device_attribute *dev_attr = to_dev_attr(attr);
struct device *dev = kobj_to_dev(kobj);
ssize_t ret = -EIO;

if (dev_attr->store)
ret = dev_attr->store(dev, dev_attr, buf, count);
return ret;
}

static const struct sysfs_ops dev_sysfs_ops = {
.show = dev_attr_show,
.store = dev_attr_store,
};

/* driver/base/core.c, line 243 */
static struct kobj_type device_ktype = {
.release = device_release,
.sysfs_ops = &dev_sysfs_ops,
.namespace = device_namespace,
};

/* include/linux/device.h, line 478 */
/* interface for exporting device attributes */
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};

至于driver的attribute,则要简单的多,其数据流为:

1
vfs---->sysfs---->kobject---->attribute---->driver_attribute

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* include/linux/device.h, line 247 */
/* sysfs interface for exporting driver attributes */

struct driver_attribute {
struct attribute attr;
ssize_t (*show)(struct device_driver *driver, char *buf);
ssize_t (*store)(struct device_driver *driver, const char *buf,
size_t count);
};

#define DRIVER_ATTR(_name, _mode, _show, _store) \
struct driver_attribute driver_attr_##_name = \
__ATTR(_name, _mode, _show, _store)

device_type

device_type 是内嵌在 struct device结构中的一个数据结构,用于指明设备的类型,并提供一些额外的辅助功能。它的的形式如下:

1
2
3
4
5
6
7
8
9
10
11
/* include/linux/device.h, line 467 */
struct device_type {
const char *name;
const struct attribute_group **groups;
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
char *(*devnode)(struct device *dev, umode_t *mode,
kuid_t *uid, kgid_t *gid);
void (*release)(struct device *dev);

const struct dev_pm_ops *pm;
};

device_type的功能包括:

  • name表示该类型的名称,当该类型的设备添加到内核时,内核会发出DEVTYPE=name类型的uevent,告知用户空间某个类型的设备available了
  • groups,该类型设备的公共attribute集合。设备注册时,会同时注册这些attribute,这就是面向对象中继承的概念
  • uevent,所有相同类型的设备,会有一些共有的uevent需要发送,由该接口实现
  • devnode,devtmpfs有关的内容,暂不说明
  • release,如果device结构没有提供release接口,就要查询它所属的type是否提供,用于释放device变量所占的空间

root device

在sysfs中有这样一个目录:/sys/devices,系统中所有的设备,都归集在该目录下。有些设备,是通过 device_register 注册到Kernel并体现在 /sys/devices/xxx/ 下。但有时候我们仅仅需要在 /sys/devices/ 下注册一个目录,该目录不代表任何的实体设备,这时可以使用下面的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* include/linux/device.h, line 859 */
/*
* Root device objects for grouping under /sys/devices
*/
extern struct device *__root_device_register(const char *name,
struct module *owner);

/*
* This is a macro to avoid include problems with THIS_MODULE,
* just as per what is done for device_schedule_callback() above.
*/
#define root_device_register(name) \
__root_device_register(name, THIS_MODULE)

extern void root_device_unregister(struct device *root);

该接口会调用 device_register函数,向内核中注册一个设备,但是没必要注册与之对应的driver。

driver

数据结构

struct device_driver

设备依赖于 driver 来进行驱动,对应的数据结构为 device_driver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct device_driver {
const char *name; // 驱动名称
struct bus_type *bus; // 驱动管理设备挂接的总线类型
struct module *owner;
const char *mod_name;

bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
enum probe_type probe_type;

const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;

int (*probe) (struct device *dev); // 相关函数,统一接口
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;

const struct dev_pm_ops *pm;

struct driver_private *p;
};

struct driver_private

其中维护了类型为 driver_private 的指针 p :

1
2
3
4
5
6
7
struct driver_private {
struct kobject kobj;
struct klist klist_devices;
struct klist_node knode_bus;
struct module_kobject *mkobj;
struct device_driver *driver;
};

其维护了 driver 自身的私有属性,比如由于它也是 kobject 的子类,因此包含了 kobj 。可通过 driver_create_file / driver_remove_file 来增删属性,属性将直接作用于 p->kobj 。

功能实现

设备驱动 probe

probe 是指在Linux内核中,如果存在相同名称的 devicedevice_driver,内核就会执行 device_driver 中的 probe回调函数。该函数就是所有driver的入口,可以执行诸如硬件设备初始化、字符设备注册、设备文件操作ops注册等动作。

设备驱动 prove 的时机按照自动触发和手动触发有如下几种:

  • struct device 类型的变量注册到内核中时自动触发(device_register,device_add,device_create_vargs,device_create)
  • struct device_driver 类型的变量注册到内核中时自动触发(driver_register)
  • 手动查找同一bus下的所有 device_driver,如果有和指定device同名的driver,执行probe操作(device_attach)
  • 手动查找同一bus下的所有 device,如果有和指定driver同名的device,执行probe操作(driver_attach)
  • 自行调用driver的probe接口,并在该接口中将该driver绑定到某个device结构中,即设置dev->driver(device_bind_driver)

probe 动作实际是由 bus 模块实现的,这不难理解: device 和 device_driver 都是挂载在bus这根线上,因此只有bus最清楚应该为哪些device、哪些driver配对。每个bus都有一个drivers_autoprobe变量,用于控制是否在device或者driver注册时,自动probe。该变量默认为1(即自动probe),bus模块将它开放到sysfs中了,因而可在用户空间修改,进而控制probe行为。

设备驱动 remove

removeprobe 的反操作,发生在 device 或者 device_driver 任何一方从内核注销时,其原理类似。

bus

在Linux设备模型中,Bus(总线)是一类特殊的设备,它是连接处理器和其它设备之间的通道(channel)。为了方便设备模型的实现,内核规定,系统中的每个设备都要连接在一个Bus上,这个Bus可以是一个内部Bus、虚拟Bus或者Platform Bus。

内核通过struct bus_type结构,抽象Bus,它是在include/linux/device.h中定义的。本文会围绕该结构,描述Linux内核中Bus的功能,以及相关的实现逻辑。最后,会简单的介绍一些标准的Bus(如Platform),介绍它们的用途、它们的使用场景。

数据结构

struct bus_type

对bus模块而言,核心数据结构就是 struct bus_type

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
struct bus_type {
const char *name; // 总线名称, 在 sysfs 中以目录形式存在
const char *dev_name; // 该总线下设备的前缀名称
struct device *dev_root; // bus 的默认父设备,实际只喝 sub system 相关
struct device_attribute *dev_attrs; // use dev_groups instead
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;

int (*match)(struct device *dev, struct device_driver *drv); // 当任何属于该Bus的device或者device_driver添加到内核时,内核都会调用该接口
int (*uevent)(struct device *dev, struct kobj_uevent_env *env); // 当任何属于该Bus的device,发生添加、移除或者其它动作时,Bus模块的核心逻辑就会调用该接口,以便bus driver能够修改环境变量。
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);

int (*online)(struct device *dev);
int (*offline)(struct device *dev);

int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);

const struct dev_pm_ops *pm;

const struct iommu_ops *iommu_ops;

struct subsys_private *p;
struct lock_class_key lock_key;
};

struct subsys_private

其中维护了类型为 subsys_private 的指针 p ,它维护了 bus 自身的私有属性:

  • 挂接在该总线上的设备集合 devices_kset
  • 与该总线相关的驱动程序集合 drivers_kset
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct subsys_private {
struct kset subsys;
struct kset *devices_kset;
struct list_head interfaces;
struct mutex mutex;

struct kset *drivers_kset;
struct klist klist_devices;
struct klist klist_drivers;
struct blocking_notifier_head bus_notifier;
unsigned int drivers_autoprobe:1;
struct bus_type *bus;

struct kset glue_dirs;
struct class *class;
};

对应到 sysfs 中,每个 bus_type 对象都对应 /sys/bus 目录下的一个子目录。子目录下必有 devicesdrivers 文件夹,里面存放指向相应设备和驱动的符号链接。以 pci_express 类型的 bus_type 为例:

1
2
3
4
# pwd
/sys/bus/pci_express
# ls
devices drivers drivers_autoprobe drivers_probe uevent

根据上面的核心数据结构,可以总结出bus模块的功能包括:

  • bus的注册和注销
  • 本bus下有device或者device_driver注册到内核时的处理
  • 本bus下有device或者device_driver从内核注销时的处理
  • device_drivers的probe处理
  • 管理bus下的所有device和device_driver

功能实现

bus 注册

bus的注册是由 bus_register接口实现的,该接口的原型是在 include/linux/device.h 中声明的,并在 drivers/base/bus.c 中实现,其原型如下:

1
int bus_register(struct bus_type *bus);

该功能的执行逻辑如下:

  • bus_typestruct subsys_private 类型的指针分配空间,并更新 priv->busbus->p 两个指针为正确的值
  • 初始化 priv->subsys.kobj 的name、kset、ktype等字段,启动name就是该bus的name(它会体现在sysfs中),kset和ktype由bus模块实现,分别为bus_ksetbus_ktype
  • 调用 kset_registerpriv->subsys 注册到内核中,该接口同时会向sysfs中添加对应的目录,如 /sys/bus/spi
  • 调用 bus_create_file 向bus目录下添加一个 uevent attribute,如 /sys/bus/spi/uevent
  • 调用 kset_create_and_add 分别向内核添加 devicesdevice_drivers kset,同时会体现在sysfs中
  • 初始化 priv 指针中的mutex、klist_devices和klist_drivers等变量
  • 调用 add_probe_files接口,在bus下添加 drivers_probedrivers_autoprobe两个attribute,如 /sys/bus/spi/drivers_probe/sys/bus/spi/drivers_autoprobe
    • drivers_probe允许用户空间程序主动出发指定bus下的device_driver的probe动作
    • drivers_autoprobe 控制是否在device或device_driver添加到内核时,自动执行probe
  • 调用 bus_add_attrs,添加由 bus_attrs 指针定义的bus的默认attribute,这些attributes最终会体现在 /sys/bus/xxx目录下

device和 device_driver 添加

内核提供了 device_registerdriver_register 两个接口,供各个driver模块使用。而这两个接口的核心逻辑,是通过bus模块的 bus_add_devicebus_add_driver实现的,下面我们看看这两个接口的处理逻辑。

这两个接口都是在 drivers/base/base.h中声明,在 drivers/base/bus.c中实现,其原型为:

1
2
3
4
5
/* drivers/base/base.h, line 106 */
int bus_add_device(struct device *dev);

/* drivers/base/base.h, line 110 */
int bus_add_driver(struct device_driver *drv);

bus_add_device 的处理逻辑:

  • 调用内部的 device_add_attrs 接口,将由bus->dev_attrs指针定义的默认attribute添加到内核中,它们会体现在 /sys/devices/xxx/xxx_device/目录中

  • 调用 sysfs_create_link接口,将该device在sysfs中的目录,链接到该bus的devices目录下,例如:

    1
    2
    3
    4
    5
    $ ls -al /sys/bus/i2c/devices/
    total 0
    drwxr-xr-x 2 root root 0 Mar 28 21:43 .
    drwxr-xr-x 4 root root 0 Mar 28 21:43 ..
    lrwxrwxrwx 1 root root 0 Mar 29 14:36 i2c-0 -> ../../../devices/pci0000:00/0000:00:01.3/i2c-0

    其中 /sys/devices/.../,为该 device 在sysfs中真正的位置,而为了方便管理,内核在该设备所在的bus的 xxx_bus/devices 目录中,创建了一个符号链接

  • 调用 sysfs_create_link 接口,在该设备的sysfs目录中(如 /sys/devices/platform/alarmtimer/)中,创建一个指向该设备所在bus目录的链接,取名为subsystem,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $  /sys/devices/platform/alarmtimer/ -al
    total 0
    drwxr-xr-x 3 root root 0 Mar 28 21:43 .
    drwxr-xr-x 11 root root 0 Mar 28 21:43 ..
    lrwxrwxrwx 1 root root 0 Mar 29 14:38 driver -> ../../../bus/platform/drivers/alarmtimer
    -rw-r--r-- 1 root root 4096 Mar 29 14:38 driver_override
    -r--r--r-- 1 root root 4096 Mar 29 14:38 modalias
    drwxr-xr-x 2 root root 0 Mar 29 14:38 power
    lrwxrwxrwx 1 root root 0 Mar 28 21:43 subsystem -> ../../../bus/platform
    -rw-r--r-- 1 root root 4096 Mar 28 21:43 uevent
  • 最后,要把该设备指针保存在 bus->priv->klist_devices

bus_add_driver的处理逻辑:

  • 为该driver的 struct driver_private 指针(priv)分配空间,并初始化其中的 priv->klist_devices、priv->driver、priv->kobj.kset等变量,同时将该指针保存在 device_driver 的p处
  • 将driver的kset(priv->kobj.kset)设置为bus的drivers kset(bus->p->drivers_kset),这就意味着所有driver的kobject都位于bus->p->drivers_kset之下(即 /sys/bus/xxx/drivers目录下)
  • 以driver的名字为参数,调用 kobject_init_and_add 接口,在sysfs中注册driver的kobject,体现在 /sys/bus/xxx/drivers/目录下,如 /sys/bus/spi/drivers/spidev
  • 将该driver保存在bus的klist_drivers链表中,并根据drivers_autoprobe的值,选择是否调用driver_attach进行probe
  • 调用driver_create_file接口,在sysfs的该driver的目录下,创建uevent attribute
  • 调用driver_add_attrs接口,在sysfs的该driver的目录下,创建由bus->drv_attrs指针定义的默认attribute
  • 同时根据suppress_bind_attrs标志,决定是否在sysfs的该driver的目录下,创建bind和unbind attribute

driver 的 probe

driver的probe时机及过程,其中大部分的逻辑会依赖bus模块的实现,主要为 bus_probe_devicedriver_attach 接口。同样,这两个接口都是在 drivers/base/base.h 中声明,在drivers/base/bus.c 中实现。

这两个结构的行为类似,逻辑也很简单,即:

  • 搜索所在的bus,比对是否有同名的device_driver(或device)
  • 如果有并且该设备没有绑定Driver则调用 device_driver 的probe接口。

虚拟 bus

在Linux内核中,有三种比较特殊的 bus,分别是system bus、virtual bus和platform bus。它们并不是一个实际存在的bus(像USB、I2C等),而是为了方便设备模型的抽象,而虚构的:

  • system bus是旧版内核提出的概念,用于抽象系统设备(如CPU、Timer等等)。而新版内核认为它是个坏点子,因为任何设备都应归属于一个普通的子系统(New subsystems should use plain subsystems, drivers/base/bus.c, line 1264),所以就把它抛弃了(不建议再使用,它的存在只为兼容旧有的实现)。
  • virtaul bus是一个比较新的bus,主要用来抽象那些虚拟设备,所谓的虚拟设备,是指不是真实的硬件设备,而是用软件模拟出来的设备,例如虚拟机中使用的虚拟的网络设备(有关该bus的描述,可参考该链接处的解释:https://lwn.net/Articles/326540/)
  • platform bus就比较普通,它主要抽象集成在CPU(SOC)中的各种设备。这些设备直接和CPU连接,通过总线寻址和中断的方式,和CPU交互信息。

class

A class is a higher-level view of a device that abstracts out low-level implementation details

数据结构

struct class

此 class 并非 C++ 中的关键字 class ,而是用于表示一种设备分类,对应的数据结构为 class

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
struct class {
const char *name; // 设备分类名
struct module *owner; // 所属模块

struct class_attribute *class_attrs; // 该class的默认attribute
const struct attribute_group **dev_groups;
struct kobject *dev_kobj; // 表示该class下的设备在/sys/dev/下的目录,现在一般有char和block两个,如果dev_kobj为NULL,则默认选择char



int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env); // 当该class下有设备发生变化时,会调用class的uevent回调函数
char *(*devnode)(struct device *dev, umode_t *mode);

void (*class_release)(struct class *class); // 用于release自身的回调函数
void (*dev_release)(struct device *dev); // 用于release class内设备的回调函数


int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
int (*shutdown)(struct device *dev);

const struct kobj_ns_type_operations *ns_type;
const void *(*namespace)(struct device *dev);

const struct dev_pm_ops *pm;

struct subsys_private *p;
};

class 只是一种抽象的概念,用于描述接口相似的一类设备。其存在的意义主要是方便用户能够基于设备的功能进行快速的定位,而不必通过思索设备所处的位置、连接方式等来定位设备。

struct class_interface

struct class_interface是这样的一个结构:它允许class driver在class下有设备添加或移除的时候,调用预先设置好的回调函数(add_dev和remove_dev)。那调用它们做什么呢?想做什么都行(例如修改设备的名称),由具体的class driver实现。

该结构的定义如下:

1
2
3
4
5
6
7
8
/* include/linux/device.h, line 434 */
struct class_interface {
struct list_head node;
struct class *class;

int (*add_dev) (struct device *, struct class_interface *);
void (*remove_dev) (struct device *, struct class_interface *);
};

功能实现

class 的注册

class的注册,是由 __class_register接口实现的,它的处理逻辑和bus的注册类似,主要包括:

  • 为class结构中的 struct subsys_private类型的指针(cp)分配空间,并初始化其中的字段,包括 cp->subsys.kobj.ksetcp->subsys.kobj.ktype等等
  • 调用 kset_register,注册该class。该过程结束后,在/sys/class/目录下,就会创建对应该class(子系统)的目录
  • 调用 add_class_attrs接口,将class结构中 class_attrs指针所指向的attribute,添加到内核中。执行完后,在 /sys/class/xxx_class/ 目录下,就会看到这些attribute对应的文件

device 注册时,和 class 有关的动作

struct device 结构会包含一个struct class指针(这从侧面说明了class是device的集合)。当某个 class driver 向内核注册了一个class后,需要使用该class 的 device,通过把自身的class指针指向该class即可,剩下的事情,就由内核在注册device时处理了。

本节,我们讲一下在device注册时,和class有关的动作:

device的注册最终是由 device_add接口(drivers/base/core.c)实现了,该接口中和class有关的动作包括:

  • 调用 device_add_class_symlinks 接口,创建描述的各种符号链接,即:
    • 在对应class的目录下,创建指向device的符号链接;
    • 在device的目录下,创建名称为subsystem、指向对应class目录的符号链接
  • 调用 device_add_attrs,添加由 class 指定的attributes(class->dev_attrs)
  • 如果存在对应该class的add_dev回调函数,调用该回调函数

uevent

在引子中提到,从 Linux 2.6 起,devfs 被 sysfs + udev 所取代。我们已经知道 sysfs 就是 Linux 统一设备模型的体现,那么 udev 是什么呢?

udev 是从 Linux 2.6 一直沿用至今的设备管理器,挂载并管理着 /dev 。和 devfs 不同的是,它运行在用户态中,允许用户进行自定义配置,并根据配置在收到事件时在 /dev 下创建设备文件。主要由三部分组成:

  • libudev 函数库,提供获取设备信息的接口。已集成到 systemd 中

  • udevd 处于用户空间的管理软件,管理 / dev 下的设备文件,已集成到 systemd 中,包括以下内容:

    • udisks 通过 dbus 提供对存储设备的访问接口
  • upower 通过 dbus 提供电源管理的接口

    • NetworkManager 通过 dbus 提供网络配置的接口
  • udevadm 命令行工具。可用来向 udevd 发送指令

Uevent是Kobject的一部分,用于在Kobject状态发生改变时,例如增加、移除等,通知用户空间程序。用户空间程序收到这样的事件后,会做相应的处理。

该机制通常是用来支持热拔插设备的,例如U盘插入后,USB相关的驱动软件会动态创建用于表示该U盘的device结构(相应的也包括其中的kobject),并告知用户空间程序,为该U盘动态的创建/dev/目录下的设备节点,更进一步,可以通知其它的应用程序,将该U盘设备mount到系统中,从而动态的支持该设备。

由此可知,Uevent 的机制是比较简单的,设备模型中任何设备有事件需要上报时,会触发Uevent提供的接口。Uevent模块准备好上报事件的格式后,可以通过两个途径把事件上报到用户空间:

  • 一种是通过kmod模块,直接调用用户空间的可执行文件
  • 另一种是通过netlink通信机制,将事件从内核空间传递给用户空间。

数据结构

kobject.h定义了uevent相关的常量和数据结构,如下:

kobject_action

kobject_action 定义了event的类型,包括:

1
2
3
4
5
6
7
8
9
10
/* include/linux/kobject.h, line 50 */
enum kobject_action {
KOBJ_ADD, // Kobject(或上层数据结构)的添加事件
KOBJ_REMOVE, // Kobject(或上层数据结构)的移除事件
KOBJ_CHANGE, // Kobject(或上层数据结构)的状态或者内容发生改变
KOBJ_MOVE, // Kobject(或上层数据结构)更改名称或者更改Parent
KOBJ_ONLINE, // Kobject(或上层数据结构)的上线事件,其实是是否使能
KOBJ_OFFLINE, // Kobject(或上层数据结构)的下线事件,其实是是否使能
KOBJ_MAX
};

struct kobj_uevent_env

前面有提到过,在利用Kmod向用户空间上报event事件时,会直接执行用户空间的可执行文件。而在Linux系统,可执行文件的执行,依赖于环境变量,因此kobj_uevent_env用于组织此次事件上报时的环境变量。

1
2
3
4
5
6
7
8
9
10
11
/* include/linux/kobject.h, line 31 */
#define UEVENT_NUM_ENVP 32 /* number of env pointers */
#define UEVENT_BUFFER_SIZE 2048 /* buffer for the variables */

/* include/linux/kobject.h, line 116 */
struct kobj_uevent_env {
char *envp[UEVENT_NUM_ENVP]; // 指针数组,用于保存每个环境变量的地址,最多可支持的环境变量数量为UEVENT_NUM_ENVP
int envp_idx; // 用于访问环境变量指针数组的index
char buf[UEVENT_BUFFER_SIZE]; // 保存环境变量的buffer,最大为UEVENT_BUFFER_SIZE
int buflen; // 访问buf的变量
};

struct kset_uevent_ops

kset_uevent_ops 是为kset量身订做的一个数据结构,里面包含filter和uevent两个回调函数,用处如下:

  • filter,当任何Kobject需要上报uevent时,它所属的kset可以通过该接口过滤,阻止不希望上报的event,从而达到从整体上管理的目的
  • name,该接口可以返回kset的名称。如果一个kset没有合法的名称,则其下的所有Kobject将不允许上报uvent
  • uevent,当任何Kobject需要上报uevent时,它所属的kset可以通过该接口统一为这些event添加环境变量。因为很多时候上报uevent时的环境变量都是相同的,因此可以由kset统一处理,就不需要让每个Kobject独自添加了
1
2
3
4
5
6
/* include/linux/kobject.h, line 123 */
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);
};

功能实现

uevent 模块提供了如下的API(这些API的实现是在 lib/kobject_uevent.c文件中):

1
2
3
4
5
6
7
/* include/linux/kobject.h, line 206 */
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp[]);

int add_uevent_var(struct kobj_uevent_env *env, const char *format, ...);

int kobject_action_type(const char *buf, size_t count, enum kobject_action *type);

kobject_uevent_env

kobject_uevent_env,以envp为环境变量,上报一个指定 action 的uevent,环境变量的作用是为执行用户空间程序指定运行环境。具体动作如下:

  • 查找kobj本身或者其parent是否从属于某个kset,如果不是,则报错返回
    • 由此可以说明,如果一个kobject没有加入kset,是不允许上报uevent的
  • 查看 kobj->uevent_suppress 是否设置,如果设置,则忽略所有的uevent上报并返回
    • 由此可知,可以通过Kobject的 uevent_suppress标志,管控Kobject的uevent的上报
  • 如果所属的kset有 uevent_ops->filter函数,则调用该函数,过滤此次上报
    • kset可以通过filter接口过滤不希望上报的event,从而达到整体的管理效果
  • 判断所属的kset是否有合法的名称,否则不允许上报uevent
  • 分配一个用于此次上报的、存储环境变量的buffer(结果保存在env指针中),并获得该Kobject在sysfs中路径信息(用户空间软件需要依据该路径信息在sysfs中访问它)
  • 调用 add_uevent_var接口,将Action、路径信息、subsystem等信息,添加到env指针中
  • 如果传入的envp不空,则解析传入的环境变量中,同样调用 add_uevent_var接口,添加到env指针中
  • 如果所属的kset存在u event_ops->uevent接口,调用该接口,添加kset统一的环境变量到env指针
  • 根据ACTION的类型,设置 kobj->state_add_uevent_sentkobj->state_remove_uevent_sent变量,以记录正确的状态
  • 调用 add_uevent_var接口,添加格式为SEQNUM=%llu的序列号
  • 如果定义了CONFIG_NET,则使用netlink发送该uevent
  • 以uevent_helper、subsystem以及添加了标准环境变量HOME=/,PATH=/sbin:/bin:/usr/sbin:/usr/bin的env指针为参数,调用kmod模块提供的call_usermodehelper函数,上报uevent。
    • 其中 uevent_helper的内容是由内核配置项 CONFIG_UEVENT_HELPER_PATH决定的,该配置项指定了一个用户空间程序,用于解析上报的uevent,例如”/sbin/hotplug”。
    • call_usermodehelper的作用,就是fork一个进程,以uevent为参数,执行 uevent_helper

kobject_uevent

kobject_uevent 和kobject_uevent_env功能一样,只是没有指定任何的环境变量。

add_uevent_var

add_uevent_var,以格式化字符的形式(类似printf、printk等),将环境变量copy到env指针中。

kobject_action_type

kobject_action_type,将enum kobject_action类型的Action,转换为字符串。

用户态 udevd

udevd 是 udev 机制的核心,负责动态在 /dev 下创建设备文件。

udev 运行在用户态,脱离驱动层的关联,基于这种设计实现,用户可以通过编写规则来动态删除和修改 /dev 下的设备文件,任意命名设备。每当 udevd 收到 uevent 时就会去匹配规则,一旦匹配上了,执行规则对应的操作。重要软件的 rule 存放在 /lib/udev/rules.d/ 下,用户自定义的规则放到 /etc/udev/rules.d/ 下,以 rules 为扩展名。命名规则类似于 grub 脚本,udev 将按数字前缀从小到大进行匹配,依次生效。

匹配主要基于几个字段:

  • SUBSYSTEM:设备类型
  • ACTION:设备触发的操作,如 add/change/remove
  • ATTR / ATTRS:设备的属性,如 class/vendor/ro/removable/size 等等,这些属性其实都能在 /sys 下对应的文件读出来。
  • KERNEL:kernel 对设备的命名。如 sd/input
  • ENV:环境设置,用来在多个 rule 之间传递信息

可用于以下用途:

  • 设置环境变量(有后续事件需要使用)
  • 在 /dev 下创建设备的别名(符号链接)
  • 运行特定的指令

当存储设备通过 USB 连接时,udevd 会通知 udisksd-daemon 去处理,挂载该设备。当网线插入时,udevd 会通知 NetworkManager-daemon 去进行相应的 IP 配置(dhclient)。

举个例子:

  1. 创建 udev 规则文件 /etc/udev/rules.d/81-usb-keyboard.rules ,内容如下:

    1
    ACTION=="add", ATTRS{name}=="*Keyboard*", SUBSYSTEMS=="input", SYMLINK+="my_kbd" RUN+="/sbin/insmod /my_kbd.ko"
  2. 当该 usb keyboard 插入时,收到 uevent :

    1
    2
    3
    4
    5
    ACTION = add
    name = xxxx-Keyboard-xxxx
    subsystems = input
    idVendor = xxxx
    idProduct = xxxx
  3. udevd 根据 uevent 匹配规则,发现 /etc/udev/rules.d/81-usb-keyboard.rules 匹配,因此运行该规则,创建一个设备的符号链接 /dev/my_kbd ,指向真正的设备,同时加载 my_kbd.ko 模块。

  4. 继续匹配规则,凡是符合的规则都会被运行

测试实战

在 linux 内核代码 samples/kobject 路径下中有 kobjectkset 的 示例代码,你也可以在我的 Github 中找到,下面结合上述介绍来理解这段代码:

kobject-example

  • 模块初始化是创建了名为 kobject-example 的 kobject
  • 为 kobject 关联了3个attribute,每个 attribute实现了自己的 showstore 函数
  • 模块卸载的时候使用 kobject_put 减少引用
kobject-example.c
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// SPDX-License-Identifier: GPL-2.0
/*
* Sample kobject implementation
*
* Copyright (C) 2004-2007 Greg Kroah-Hartman <greg@kroah.com>
* Copyright (C) 2007 Novell Inc.
*/
#include <linux/kobject.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/module.h>
#include <linux/init.h>

/*
* This module shows how to create a simple subdirectory in sysfs called
* /sys/kernel/kobject-example In that directory, 3 files are created:
* "foo", "baz", and "bar". If an integer is written to these files, it can be
* later read out of it.
*/

static int foo;
static int baz;
static int bar;

/*
* The "foo" file where a static variable is read from and written to.
*/
static ssize_t foo_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
return sprintf(buf, "%d\n", foo);
}

static ssize_t foo_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t count)
{
int ret;

ret = kstrtoint(buf, 10, &foo);
if (ret < 0)
return ret;

return count;
}

/* Sysfs attributes cannot be world-writable. */
static struct kobj_attribute foo_attribute =
__ATTR(foo, 0664, foo_show, foo_store);

/*
* More complex function where we determine which variable is being accessed by
* looking at the attribute for the "baz" and "bar" files.
*/
static ssize_t b_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
int var;

if (strcmp(attr->attr.name, "baz") == 0)
var = baz;
else
var = bar;
return sprintf(buf, "%d\n", var);
}

static ssize_t b_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t count)
{
int var, ret;

ret = kstrtoint(buf, 10, &var);
if (ret < 0)
return ret;

if (strcmp(attr->attr.name, "baz") == 0)
baz = var;
else
bar = var;
return count;
}

static struct kobj_attribute baz_attribute =
__ATTR(baz, 0664, b_show, b_store);
static struct kobj_attribute bar_attribute =
__ATTR(bar, 0664, b_show, b_store);


/*
* Create a group of attributes so that we can create and destroy them all
* at once.
*/
static struct attribute *attrs[] = {
&foo_attribute.attr,
&baz_attribute.attr,
&bar_attribute.attr,
NULL, /* need to NULL terminate the list of attributes */
};

/*
* An unnamed attribute group will put all of the attributes directly in
* the kobject directory. If we specify a name, a subdirectory will be
* created for the attributes with the directory being the name of the
* attribute group.
*/
static struct attribute_group attr_group = {
.attrs = attrs,
};

static struct kobject *example_kobj;

static int __init example_init(void)
{
int retval;

/*
* Create a simple kobject with the name of "kobject_example",
* located under /sys/kernel/
*
* As this is a simple directory, no uevent will be sent to
* userspace. That is why this function should not be used for
* any type of dynamic kobjects, where the name and number are
* not known ahead of time.
*/
example_kobj = kobject_create_and_add("kobject_example", kernel_kobj);
if (!example_kobj)
return -ENOMEM;

/* Create the files associated with this kobject */
retval = sysfs_create_group(example_kobj, &attr_group);
if (retval)
kobject_put(example_kobj);

return retval;
}

static void __exit example_exit(void)
{
kobject_put(example_kobj);
}

module_init(example_init);
module_exit(example_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Greg Kroah-Hartman <greg@kroah.com>");

加载到内核后,可以在 /sys/kernel 看到对应的目录和属性:

1
2
$ ls /sys/kernel/kobject_example/
bar baz foo

kset-example

  • 创建了名为 kset-example 的 kset,同时为 kset 关联了3个 kobject
  • foo 这三个 object 都是继承自 kobject,并且为其 kobj 成员指定了 ksetktype
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
/*
* Sample kset and ktype implementation
*
* Copyright (C) 2004-2007 Greg Kroah-Hartman <greg@kroah.com>
* Copyright (C) 2007 Novell Inc.
*/
#include <linux/kobject.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/init.h>

/*
* This module shows how to create a kset in sysfs called
* /sys/kernel/kset-example
* Then tree kobjects are created and assigned to this kset, "foo", "baz",
* and "bar". In those kobjects, attributes of the same name are also
* created and if an integer is written to these files, it can be later
* read out of it.
*/


/*
* This is our "object" that we will create a few of and register them with
* sysfs.
*/
struct foo_obj {
struct kobject kobj;
int foo;
int baz;
int bar;
};
#define to_foo_obj(x) container_of(x, struct foo_obj, kobj)

/* a custom attribute that works just for a struct foo_obj. */
struct foo_attribute {
struct attribute attr;
ssize_t (*show)(struct foo_obj *foo, struct foo_attribute *attr, char *buf);
ssize_t (*store)(struct foo_obj *foo, struct foo_attribute *attr, const char *buf, size_t count);
};
#define to_foo_attr(x) container_of(x, struct foo_attribute, attr)

/*
* The default show function that must be passed to sysfs. This will be
* called by sysfs for whenever a show function is called by the user on a
* sysfs file associated with the kobjects we have registered. We need to
* transpose back from a "default" kobject to our custom struct foo_obj and
* then call the show function for that specific object.
*/
static ssize_t foo_attr_show(struct kobject *kobj,
struct attribute *attr,
char *buf)
{
struct foo_attribute *attribute;
struct foo_obj *foo;

attribute = to_foo_attr(attr);
foo = to_foo_obj(kobj);

if (!attribute->show)
return -EIO;

return attribute->show(foo, attribute, buf);
}

/*
* Just like the default show function above, but this one is for when the
* sysfs "store" is requested (when a value is written to a file.)
*/
static ssize_t foo_attr_store(struct kobject *kobj,
struct attribute *attr,
const char *buf, size_t len)
{
struct foo_attribute *attribute;
struct foo_obj *foo;

attribute = to_foo_attr(attr);
foo = to_foo_obj(kobj);

if (!attribute->store)
return -EIO;

return attribute->store(foo, attribute, buf, len);
}

/* Our custom sysfs_ops that we will associate with our ktype later on */
static const struct sysfs_ops foo_sysfs_ops = {
.show = foo_attr_show,
.store = foo_attr_store,
};

/*
* The release function for our object. This is REQUIRED by the kernel to
* have. We free the memory held in our object here.
*
* NEVER try to get away with just a "blank" release function to try to be
* smarter than the kernel. Turns out, no one ever is...
*/
static void foo_release(struct kobject *kobj)
{
struct foo_obj *foo;

foo = to_foo_obj(kobj);
kfree(foo);
}

/*
* The "foo" file where the .foo variable is read from and written to.
*/
static ssize_t foo_show(struct foo_obj *foo_obj, struct foo_attribute *attr,
char *buf)
{
return sprintf(buf, "%d\n", foo_obj->foo);
}

static ssize_t foo_store(struct foo_obj *foo_obj, struct foo_attribute *attr,
const char *buf, size_t count)
{
int ret;

ret = kstrtoint(buf, 10, &foo_obj->foo);
if (ret < 0)
return ret;

return count;
}

/* Sysfs attributes cannot be world-writable. */
static struct foo_attribute foo_attribute =
__ATTR(foo, 0664, foo_show, foo_store);

/*
* More complex function where we determine which variable is being accessed by
* looking at the attribute for the "baz" and "bar" files.
*/
static ssize_t b_show(struct foo_obj *foo_obj, struct foo_attribute *attr,
char *buf)
{
int var;

if (strcmp(attr->attr.name, "baz") == 0)
var = foo_obj->baz;
else
var = foo_obj->bar;
return sprintf(buf, "%d\n", var);
}

static ssize_t b_store(struct foo_obj *foo_obj, struct foo_attribute *attr,
const char *buf, size_t count)
{
int var, ret;

ret = kstrtoint(buf, 10, &var);
if (ret < 0)
return ret;

if (strcmp(attr->attr.name, "baz") == 0)
foo_obj->baz = var;
else
foo_obj->bar = var;
return count;
}

static struct foo_attribute baz_attribute =
__ATTR(baz, 0664, b_show, b_store);
static struct foo_attribute bar_attribute =
__ATTR(bar, 0664, b_show, b_store);

/*
* Create a group of attributes so that we can create and destroy them all
* at once.
*/
static struct attribute *foo_default_attrs[] = {
&foo_attribute.attr,
&baz_attribute.attr,
&bar_attribute.attr,
NULL, /* need to NULL terminate the list of attributes */
};

/*
* Our own ktype for our kobjects. Here we specify our sysfs ops, the
* release function, and the set of default attributes we want created
* whenever a kobject of this type is registered with the kernel.
*/
static struct kobj_type foo_ktype = {
.sysfs_ops = &foo_sysfs_ops,
.release = foo_release,
.default_attrs = foo_default_attrs,
};

static struct kset *example_kset;
static struct foo_obj *foo_obj;
static struct foo_obj *bar_obj;
static struct foo_obj *baz_obj;

static struct foo_obj *create_foo_obj(const char *name)
{
struct foo_obj *foo;
int retval;

/* allocate the memory for the whole object */
foo = kzalloc(sizeof(*foo), GFP_KERNEL);
if (!foo)
return NULL;

/*
* As we have a kset for this kobject, we need to set it before calling
* the kobject core.
*/
foo->kobj.kset = example_kset;

/*
* Initialize and add the kobject to the kernel. All the default files
* will be created here. As we have already specified a kset for this
* kobject, we don't have to set a parent for the kobject, the kobject
* will be placed beneath that kset automatically.
*/
retval = kobject_init_and_add(&foo->kobj, &foo_ktype, NULL, "%s", name);
if (retval) {
kobject_put(&foo->kobj);
return NULL;
}

/*
* We are always responsible for sending the uevent that the kobject
* was added to the system.
*/
kobject_uevent(&foo->kobj, KOBJ_ADD);

return foo;
}

static void destroy_foo_obj(struct foo_obj *foo)
{
kobject_put(&foo->kobj);
}

static int __init example_init(void)
{
/*
* Create a kset with the name of "kset_example",
* located under /sys/kernel/
*/
example_kset = kset_create_and_add("kset_example", NULL, kernel_kobj);
if (!example_kset)
return -ENOMEM;

/*
* Create three objects and register them with our kset
*/
foo_obj = create_foo_obj("foo");
if (!foo_obj)
goto foo_error;

bar_obj = create_foo_obj("bar");
if (!bar_obj)
goto bar_error;

baz_obj = create_foo_obj("baz");
if (!baz_obj)
goto baz_error;

return 0;

baz_error:
destroy_foo_obj(bar_obj);
bar_error:
destroy_foo_obj(foo_obj);
foo_error:
kset_unregister(example_kset);
return -EINVAL;
}

static void __exit example_exit(void)
{
destroy_foo_obj(baz_obj);
destroy_foo_obj(bar_obj);
destroy_foo_obj(foo_obj);
kset_unregister(example_kset);
}

module_init(example_init);
module_exit(example_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Greg Kroah-Hartman <greg@kroah.com>");

加载模块到内核后,可以看到每个 kobject 都是一个目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ tree /sys/kernel/kset_example/
/sys/kernel/kset_example/
|-- bar
| |-- bar
| |-- baz
| `-- foo
|-- baz
| |-- bar
| |-- baz
| `-- foo
`-- foo
|-- bar
|-- baz
`-- foo

参考资料