本期我们来深入学习 sys 文件系统。它与 proc 文件系统一样,同属于内核伪文件系统,主要用于向用户空间暴露内核数据。那么,为什么在有了 proc 文件系统之后,还要引入 sys 文件系统呢?让我们带着这个问题,开始今天的探索。
sys 文件系统在内核中的定义如下:
static struct file_system_type sysfs_fs_type = {
.name = "sysfs",
.init_fs_context = sysfs_init_fs_context,
.kill_sb = sysfs_kill_sb,
.fs_flags = FS_USERNS_MOUNT,
};
它是基于 ramfs 的内存型伪文件系统,不占用磁盘空间,随着内核的运行而动态生成和销毁。sysfs 是随 Linux 2.6 内核 一同引入的重要特性,通常挂载在 /sys 目录下。其核心目的是将内核设备模型以一种层次化的目录结构导出到用户空间。
简单概括 sys 和 proc 文件系统的区别:proc 文件系统用于暴露进程与内核的运行信息,数据结构相对松散;而 sys 文件系统则基于 kobject 设备模型统一管理硬件与驱动,层次结构规范清晰。
sys 文件系统的目录结构
典型的 /sys 目录结构如下所示:

相关目录及其主要功能如下:
/sys/block:块设备(如磁盘、分区)的视图目录,提供块设备的相关信息与配置。
/sys/bus:按照总线类型(如 PCI、USB、I2C)来组织和展示设备与驱动。
/sys/class:按照设备的功能进行分类(如网络设备、输入设备)。
/sys/dev:按照设备号(主/次设备号)来索引字符设备和块设备,便于快速定位。
/sys/devices:这是 sysfs 的核心,展示了系统中所有设备的真实物理拓扑结构,构成了最底层的设备树。
/sys/firmware:存放系统固件的相关信息。
/sys/fs:包含各种文件系统的运行时信息和配置。
/sys/kernel:提供内核的全局配置、调试接口以及各内核子系统的相关信息。
/sys/module:包含所有已加载内核模块的信息。
/sys/power:系统级的电源管理相关目录。
从内核视角看 sys 文件系统
与 proc 文件系统类似,sys 文件系统内部也维护着一棵文件树。理解这棵内部文件树是掌握 sysfs 的关键,其基本原理如下图所示。

图1:sys 文件系统内核实现原理
内核在初始化时会调用 start_kernel() 函数,该函数进而调用 sysfs_init() 来初始化 sys 内部文件树。这棵树中的每个节点(无论是文件还是目录)都由 struct kernfs_node 结构表示:
struct kernfs_node {
atomic_t count; /* 节点的全局引用计数 */
atomic_t active; /* 节点的活跃引用计数 */
struct kernfs_node *parent; /* 指向父节点(当前节点所属的目录)*/
const char *name; /* 节点名称(文件/目录名) */
struct rb_node rb; /* 红黑树节点*/
const void *ns; /* 命名空间标签 */
unsigned int hash; /* 哈希值(由ns + name计算)*/
union {
struct kernfs_elem_dir dir; /* 目录节点数据 */
struct kernfs_elem_symlink symlink;/* 符号链接节点数据 */
struct kernfs_elem_attr attr; /* 属性文件节点数据 */
};
void *priv; /* 节点私有数据指针 */
u64 id; /* 64位唯一ID,用于生成 inode 号 */
unsigned short flags; /* 节点标志 */
umode_t mode; /* 节点权限 */
struct kernfs_iattrs *iattr; /* 节点扩展属性 */
};
struct kernfs_node 是 Linux 内核中 kernfs 子系统的核心数据结构(注意:sysfs 正是基于 kernfs 构建的)。它封装了一个节点(目录、文件或符号链接)的所有元数据,包括名称、权限、父子关系、类型信息、引用计数和操作接口。
-
当 kernfs_node 表示一个目录时,联合体中的 dir 成员生效。dir 的数据类型是 struct kernfs_elem_dir:
struct kernfs_elem_dir {
/* 子目录数量 */
unsigned long subdirs;
/* 子节点红黑树根 */
struct rb_root children;
/* 所属的kernfs根目录 */
struct kernfs_root *root;
};
children 成员是一棵红黑树的根节点,该目录下的所有子节点都需要通过其 kernfs_node 结构中的 rb 成员插入到这棵树中,以实现高效的查找和遍历。
-
当 kernfs_node 表示一个属性文件时,联合体中的 attr 成员生效。attr 的数据类型是 struct kernfs_elem_attr:
struct kernfs_elem_attr {
/* 属性文件的读写操作函数表 */
const struct kernfs_ops *ops;
/* 打开状态链表 */
struct kernfs_open_node *open;
/* 文件大小 */
loff_t size;
};
attr 中维护了一个函数表指针 ops,这个函数表里定义了该属性文件的读写方法。
-
当 kernfs_node 表示一个符号链接时,联合体中的 symlink 成员生效。symlink 的数据类型是 struct kernfs_elem_symlink:
struct kernfs_elem_symlink {
/* 指向目标kernfs_node */
struct kernfs_node *target_kn;
};
符号链接的结构比较简单,target_kn 直接指向目标目录或属性文件的 kernfs_node。
重要:用户空间无法直接创建 sys 文件或目录,只能由内核来创建。内核提供了一系列 API 函数来完成这项工作,如下表所示。

表1:创建 sys 文件/目录的核心函数
sys 内部文件树初始化完毕后,start_kernel 函数会执行 initramfs 的 init 脚本来挂载 sys 文件系统,通常使用的挂载命令是:mount -t sysfs sysfs /sys。挂载完成后,用户程序就能够通过 VFS(虚拟文件系统)来访问 sysfs 中的文件了。
在 VFS 层面,dentry 与 sys 内部文件树的 kernfs_node 存在对应关系。当用户程序访问一个 sys 文件时,内核会基于对应的 kernfs_node 节点动态生成一个 struct inode 结构,并将访问该文件所需的方法(函数表)赋值给这个 inode。这样,标准的 VFS 访问路径就建立起来了。
核心概念:kobject、kset 与 attribute
要深入理解 sysfs,就必须厘清 kobject、kset 和 attribute 这三个核心概念。我们还是从内核源码的角度来分析它们的关系,如下图所示。

图2:kobject、kset、attribute 机制
先做一个概括:
- kobject:每个 kobject 对应 sys 文件系统中的一个目录。
- kset:是一个特殊的 kobject,用于管理一组同类型的 kobject(例如,管理所有块设备的 kobject)。
- attribute:描述 sys 属性文件的元数据(名称、权限),是创建 sys 文件的基础。
简单来说,一个 kset 可以包含多个 kobject,而一个 kobject 可以关联多个 attribute,它们共同构成了 sys 文件系统的树形层次结构。
深入剖析 kobject
kobject 即 struct kobject,它代表 sys 中的一个目录:
struct kobject {
const char *name; /* sysfs目录名 */
struct list_head entry; /* kset链表节点 */
struct kobject *parent; /* 指向父对象 */
struct kset *kset; /* 所属kset */
const struct kobj_type *ktype; /* 对象类型 */
struct kernfs_node *sd; /* sysfs目录节点 */
};
注意,kobject 中包含了一个 sd 成员(struct kernfs_node 类型)。可以这样理解:kobject 在某种程度上是 kernfs_node 的“上层封装”。kobject 通过这个 sd 成员将自己接入 sys 内部文件树。kobject 在 kernfs_node 的基础上进行了扩展,其 ktype 成员定义了该目录下属性文件的通用读写方法:
struct kobj_type {
struct sysfs_ops *sysfs_ops; /* sysfs文件读写规则 */
......
};
struct sysfs_ops {
ssize_t (*show)(...); /* 读取属性值 */
ssize_t (*store)(...); /* 写入属性值 */
};
(sys 文件具体的读写流程将在最后一节详细介绍。)
kobject 还有 kset 和 entry 两个重要成员。kset 指向该 kobject 所属的 kset 集合;当它被设置后,意味着该 kobject 加入了某个集合。entry 则是一个链表节点,kobject 通过它将自己插入到所属 kset 管理的 kobject 链表中。
理解 kset
kset 即 struct kset:
struct kset {
struct list_head list; /* 链表头:管理组内所有kobject */
struct kobject kobj; /* 内嵌kobject,将kset加入sysfs */
......
};
kset 内部包含了一个 kobject(kobj 成员),因此 kset 本身也是一个特殊的 kobject。除了具备 kobject 的目录表示能力外,kset 的核心功能是管理同一类别的 kobject。它维护了一个 kobject 链表(list 成员),同一类别的各个 kobject 通过其自身的 entry 成员插入到这个链表中。
例如,/sys/bus 目录本身就是一个 kset。bus 目录下的各个子目录(如 pci、usb)代表不同的总线类型,它们都是 kobject,并且都被加入到 bus 这个 kset 中进行统一管理。
attribute 及其扩展
attribute 即 struct attribute,它是属性文件的“蓝图”:
struct attribute {
const char *name; /* 属性文件名 */
umode_t mode; /* 文件权限 */
};
当 kernfs_node 表示一个属性文件时,其 priv(私有数据指针)通常会指向一个 attribute 结构。然而,基础的 attribute 只包含文件名和权限,并没有包含实际读写文件内容的函数。因此,在内核实际开发中,会对 attribute 进行扩展。常见的扩展结构有 struct device_attribute、struct kobj_attribute 等,如下图所示。

图3:attribute 扩展结构
这些扩展结构在基础 attribute 的基础上,增加了针对特定上下文的 show 和 store 回调函数。一个 sys 属性文件最终的读写行为,就由这些扩展结构中的回调函数具体实现来决定。
实例解析:读取 /sys/xxx 文件的完整流程
前面我们学习了 sys 文件系统的理论知识,现在通过一个具体的示例来巩固理解。下图展示了用户程序使用 cat /sys/xxx 命令读取一个 sys 属性文件时,内核中发生的完整调用链条。

图4:读取 sys 文件的内核流程示例
cat 命令首先会打开目标文件,这个过程大致如下:
- 在进程中申请一个未使用的文件描述符(fd)。
- 创建并初始化一个
file 对象。
- 建立 fd 和
file 对象的映射关系,并将此关系记录在进程的“已打开文件表”中。
file 对象的初始化依赖于 kernfs_node。内核根据文件路径 (/sys/xxx) 在 sys 内部文件树中找到目标文件对应的 kernfs_node。file 对象的 private_data 成员最终会指向这个 kernfs_node,从而建立起关联。同时,file 对象的 i_fop 成员会被设置为 sys 文件系统的操作函数表。
当 cat 命令开始读取文件内容时,会调用该函数表中的 kernfs_fop_read_iter() 函数。此后,内核会经历一连串的函数调用接力,例如:kernfs_fop_read_iter() -> sysfs_kf_read() -> my_sysfs_show() -> my_attr_show()。最终,内核会调用到扩展 attribute(例如 kobj_attribute)中定义的 show 回调函数(在示例中具体实现为 my_attr_show() 函数)。
在示例的 my_attr_show() 函数中,通过 snprintf() 将一个内核整型变量格式化为字符串,输出到内核缓冲区。随后,内核再将缓冲区中的数据拷贝到用户空间 cat 命令提供的缓冲区中。至此,一次完整的 /sys/xxx 文件读取操作就完成了。
希望这篇从内核角度深入剖析 sysfs 的文章,能帮助你更好地理解 Linux 设备模型的基石。如果你对这类底层技术原理感兴趣,欢迎在 云栈社区 与更多开发者交流探讨。