Skip to content

LV006-参考资料解析

这里有一篇内核提供的 configfs 参考文档,我们来学习一下。若笔记中有错误或者不合适的地方,欢迎批评指正 😃。

一、参考资料

参考的文档可以看这里:configfs.txt - Documentation/filesystems/configfs/configfs.txt - Linux source code v4.19.17

二、内容翻译

1. 什么是 configfs?

configfs 是一种基于 RAM 的文件系统, 提供了与 sysfs 功能相反的功能。 sysfs 是内核对象的基于文件系统的视图, 而 configfs 是内核对象(或 config_items) 的基于文件系统的管理器。

使用 sysfs 时, 在内核中创建一个对象(例如, 当发现设备时) , 并将其注册到 sysfs 中。 然后, 它的属性会出现在 sysfs 中, 允许用户空间通过 readdir(3)/read(2)读取属性。 它可能允许通过 write(2)修改某些属性。 重要的是, 对象在内核中创建和销毁, 内核控制 sysfs 表示的生命周期, 而 sysfs 只是对所有这些的一种窗口。

通过显式的用户空间操作(例如 mkdir(2)) , 可以创建一个 configfs config_item。 通过 rmdir(2)销毁它。属性在 mkdir(2)时出现, 并且可以通过 read(2)和 write(2)读取或修改。 与 sysfs 类似, readdir(3)查询项目和/或属性的列表。 可以使用 symlink(2)将项目组合在一起。 与 sysfs 不同, 表示的生命周期完全由用户空间驱动。 支持项目的内核模块必须响应此操作。

sysfs 和 configfs 可以并且应该在同一系统上同时存在。 它们不是彼此的替代品。

2. 使用 configfs

configfs 可以作为模块编译或集成到内核中。 您可以通过以下方式访问它:

shell
mount -t configfs none /config

除非也加载了客户端模块, 否则 configfs 树将为空。 这些模块将其项目类型注册为 configfs 的子系统。一旦加载了客户端子系统, 它将显示为/config 下的一个或多个子目录。 与 sysfs 一样, configfs 树始终存在,无论是否挂载在/config 上。

可以通过 mkdir(2)创建项目。 项目的属性也将同时出现。 readdir(3)可以确定属性是什么, read(2)可以查询其默认值, write(2)可以存储新值。 不要在一个属性文件中混合多个属性。

configfs 有两种类型的属性:

普通属性(Normal attributes)类似于 sysfs 属性, 是小型的 ASCII 文本文件, 最大大小为一页(PAGE_SIZE,在 i386 上为 4096)。 最好每个文件只使用一个值, 并且与 sysfs 相同的注意事项也适用。 configfs 期望 write(2)一次存储整个缓冲区。 当写入普通 configfs 属性时, 用户空间进程应首先读取整个文件, 修改要更改的部分, 然后将整个缓冲区写回。

二进制属性(Binary attributes)与 sysfs 二进制属性类似, 但语义上有一些细微的变化。 不适用 PAGE_SIZE 限制, 但整个二进制项必须适应单个内核 vmalloc 的缓冲区。 来自用户空间的 write(2)调用是缓冲的, 并且在最终关闭时将调用属性的 write_bin_attribute 方法, 因此用户空间必须检查 close(2)的返回代码以验证操作是否成功完成。 为避免恶意用户 OOM(Out of Memory) 内核, 有每个二进制属性的最大缓冲区值。

当需要销毁项目时, 使用 rmdir(2)将其删除。 如果任何其他项目通过 symlink(2)链接到它, 则无法销毁该项目。 可以使用 unlink(2)删除链接。

3. 配置 FakeNBD:一个示例

假设有一个网络块设备( Network Block Device, NBD) 驱动程序, 允许您访问远程块设备。 将其称为 FakeNBD。 FakeNBD 使用 configfs 进行配置。 显然, 将有一个方便的程序供系统管理员使用来配置 FakeNBD,但某种方式下, 该程序必须告知驱动程序。 这就是 configfs 的用武之地。

加载 FakeNBD 驱动程序时, 它会向 configfs 注册自己。 readdir(3)可以看到这一点:

shell
# ls /config
fakenbd

可以使用 mkdir(2)创建 fakenbd 连接。 名称是任意的, 但工具可能会对名称进行一些处理。 也许它是一个 UUID 或磁盘名称:

shell
# mkdir /config/fakenbd/disk1
# ls /config/fakenbd/disk1
target device rw

target 属性包含 FakeNBD 将连接到的服务器的 IP 地址。 device 属性是服务器上的设备。 可预测的是, rw 属性确定连接是只读还是读写。

shell
# echo 10.0.0.1 > /config/fakenbd/disk1/target
# echo /dev/sda1 > /config/fakenbd/disk1/device
# echo 1 > /config/fakenbd/disk1/rw

target 属性包含 FakeNBD 将连接到的服务器的 IP 地址。 device 属性是服务器上的设备。 可预测的是,rw 属性确定连接是只读还是读写。

4. 使用 configfs 进行编码

configfs 中的每个对象都是一个 config_item。 config_item 反映了子系统中的一个对象。 它具有与该对象上的值相匹配的属性。 configfs 处理该对象及其属性的文件系统表示, 使得子系统只需关注基本的 show/store 交互。

项目是在 config_group 内创建和销毁的。 组是共享相同属性和操作的项目集合。 项目通过 mkdir(2)创建, 并通过 rmdir(2)移除, 但 configfs 会处理这些操作。 组具有一组操作来执行这些任务。

子系统是客户端模块的顶层。 在初始化过程中, 客户端模块向 configfs 注册子系统, 该子系统将在 configfs 文件系统的顶部显示为一个目录。 子系统也是一个 config_group, 并且可以执行 config_group 的所有功能。

4.1 struct config_item

c
	struct config_item {
		char                    *ci_name;
		char                    ci_namebuf[UOBJ_NAME_LEN];
		struct kref             ci_kref;
		struct list_head        ci_entry;
		struct config_item      *ci_parent;
		struct config_group     *ci_group;
		struct config_item_type *ci_type;
		struct dentry           *ci_dentry;
	};

	void config_item_init(struct config_item *);
	void config_item_init_type_name(struct config_item *,
					const char *name,
					struct config_item_type *type);
	struct config_item *config_item_get(struct config_item *);
	void config_item_put(struct config_item *);

通常, struct config_item 被嵌入在一个容器结构中, 这个结构实际上代表了子系统正在做的事情。 该结构中的 config_item 部分是对象与 configfs 进行交互的方式。

无论是在源文件中静态定义还是由父 config_group 创建, config_item 都必须调用其中一个_init()函数。这将初始化引用计数并设置适当的字段。

所有使用 config_item 的用户都应该通过 config_item_get()对其进行引用, 并在完成后通过 config_item_put()释放引用。

单独一个 config_item 不能做更多的事情, 只能出现在 configfs 中。 通常, 子系统希望该项显示和/或存储属性, 以及其他一些操作。 为此, 它需要一个类型。

4.2 struct config_item_type

c
	struct configfs_item_operations {
		void (*release)(struct config_item *);
		int (*allow_link)(struct config_item *src,
				  struct config_item *target);
		void (*drop_link)(struct config_item *src,
				 struct config_item *target);
	};

	struct config_item_type {
		struct module                           *ct_owner;
		struct configfs_item_operations         *ct_item_ops;
		struct configfs_group_operations        *ct_group_ops;
		struct configfs_attribute               **ct_attrs;
		struct configfs_bin_attribute		**ct_bin_attrs;
	};

config_item_type 的最基本功能是定义可以在 config_item 上执行的操作。 所有动态分配的项目都需要提供 ct_item_ops-> release()方法。 当 config_item 的引用计数达到零时, 将调用此方法。

4.3 struct configfs_attribute

c
	struct configfs_attribute {
		char                    *ca_name;
		struct module           *ca_owner;
		umode_t                  ca_mode;
		ssize_t (*show)(struct config_item *, char *);
		ssize_t (*store)(struct config_item *, const char *, size_t);
	};

当 config_item 希望将属性显示为项目的 configfs 目录中的文件时, 它必须定义一个描述该属性的 configfs_attribute。 然后将属性添加到以 NULL 结尾的 config_item_type-> ct_attrs 数组中。 当项目出现在 configfs 中时, 属性文件将显示为 configfs_attribute-> ca_name 文件名。 configfs_attribute-> ca_mode 指定文件权限。

如果属性是可读的并提供了一个-> show 方法, 每当用户空间请求对属性进行 read(2)时, 该方法将被调用。 如果属性是可写的并提供了一个-> store 方法, 每当用户空间请求对属性进行 write(2)时, 该方法将被调用。

4.4 struct configfs_bin_attribute

c
	struct configfs_attribute {
		struct configfs_attribute	cb_attr;
		void				*cb_private;
		size_t				cb_max_size;
	};

当需要使用二进制数据块作为文件内容显示在项目的 configfs 目录中时, 可以使用二进制属性(binaryattribute) 。 为此, 将二进制属性添加到以 NULL 结尾的 config_item_type-> ct_bin_attrs 数组中, 当项目出现在 configfs 中时, 属性文件将显示为 configfs_bin_attribute-> cb_attr.ca_name 文件名。configfs_bin_attribute-> cb_attr.ca_mode 指定文件权限。

b_private 成员供驱动程序使用, 而 cb_max_size 成员指定要使用的 vmalloc 缓冲区的最大大小。

如果二进制属性是可读的, 并且 config_item 提供了 ct_item_ops-> read_bin_attribute()方法, 那么每当用户空间请求对属性进行 read(2)时, 该方法将被调用。 对于 write(2), 也会发生相反的情况。 读取/写入是缓冲的, 因此只会发生单个读取/写入; 属性本身不需要关心这一点。

4.5 struct config_group

config_item 不能独立存在。 创建 config_item 的唯一方法是通过在 config_group 上执行 mkdir(2)操作。这将触发创建子项。

c
	struct config_group {
		struct config_item		cg_item;
		struct list_head		cg_children;
		struct configfs_subsystem 	*cg_subsys;
		struct list_head		default_groups;
		struct list_head		group_entry;
	};

	void config_group_init(struct config_group *group);
	void config_group_init_type_name(struct config_group *group,
					 const char *name,
					 struct config_item_type *type);

config_group 结构包含一个 config_item。 适当配置该项意味着组可以作为一个独立的项进行操作。 然而, 它还可以做更多的事情: 它可以创建子项或子组。 这是通过在组的 config_item_type 上指定的组操作来实现的。

c
	struct configfs_group_operations {
		struct config_item *(*make_item)(struct config_group *group,
						 const char *name);
		struct config_group *(*make_group)(struct config_group *group,
						   const char *name);
		int (*commit_item)(struct config_item *item);
		void (*disconnect_notify)(struct config_group *group,
					  struct config_item *item);
		void (*drop_item)(struct config_group *group,
				  struct config_item *item);
	};

组通过提供 ct_group_ops-> make_item()方法来创建子项。 如果提供了该方法, 它将在组的目录中的 mkdir(2)操作中调用。 子系统分配一个新的 config_item(或更常见的是其容器结构) , 对其进行初始化,并将其返回给 configfs。 然后, configfs 将填充文件系统树以反映新的项。

如果子系统希望子项本身成为一个组, 子系统将提供 ct_group_ops-> make_group()。 其他操作与之相同,在组上使用组的_init()函数。

最后, 当用户空间对项或组调用 rmdir(2)时, 将调用 ct_group_ops-> drop_item()。 由于 config_group 也是一个 config_item, 因此不需要单独的 drop_group()方法。 子系统必须对在项分配时初始化的引用执行 config_item_put()。 如果子系统没有其他工作要执行, 可以省略 ct_group_ops-> drop_item()方法, configfs 将代表子系统对项执行 config_item_put()。

重要提示: drop_item()是 void 类型的, 因此无法失败。 当调用 rmdir(2)时, configfs 将从文件系统树中删除该项(假设没有子项) 。 子系统负责对此作出响应。 如果子系统在其他线程中引用该项, 则内存是安全的。 实际上, 该项从子系统的使用中消失可能需要一些时间。 但它已经从 configfs 中消失了。

当调用 drop_item()时, 项的链接已经被拆除。 它不再引用其父项, 并且在项层次结构中没有位置。 如果客户端在此拆除发生之前需要进行一些清理工作, 子系统可以实现 ct_group_ops-> disconnect_notify()方法。 该方法在 configfs 将项从文件系统视图中删除之后、 但在将项从其父组中删除之前调用。 与 drop_item()一样, disconnect_notify()是 void 类型的, 不会失败。 客户端子系统不应在此处删除任何引用, 因为它们仍然必须在 drop_item()中执行。

只要 config_group 仍然具有子项, 就无法删除它。 这在 configfs 的 rmdir(2)代码中实现。 不会调用-> drop_item(), 因为项尚未被删除。 rmdir(2)将失败, 因为目录不为空。

4.6 struct configfs_subsystem

子系统通常在 module_init 时间注册自身。 这告诉 configfs 将子系统显示在文件树中。

c
	struct configfs_subsystem {
		struct config_group	su_group;
		struct mutex		su_mutex;
	};

	int configfs_register_subsystem(struct configfs_subsystem *subsys);
	void configfs_unregister_subsystem(struct configfs_subsystem *subsys);

一个子系统由一个顶级的 config_group 和一个互斥锁组成。 config_group 是创建子 config_item 的地方。对于子系统来说, 这个组通常是静态定义的。 在调用 configfs_register_subsystem()之前, 子系统必须通过常规的 group_init()函数对组进行初始化, 并且还必须初始化互斥锁。

当注册调用返回时, 子系统将处于活动状态, 并且将通过 configfs 可见。 此时, 可以调用 mkdir(2), 子系统必须准备好接收该调用。

5. 示例

这些基本概念的最佳示例是在 configfs_sample.c - samples/configfs/configfs_sample.c - Linux source code v4.19.17 中的 simple_children 子系统/组和 simple_child 项。 它展示了一个简单的对象, 显示和存储属性, 以及一个简单的组来创建和销毁这些子项。

层次结构导航和子系统互斥锁

configfs 提供了一个额外的功能。 由于 config_groups 和 config_items 出现在文件系统中, 它们按层次结构排列。 子系统永远不会触及文件系统的部分, 但子系统可能对此层次结构感兴趣。 因此, 层次结构通过 config_group-> cg_children 和 config_item-> ci_parent 结构成员进行镜像。

子系统可以通过 cg_children 列表和 ci_parent 指针遍历子系统创建的树。 这可能与 configfs 对层次结构的管理发生竞争, 因此 configfs 使用子系统互斥锁来保护修改操作。 每当子系统想要遍历层次结构时, 必须在子系统互斥锁的保护下进行。

在新分配的项尚未链接到该层次结构时, 子系统将无法获取互斥锁。 类似地, 在放弃项尚未取消链接时, 它也无法获取互斥锁。 这意味着项的 ci_parent 指针在项位于 configfs 中时永远不会为 NULL, 并且项仅在其父项的 cg_children 列表中存在相同的时间段。 这允许子系统在持有互斥锁时信任 ci_parent 和 cg_children。

通过 symlink(2)进行项聚合

configfs 通过 group-> item 父/子关系提供了一个简单的组。 然而, 通常需要在父/子连接之外进行聚合。这是通过 symlink(2)实现的。

config_item 可以提供 ct_item_ops-> allow_link()和 ct_item_ops-> drop_link()方法。 如果存在-> allow_link()方法, 可以使用 config_item 作为链接源调用 symlink(2)。 这些链接仅允许在 configfs config_item 之间创建。任何在 configfs 文件系统之外的 symlink(2)尝试都将被拒绝。

调用 symlink(2)时, 将使用源 config_item 的-> allow_link()方法和自身和目标项作为参数。 如果源项允许链接到目标项, 则返回 0。 如果源项只希望链接到某种类型的对象(例如, 在其自己的子系统中) , 则可以拒绝链接。

在符号链接上调用 unlink(2)时, 将通过-> drop_link()方法通知源项。 与-> drop_item()方法一样, 这是一个无返回值的函数, 无法返回失败。 子系统负责响应该更改。

在任何项链接到其他项时, 无法删除 config_item, 也无法在有项链接到它时删除 config_item。在 configfs 中, 不允许存在悬空的符号链接。

自动创建的子组

新的 config_group 可能希望具有两种类型的子 config_item。 虽然可以通过-> make_item()中的魔术名称来编码这一点, 但更明确的做法是让用户空间看到这种分歧的方法。

configfs 提供了一种方法, 通过该方法可以在父级创建时自动在其中创建一个或多个子组。 因此, mkdir("parent")将导致创建 "parent"、 "parent/subgroup1", 一直到 "parent/subgroupN"。 类型为 1 的项现在可以在 "parent/subgroup1" 中创建, 类型为 N 的项可以在 "parent/subgroupN" 中创建。

这些自动创建的子组, 或默认组, 不排除父组的其他子项。 如果存在 ct_group_ops-> make_group(), 可以直接在父组上创建其他子组。

通过向父 config_group 结构添加它们, configfs 子系统可以指定默认组。 这是一个关于 configfs 的 C 代码示例, 它展示了 configfs 的一些基本概念和用法。

首先, 在代码中定义了一个名为 "simple_children" 的子系统/组和一个名为 "simple_child" 的项。 这个简单的对象展示了如何显示和存储属性, 并且使用一个简单的组来创建和销毁这些子项。

configfs 中的子系统和组是以层次结构排列的, 可以通过 config_group-> cg_children 和 config_item-> ci_parent 来遍历子系统创建的树。 为了保护修改操作, configfs 使用了子系统互斥锁。

在代码中还使用了 symlink(2)函数来创建项之间的链接。 config_item 可以提供 allow_link()和 drop_link()方法来控制链接的创建和删除。 使用 symlink(2)函数创建的链接只允许在 configfs 中的 config_item 之间创建, 对于 configfs 文件系统之外的 symlink(2)尝试会被拒绝。

此外, 代码中还介绍了自动创建的子组的概念。 通过在父级创建时自动创建一个或多个子组, 可以更方便地组织 config_item。 这些自动创建的子组不会排除父组的其他子项。

这只是一个简单的示例, 用于介绍 configfs 的基本概念和用法。 实际使用 configfs 时, 可能会有更复杂的场景和用法。

参考资料

configfs-用户空间控制的内核对象配置 - abin 在路上 - 博客园

14_configfs 的使用与内部机制 — Linux 设备驱动开发教程中心

Welcome to 100ask’s documentation — Linux 设备驱动开发教程中心