在 Linux 内核的代码实现中,sysfs 是一种基于内存的虚拟文件系统机制,用于向用户空间提供内核的数据结构和其中属性变量的信息读取与修改功能。作为一个虚拟文件系统的接口,sysfs通过挂载在 /sys 目录下提供一系列文件和目录,来映射内核的各种属性和设备信息,使得开发者可以在开发运维的过程中方便地调试和监控内核模块。
基本原理
Sysfs 这一机制的实现离不开结构体kobject,即内核对象(kernel object)。在linux内核代码中,通常不会单独地创建一个kobject,而是将kobject嵌入到某一结构体中使用,从而实现对此结构体中的属性(attribute)按照sysfs机制读写的功能。在sysfs文件系统中内核元素与用户空间中显示的元素映射关系如下表:
内核 |
用户空间 |
kobject |
目录 |
attribute |
文件(通常为ASCII文本文件) |
relationship |
链接 |
其中链接是指在某些情况下,结构体中包含另一个具有kobject的结构体,则可以通过内核源码中创建链接的方法在对应的sysfs目录下建立链接,以方便相互之间的访问操作。
在kobject中有指向其父kobject的指针变量,从而管理其在文件系统中的层级结构关系,即kobject的目录应当存放在其父的目录下,根目录为/sys。
当包含着kobject的结构体初始化时,通常也要求初始化并向sysfs注册kobject,此时对应的文件系统中即会创建相应的目录与文件。
Sysfs会保持动态更新:一方面,当内核中对象的属性或者设备状态发生变化时,会对相应的文件和目录进行更新;另一方面,sysfs通过内核中的事件监听器来感知用户空间对/sys目录下文件的读写,在读写操作成功时文件系统会触发对应的事件,在内核中sysfs会调用关联的回调函数进行处理——更新或读取内核对象的属性。
在kobject结构体中,可以详细定义需要对用户空间提供链接的属性和属性操作权限(主要是读写),以及如何处理读写流程的函数(包括数据范围的限制、特定选项的选择、标志位的转化等)。
代码实现
Sysfs中映射的内核属性主要包括两个参数:名称name与给用户空间提供的操作权限mode。权限定义方式与文件系统中的定义类似,如用“0644”表示root用户对该属性有读写权限,其他用户只有读权限。
struct attribute {
const char *name;
umode_t mode;
...
};
通常来说,在初始化需要使用sysfs机制的结构体时,要求调用kobject_init对结构体中的kobject进行初始化:
void kobject_init(struct kobject *kobj, struct kobj_type *ktype){
...
kobject_init_internal(kobj);
kobj->ktype = ktype;
...
}
初始化过程除了对kobject基本的参数赋值外,主要是传递赋值了kobject中的kobj_type结构体:
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);
};
在kobj_type中包含了对内核对象属性与相关管理操作的回调函数的详细定义:
- release:指向释放函数的函数指针,当kobject的引用计数kref减至0时,调用此函数来释放内核对象以及相关资源;
- sysfs_ops:包含用于处理 Sysfs 文件系统操作的一组函数的结构体,主要是读取(show)和写入(store)属性文件等;
- default_attrs:指向一组对象属性的数组的指针,即内核对象需要通过sysfs提供给用户空间的属性集合;
- kobj_ns_type_operations:用于确定子对象的命名空间类型的函数指针;
- namespace:用于获取对象所属的命名空间的函数指针;
- get_ownership:获取内核对象所有者的信息的函数指针,uid和gid为指向内核对象的用户id与组id的指针。
使用方式
通常来说,想要快捷的使用sysfs机制只需要定义kobject中的部分属性功能即可,以下的示例代码提供了一种较为简洁的实现思路:
// kobject释放函数的实现
void example_release(struct kobject *kobj) {
// 在这里实现释放kobject及相关资源的逻辑
}
// kobject中对attribute的show函数的实现
ssize_t example_show(struct kobject *kobj, struct attribute *attr, char *buf) {
// 在这里实现读取属性的逻辑
// 将读取的结果写入buf中
}
// kobject中对attribute的store函数的实现
ssize_t example_store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t size) {
// 在这里实现写入属性的逻辑
// 根据buf中的内容进行相应的处理
}
// 定义需要对用户空间提供链接的属性
static struct attribute example_attr1 = {
.name = "attr_name1",
.mode = 0644
}
static struct attribute example_attr2 = {
.name = "attr_name2",
.mode = 0200
}
static struct attribute *example_attrs[] = {
&example_attr1,
&example_attr2,
NULL
}
// 将以上内容定义到kobj_type结构体中
struct kobj_type example_ktype = {
.release = example_release,
.sysfs_ops = &((const struct sysfs_ops) {
.show = example_show,
.store = example_store
}),
.default_attrs = example_attrs
}
// 在有实际意义的结构体中添加内核对象kobject
struct example{
...
struct kobject kobj;
}
// 在初始化对应结构体example时,初始化内核对象kobject,并传递kobj_type结构体
// 再调用kobject_add向sysfs注册
kobject_init(&example->kobj, &example_ktype);
kobject_add(&example->kobj, example_parent, "example_filename");
// 至此在内核模块初始化后文件系统/sys中对应的example_parent目录下会生成example_filename的目录
// 其中有example_attr1、example_attr2两个文件提供对应的读写功能