[TOC]

fs/sysfs/fs.c & dir.c 内核对象文件系统(Kernel Object Filesystem) 将内核对象层次结构导出到用户空间

历史与背景

这项技术是为了解决什么特定问题而诞生的?

Sysfs(System Filesystem)的诞生是为了解决一个在早期Linux内核中日益严重的问题:/proc文件系统的混乱

  • /proc的无序扩张/proc文件系统最初设计用于提供关于系统中正在运行的进程的信息(即/proc/[pid]目录)。然而,由于其便利性,内核开发者开始将各种与进程无关的系统信息、硬件状态和可调参数也塞入/proc,导致其结构混乱、内容混杂,缺乏统一的逻辑。
  • 缺乏结构化视图/proc中的信息是平面的、零散的,无法清晰地反映出系统中设备、驱动和总线之间复杂的层次关系和连接关系。例如,你很难从/proc中直观地看出某个USB设备连接在哪条总线上,以及它正在使用哪个驱动程序。

Sysfs是作为统一设备模型(Unified Device Model),或称为kobject模型,的一部分而被创造出来的。它的核心目标是提供一个严格结构化的、基于对象关系的文件系统视图来替代/proc中混乱的硬件信息部分,让用户空间能够清晰地看到内核是如何组织和看待系统中的每一个组件的。

它的发展经历了哪些重要的里程碑或版本迭代?

Sysfs的发展是Linux 2.6内核系列引入的最重大的变革之一。

  • 诞生于2.5开发内核:Sysfs和其背后的kobject模型在Linux 2.5的开发周期中被引入,并随着Linux 2.6.0的正式发布而成为内核的标准部分。这是一个革命性的变化,而不是一个渐进的迭代。
  • 清理/proc:在sysfs被引入后,社区花费了大量的精力将原本位于/proc中的硬件和系统设备信息迁移到sysfs中,让/proc回归其主要关注于进程信息的本职。
  • 催生udev:Sysfs提供的结构化信息和事件通知机制(通过uevent)是现代设备管理器(如udevsystemd-udevd)能够实现的基础。udev通过监视sysfs产生的事件,来动态地创建设备节点(/dev下的文件)、加载驱动模块和执行配置脚本,实现了真正的动态热插拔设备管理。

目前该技术的社区活跃度和主流应用情况如何?

Sysfs是现代Linux系统中一个极其核心、稳定且不可或缺的组成部分。它不是一个经常添加新功能的“活跃”子系统,而是作为内核与用户空间交互的基石被严格维护。

  • 主流应用
    • 所有现代Linux发行版都依赖sysfs进行设备管理、电源管理和系统状态监控。
    • systemdudev:完全基于sysfs来管理设备。
    • 容器技术(Docker, LXC):通过控制cgroups(它也通过一个类似sysfs的虚拟文件系统暴露)来隔离资源,而cgroups的管理接口与sysfs的设计思想一致。
    • 命令行工具:如lspci, lsusb, lsscsi等工具使用sysfs来获取设备信息并展示其拓扑结构。
    • 系统监控和调优:管理员和脚本通过读写sysfs中的文件来监控设备状态(如网络链接速度)或调整参数(如CPU调速器策略、屏幕亮度)。

核心原理与设计

它的核心工作原理是什么?

Sysfs的核心原理是将内核中的kobject(Kernel Object)层次结构直接映射为一个目录和文件的层次结构。它本身是一个虚拟文件系统,不存储任何实际数据。

  1. kobject核心:在内核中,有一个通用的数据结构struct kobject。任何需要被导出到sysfs的内核组件(如struct device, struct bus_type, struct driver)都会内嵌一个kobjectkobject提供了引用计数、父子关系指针和名称,从而在内核内存中形成一个巨大的对象树。
  2. 映射规则
    • 一个kobject映射为一个目录。目录的名称就是kobject的名称。
    • kobject之间的父子关系映射为目录的嵌套关系
    • kobject的属性(Attribute)映射为目录中的文件
  3. 文件操作的重定向:当用户空间的程序对sysfs中的一个文件进行读(read)或写(write)操作时,sysfs文件系统驱动并不会去磁盘上查找数据。相反,它会:
    • 找到该文件对应的kobject和attribute结构。
    • attribute结构中定义了两个函数指针:show()用于读操作,store()用于写操作。
    • 读操作:sysfs调用show()函数。这个show函数是由拥有该kobject的驱动程序实现的。驱动代码会读取硬件寄存器或内核变量,将其格式化为文本字符串,然后通过sysfs返回给用户。
    • 写操作:sysfs调用store()函数。驱动实现的store函数会接收来自用户的字符串,解析它,然后根据其内容去修改硬件寄存ators或内核变量。

简单来说,读写sysfs文件,实际上是在远程调用(RPC)内核中特定驱动程序提供的函数。

它的主要优势体现在哪些方面?

  • 结构清晰:提供了内核对象模型的严格、一致的视图。
  • “一文件一值”原则:每个文件通常只包含一个值,这使得脚本解析和程序处理变得极其简单。
  • 统一的交互接口:为用户空间提供了一个统一的、基于文件I/O的API来与各种不同的设备驱动进行交互,替代了大量混乱的ioctl调用。
  • 事件机制:提供了uevent机制,允许内核在对象状态改变(如设备添加/移除)时向用户空间发送通知。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 固化的ABI:一旦一个sysfs接口被加入到内核并发布,它就被视为一个稳定的应用程序二进制接口(ABI)。这意味着它几乎不能被修改或移除,否则会破坏依赖它的用户空间程序。这给内核开发带来了很大的维护负担。
  • 基于文本的低效性:所有数据都必须在内核和用户空间之间以文本字符串的形式来回转换,这对于大量数据的传输来说效率很低。
  • 缺乏原子性:如果要修改多个相互关联的属性,需要对多个文件进行多次写操作,这期间无法保证原子性。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

  • 查看设备拓扑:管理员想知道一个USB硬盘(如sdb)物理上插在哪里,可以查看/sys/block/sdb目录下的符号链接,它会指向devices目录下该设备的完整路径,清晰地展示其位于哪个USB端口、哪个控制器之下。
  • 修改设备参数:用户想要调节笔记本屏幕亮度,可以通过向/sys/class/backlight/acpi_video0/brightness文件写入一个数值来实现。
  • 控制设备状态:管理员需要临时解绑一个设备与其驱动的绑定关系(例如为了绑定到一个测试驱动),可以通过向/sys/bus/pci/drivers/my_driver/unbind文件写入设备的PCI地址来完成。
  • 电源管理:向/sys/power/state写入mem可以触发系统休眠。

是否有不推荐使用该技术的场景?为什么?

  • 大量数据传输:例如,从一个设备读取连续的数据流(如摄像头视频流)。这种场景应该使用字符设备(character device)接口,因为它更高效。
  • 复杂的、事务性的操作:例如,配置一个需要一次性提交多个参数才能生效的硬件设备。这种场景更适合使用ioctlnetlink套接字,因为它们可以传递复杂的二进制数据结构并保证操作的原子性。
  • 普通文件存储:sysfs是一个虚拟文件系统,不用于存储用户数据。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 sysfs procfs (/proc) debugfs configfs
主要目的 展示kobject模型,提供稳定的设备和系统结构视图。 提供进程信息和一些遗留的、杂项的系统信息。 内核调试。专为内核开发者提供一个临时的、不稳定的接口来查看或修改内核数据。 让用户空间创建和配置内核对象
ABI稳定性 稳定。被视为内核的正式ABI,不能轻易破坏。 部分稳定/proc/[pid]部分是稳定的,其他文件则稳定性不一。 不稳定。没有任何稳定性保证,接口可以随时改变。 稳定。作为配置接口,其结构也需要保持稳定。
结构 非常严格。基于kobject的层次结构,“一文件一值”。 混合结构。部分有结构(进程目录),部分是单一文件包含多项非结构化信息。 无固定结构。开发者可以随意创建目录和文件。 严格。基于目录和属性来管理对象,但操作方向与sysfs相反。
数据流向 内核 -> 用户空间。主要用于从内核导出(export)信息。 内核 -> 用户空间 双向,但无任何规则。 用户空间 -> 内核。主要用于从用户空间创建(create)和配置内核对象。
生命周期 内核对象存在,sysfs中的条目就存在(内核驱动)。 进程存在,目录就存在;或内核模块加载后创建。 开发者在代码中手动创建和移除。 用户空间程序通过mkdirrmdir来创建和销毁内核对象。
典型例子 /sys/devices, /sys/class /proc/1234/status, /proc/meminfo /sys/kernel/debug/pinctrl /sys/kernel/config/usb_gadget

include/linux/sysfs.h

sysfs_get 函数用于获取 sysfs 节点的引用计数

1
2
3
4
5
static inline struct kernfs_node *sysfs_get(struct kernfs_node *kn)
{
kernfs_get(kn);
return kn;
}

fs/sysfs/mount.c

sysfs_init 设置和注册 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
26
27
28
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,
};

int __init sysfs_init(void)
{
int err;
/* 创建 sysfs 的根节点(sysfs_root)。
KERNFS_ROOT_EXTRA_OPEN_PERM_CHECK 是一个标志,
用于启用额外的权限检查机制,确保文件系统的安全性 */
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 转换为内核对象节点 */
sysfs_root_kn = kernfs_root_to_node(sysfs_root);
/* 将 sysfs_fs_type 注册为文件系统类型 */
err = register_filesystem(&sysfs_fs_type);
if (err) {
kernfs_destroy_root(sysfs_root);
return err;
}

return 0;
}

fs/sysfs/dir.c

sysfs_create_dir_ns 为对象创建带有命名空间标签的目录

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
/**
* sysfs_create_dir_ns - 为对象创建带有命名空间标签的目录
* @kobj: 需要为其创建目录的对象
* @ns: 要使用的命名空间标签
*/
int sysfs_create_dir_ns(struct kobject *kobj, const void *ns)
{
struct kernfs_node *parent, *kn;
kuid_t uid;
kgid_t gid;

if (WARN_ON(!kobj))
return -EINVAL;

if (kobj->parent)
parent = kobj->parent->sd;
else
/* 如果没有父对象,则使用 sysfs 的根节点(sysfs_root_kn) */
parent = sysfs_root_kn;

if (!parent)
return -ENOENT;

/* 调用 kobject_get_ownership 获取 kobject 的所有者(uid 和 gid),用于设置目录的权限 */
kobject_get_ownership(kobj, &uid, &gid);

/* 在指定的父目录中创建一个新的目录。
使用 kobject_name(kobj) 作为目录名称,0755 作为权限,并关联命名空间标签(ns)*/
/* 0755 的每一位数字表示一组用户的权限:
0:特殊权限位(通常为 0,表示没有特殊权限)。
7:所有者权限,表示读、写和执行权限。
5:所属组权限,表示读和执行权限。
5:其他用户权限,表示读和执行权限。
权限分解:

7(所有者权限):读(r)、写(w)、执行(x)。
5(所属组权限):读(r)、执行(x)。
5(其他用户权限):读(r)、执行(x)。 */
kn = kernfs_create_dir_ns(parent, kobject_name(kobj), 0755, uid, gid,
kobj, ns);
if (IS_ERR(kn)) {
if (PTR_ERR(kn) == -EEXIST)
sysfs_warn_dup(parent, kobject_name(kobj));
return PTR_ERR(kn);
}

kobj->sd = kn;
return 0;
}

kernfs_get 获取 kernfs_node 的引用计数

1
2
3
4
5
6
7
8
9
10
11
12
/**
* kernfs_get - 获取 kernfs_node 的引用计数
* @kn: 目标 kernfs_node
*/
void kernfs_get(struct kernfs_node *kn)
{
if (kn) {
WARN_ON(!atomic_read(&kn->count));
atomic_inc(&kn->count);
}
}
EXPORT_SYMBOL_GPL(kernfs_get);

fs/sysfs/file.c

sysfs_create_bin_file: 为kobject创建一个二进制属性文件

此函数是一个通用的内核API,作用是在sysfs中为一个给定的内核对象(kobject,表现为目录)创建一个代表二进制数据(binary data)的属性文件。与普通的文本属性文件不同,二进制属性文件允许用户空间程序直接读写大块的、非文本格式的原始数据,例如固件、校准数据或硬件寄存器快照。

这个函数本质上是一个封装器(wrapper),它首先对输入参数进行有效性检查,然后获取该kobject应该具有的文件所有权信息(用户ID和组ID),最后调用一个更底层的内部函数sysfs_add_bin_file_mode_ns来完成实际的文件创建工作。

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
/**
* sysfs_create_bin_file - 为一个对象创建一个二进制文件.
* @kobj: 要添加文件的对象(kobject). 在sysfs中表现为一个目录.
* @attr: 二进制属性的描述符.
*/
/*
* EXPORT_SYMBOL_GPL(sysfs_create_bin_file);
* 这是一个宏, 用于将此函数的符号导出到内核的符号表中.
* 这意味着其他可加载的内核模块(LKM)可以调用这个函数.
* "GPL"后缀表示只有遵循GPL兼容许可证的模块才能使用此符号,
* 这是为了维护内核代码的许可证一致性.
*/
int sysfs_create_bin_file(struct kobject *kobj,
const struct bin_attribute *attr)
{
/*
* 定义两个变量, 分别用于存储文件的用户ID (uid) 和组ID (gid).
* kuid_t 和 kgid_t 是内核中用于表示用户和组ID的特殊类型.
*/
kuid_t uid;
kgid_t gid;

/*
* WARN_ON 是一个内核调试宏. 它检查括号内的条件是否为真.
* 如果条件为真 (即 kobj, kobj->sd, 或 attr 有一个是NULL指针),
* 它会在内核日志中打印一个警告信息和函数调用栈, 这对于调试非常有用.
* 这里的检查是为了确保传入的 kobject 及其内部的 sysfs 目录项(sd)和属性描述符都是有效的.
* 如果检查失败, 函数返回 -EINVAL (表示无效参数).
*/
if (WARN_ON(!kobj || !kobj->sd || !attr))
return -EINVAL;

/*
* 调用 kobject_get_ownership 函数.
* 这个函数会查询 kobj 的所有权信息, 并将结果填充到 uid 和 gid 变量的地址中.
* 在sysfs中, 文件通常会继承其父目录(kobject)的所有权.
*/
kobject_get_ownership(kobj, &uid, &gid);
/*
* 调用 sysfs_add_bin_file_mode_ns, 这是一个更底层的内部函数, 负责实际的文件创建.
* 参数说明:
* kobj->sd: 指向 kobject 内部的 sysfs 目录项 (sysfs_dirent), 这是文件系统内部的实际表示.
* attr: 传递二进制属性描述符.
* attr->attr.mode: 从属性描述符中提取文件模式 (即权限, 如 0444).
* attr->size: 从属性描述符中提取文件大小.
* uid, gid: 传递刚刚获取到的所有权信息.
* NULL: 传递一个空的命名空间(namespace)标签, 表示在默认的命名空间中创建.
* 函数将底层函数的返回值直接作为自己的返回值.
*/
return sysfs_add_bin_file_mode_ns(kobj->sd, attr, attr->attr.mode,
attr->size, uid, gid, NULL);
}
/*
* 将函数符号 sysfs_create_bin_file 导出, 使其对GPL兼容的内核模块可见.
*/
EXPORT_SYMBOL_GPL(sysfs_create_bin_file);

sysfs_add_bin_file_mode_ns: 创建一个具有指定模式、所有权和命名空间的二进制sysfs文件

该函数是sysfs文件系统中创建二进制属性文件的底层核心实现。它由上层函数sysfs_create_bin_file调用,负责处理文件创建的全部逻辑:根据驱动程序提供的回调函数(read, write, mmap)来选择合适的内核文件操作集(kernfs_ops),准备所有必要的元数据(权限、所有者、大小等),并最终调用更底层的kernfs(内核文件系统)接口来在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
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
/*
* 函数 sysfs_add_bin_file_mode_ns
* @parent: 指向父 kernfs_node 的指针. kernfs_node 是 sysfs (以及其他伪文件系统) 的内部目录项表示.
* @battr: 指向 const struct bin_attribute 的指针, 包含了二进制文件的所有属性, 包括回调函数和名称.
* @mode: 文件模式(权限), 为 umode_t 类型.
* @size: 文件的大小, 为 size_t 类型.
* @uid: 文件的用户ID, 为 kuid_t 类型.
* @gid: 文件的组ID, 为 kgid_t 类型.
* @ns: 指向 const void 的指针, 代表文件所属的命名空间标签, 通常为 NULL.
* @return: 成功时返回0, 失败时返回负的错误码.
*/
int sysfs_add_bin_file_mode_ns(struct kernfs_node *parent,
const struct bin_attribute *battr, umode_t mode, size_t size,
kuid_t uid, kgid_t gid, const void *ns)
{
/*
* 从二进制属性结构体(battr)中获取其内嵌的通用属性结构体(attr).
* struct bin_attribute 的第一个成员就是 struct attribute.
*/
const struct attribute *attr = &battr->attr;
/*
* 定义一个指向 lock_class_key 的指针 key, 并初始化为 NULL.
* lock_class_key 用于内核的锁依赖性验证器 (lockdep).
*/
struct lock_class_key *key = NULL;
/*
* 定义一个指向 kernfs_ops 的指针 ops.
* kernfs_ops 结构体包含了一组文件操作的回调函数指针 (如 read, write, mmap).
*/
const struct kernfs_ops *ops;
/*
* 定义一个指向 kernfs_node 的指针 kn.
* 它将用来接收新创建的文件节点.
*/
struct kernfs_node *kn;

/*
* 进行有效性检查: 一个驱动不能同时提供旧版(read)和新版(read_new)的回调函数.
* 这是为了避免API使用的混淆. 如果同时提供, 返回无效参数错误.
*/
if (battr->read && battr->read_new)
return -EINVAL;

/*
* 对写操作也进行同样的有效性检查.
*/
if (battr->write && battr->write_new)
return -EINVAL;

/*
* 这是此函数的核心逻辑之一: 根据驱动提供的回调函数来选择合适的 kernfs_ops 集合.
* 如果驱动提供了 mmap 回调, 则使用支持 mmap 的操作集 sysfs_bin_kfops_mmap.
*/
if (battr->mmap)
ops = &sysfs_bin_kfops_mmap;
/*
* 如果同时提供了读(read或read_new)和写(write或write_new)回调, 则使用读写操作集 sysfs_bin_kfops_rw.
*/
else if ((battr->read || battr->read_new) && (battr->write || battr->write_new))
ops = &sysfs_bin_kfops_rw;
/*
* 如果只提供了读回调, 则使用只读操作集 sysfs_bin_kfops_ro.
*/
else if (battr->read || battr->read_new)
ops = &sysfs_bin_kfops_ro;
/*
* 如果只提供了写回调, 则使用只写操作集 sysfs_bin_kfops_wo.
*/
else if (battr->write || battr->write_new)
ops = &sysfs_bin_kfops_wo;
/*
* 如果没有提供任何读写回调, 则使用一个空操作集. 在sysfs中会创建一个零字节的文件.
*/
else
ops = &sysfs_file_kfops_empty;

/*
* 这是一个条件编译块, 仅当内核配置了 CONFIG_DEBUG_LOCK_ALLOC 时才生效.
* 它的作用是为锁依赖性验证器提供信息.
*/
#ifdef CONFIG_DEBUG_LOCK_ALLOC
/*
* 如果属性没有被标记为忽略锁检查 (ignore_lockdep)...
*/
if (!attr->ignore_lockdep)
/*
* ...则获取一个锁类型密钥. 优先使用驱动明确提供的 key,
* 否则使用属性结构体中静态定义的 skey.
*/
key = attr->key ?: (struct lock_class_key *)&attr->skey;
#endif

/*
* 调用底层的 kernfs 接口 __kernfs_create_file 来创建文件.
* parent: 父目录节点.
* attr->name: 文件名.
* mode & 0777: 文件权限, 使用位与操作确保只传递权限位, 去除文件类型等其他信息.
* uid, gid: 用户和组ID.
* size: 文件大小.
* ops: 上面选择的文件操作集.
* (void *)attr: 一个私有数据指针, 这里将整个属性描述符传递过去, 供ops中的回调函数使用.
* ns: 命名空间.
* key: 传递给锁验证器的锁类型密钥.
*/
kn = __kernfs_create_file(parent, attr->name, mode & 0777, uid, gid,
size, ops, (void *)attr, ns, key);
/*
* 检查 __kernfs_create_file 的返回值是否是一个错误.
* 内核函数通常通过返回一个编码了错误码的特殊指针来表示错误.
*/
if (IS_ERR(kn)) {
/*
* 如果错误是 -EEXIST (文件已存在)...
*/
if (PTR_ERR(kn) == -EEXIST)
/*
* ...则调用 sysfs_warn_dup 在内核日志中打印一个重复创建的警告.
*/
sysfs_warn_dup(parent, attr->name);
/*
* 从指针中提取错误码并返回.
*/
return PTR_ERR(kn);
}
/*
* 如果 kn 不是一个错误指针, 说明文件创建成功, 返回0.
*/
return 0;
}