什么是configfs
configfs 是一个基于 RAM 的文件系统,提供与 sysfs 相反的功能。sysfs 是一个基于文件系统的内核对象视图,而 configfs 是一个基于文件系统的内核对象管理器,即 config_items。
使用 sysfs,可以在内核中创建一个对象(例如,当发现设备时),并将其注册到 sysfs 中。然后,它的属性出现在 sysfs 中,允许用户空间通过 readdir(3)/read(2) 读取属性。它可能允许通过 write(2) 修改某些属性。重点是,对象是在内核中创建和销毁的,内核控制 sysfs 表示的生命周期,而 sysfs 仅仅是所有这些的一个窗口。
configfs config_item 通过显式用户空间操作创建:mkdir(2)。它通过 rmdir(2) 销毁。属性在 mkdir(2) 时出现,可以通过 read(2) 和 write(2) 读取或修改。与 sysfs 一样,readdir(3) 查询项目和/或属性的列表。symlink(2) 可用于将项目组合在一起。与 sysfs 不同,表示的生命周期完全由用户空间驱动。支持项目的内核模块必须对此做出响应。
sysfs 和 configfs 可以且应该共存于同一系统中。两者不可互相替代。
使用configfs
configfs 可以编译为模块或编译到内核中。你可以这样访问它:
mount -t configfs none /config
除非还加载了客户端模块,否则 configfs 树将为空。这些模块将其项类型作为子系统注册到 configfs。加载客户端子系统后,它将作为子目录(或多个子目录)出现在 /config 下。与 sysfs 一样,无论是否安装在 /config 上,configfs 树始终存在。
通过 mkdir(2) 创建项目。此时还会显示项目的属性。readdir(3) 可以确定属性是什么,read(2) 可以查询其默认值,write(2) 可以存储新值。不要在一个属性文件中混合多个属性。
configfs 属性有两种类型:
-
普通属性与 sysfs 属性类似,都是小型 ASCII 文本文件,最大大小为一页(PAGE_SIZE,在 i386 上为 4096)。最好每个文件只使用一个值,并且适用 sysfs 中的相同注意事项。Configfs 要求 write(2) 一次存储整个缓冲区。在写入普通 configfs 属性时,用户空间进程应首先读取整个文件,修改它们希望更改的部分,然后将整个缓冲区写回。
-
二进制属性,与 sysfs 二进制属性有些相似,但在语义上略有不同。PAGE_SIZE 限制不适用,但整个二进制项必须适合单个内核 vmalloc 缓冲区。来自用户空间的 write(2) 调用被缓冲,属性的 write_bin_attribute 方法将在最终关闭时调用,因此用户空间必须检查 close(2) 的返回代码以验证操作是否成功完成。为了避免恶意用户 OOMing 内核,每个二进制属性都有一个最大缓冲区值。
当需要销毁某个项目时,使用 rmdir(2) 将其移除。如果任何其他项目与该项目有链接(通过 symlink(2)),则该项目无法被销毁。可以通过 unlink(2) 移除链接。
配置FakeNBD:一个示例
假设有一个网络块设备 (NBD) 驱动程序,它允许您访问远程块设备。称之为 FakeNBD。FakeNBD 使用 configfs 进行配置。显然,系统管理员会使用一个不错的程序来配置 FakeNBD,但该程序必须以某种方式将其告知驱动程序。这就是 configfs 的作用所在。
当 FakeNBD 驱动程序被加载时,它会向 configfs 注册自身。readdir(3) 可以正常看到这种情况:
# ls /config
fakenbd
可以使用 mkdir(2) 创建 fakenbd 连接。名称是任意的,但该工具可能会使用该名称。可能是 uuid 或磁盘名称:
# mkdir /config/fakenbd/disk1
# ls /config/fakenbd/disk1
target device rw
target 属性包含 FakeNBD 将要连接的服务器的 IP 地址。device 属性是服务器上的设备。可以预见的是,rw 属性决定了连接是只读的还是读写的:
# echo 10.0.0.1 > /config/fakenbd/disk1/target
# echo /dev/sda1 > /config/fakenbd/disk1/device
# echo 1 > /config/fakenbd/disk1/rw
就是这样。就这些了。现在设备已配置完毕,通过 shell 即可。
使用configfs进行编码
configfs 中的每个对象都是一个 config_item。config_item 反映子系统中的对象。它具有与该对象上的值匹配的属性。configfs 处理该对象及其属性的文件系统表示,允许子系统忽略除基本显示/存储交互之外的所有内容。
项目在 config_group 中创建和销毁。组是共享相同属性和操作的项目的集合。项目由 mkdir(2) 创建,由 rmdir(2) 删除,但 configfs 会处理这些。组有一组操作来执行这些任务
子系统是客户端模块的顶层。在初始化期间,客户端模块将子系统注册到 configfs,子系统作为目录出现在 configfs 文件系统的顶层。子系统也是一个 config_group,可以执行 config_group 可以执行的所有操作。
struct config_item(配置项)
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 中。通常,子系统希望该项显示和/或存储属性等。为此,它需要一个类型。
config_item_type
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 的引用计数达到零时,将调用此方法。
configfs_attribute
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) 操作时,都会调用该方法。
configfs_bin_attribute
struct configfs_bin_attribute {struct configfs_attribute cb_attr;void *cb_private;size_t cb_max_size;
};
当需要使用二进制 blob 作为项目 configfs 目录中文件的内容出现时,将使用二进制属性。为此,将二进制属性添加到以 NULL 结尾的数组 config_item_type->ct_bin_attrs,并且项目出现在 configfs 中,属性文件将与 configfs_bin_attribute->cb_attr.ca_name 文件名一起出现。configfs_bin_attribute->cb_attr.ca_mode 指定文件权限。cb_private 成员供驱动程序使用,而 cb_max_size 成员指定要使用的最大 vmalloc 缓冲区量。
如果二进制属性可读,并且 config_item 提供了 ct_item_ops->read_bin_attribute() 方法,则每当用户空间请求对属性执行 read(2) 操作时,都会调用该方法。对于 write(2) 操作,情况正好相反。读取/写入操作被缓冲,因此只会发生一次读取/写入操作;属性本身无需关心它。
struct config_group(配置组)
config_item 不能独立存在。创建 config_item 的唯一方法是通过 config_group 上的 mkdir(2)。这将触发子项的创建:
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,
config_group 结构包含一个 config_item。正确配置该项目意味着组可以作为其自身的项目。但是,它可以做更多:它可以创建子项目或组。这是通过组的 config_item_type 上指定的组操作来实现的:
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);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()。其他所有操作均相同,使用组上的 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() 为空,因此不会失败。调用 rmdir(2) 时,configfs 将从文件系统树中删除该项目(假设它没有子项来保持忙碌)。子系统负责对此做出响应。如果子系统在其他线程中引用了该项目,则内存是安全的。该项目实际上从子系统的使用中消失可能需要一些时间。但它已从 configfs 中消失。
调用 drop_item() 时,项目的链接已被拆除。它不再引用其父级,也不在项目层次结构中。如果客户端需要在拆除之前进行一些清理,子系统可以实现 ct_group_ops->disconnect_notify() 方法。该方法在 configfs 从文件系统视图中删除项目之后但在从其父组中删除项目之前调用。与 drop_item() 一样,disconnect_notify() 为空且不能失败。客户端子系统不应在此处删除任何引用,因为它们仍必须在 drop_item() 中执行此操作。
当 config_group 仍有子项时,无法将其删除。这是在 configfs rmdir(2) 代码中实现的。不会调用 ->drop_item(),因为项尚未删除。rmdir(2) 将失败,因为目录不为空。
configfs_subsystem
子系统必须注册自身,通常在 module_init 时。这会告诉 configfs 让子系统出现在文件树中:
struct configfs_subsystem {struct config_group su_group;struct mutex su_mutex;
};int configfs_register_subsystem(struct configfs_subsystem *subsys);
void configfs_unregister_subsystem
子系统由顶层 config_group 和互斥锁组成。子 config_items 就是在组内创建的。对于子系统,该组通常是静态定义的。在调用 configfs_register_subsystem() 之前,子系统必须通过常用的 group _init() 函数初始化组,并且还必须初始化互斥锁。
当 register 调用返回时,子系统处于活动状态,并且将通过 configfs 可见。此时,可以调用 mkdir(2),并且子系统必须为此做好准备。
一个例子
这些基本概念的最佳示例是 samples/configfs/configfs_sample.c 中的 simple_children 子系统/组和 simple_child 项。它展示了一个显示和存储属性的简单对象,以及一个创建和销毁这些子项的简单组。
层次结构导航和子系统互斥锁
configfs 还提供了额外的好处。由于 config_groups 和 config_items 出现在文件系统中,因此它们按层次结构排列。子系统永远不会接触文件系统部分,但子系统可能对此层次结构感兴趣。因此,层次结构通过 config_group->cg_children 和 config_item->ci_parent 结构成员进行镜像。
子系统可以浏览 cg_children 列表和 ci_parent 指针来查看子系统创建的树。这可能会与 configfs 对层次结构的管理发生争用,因此 configfs 使用子系统互斥锁来保护修改。每当子系统想要浏览层次结构时,它都必须在子系统互斥锁的保护下进行。
当新分配的项目尚未链接到此层次结构时,子系统将无法获取互斥锁。同样,当删除的项目尚未取消链接时,它将无法获取互斥锁。这意味着当项目位于 configfs 中时,项目的 ci_parent 指针永远不会为 NULL,并且项目只会在其父级的 cg_children 列表中停留相同的时间。这允许子系统在 ci_parent 和 cg_children 持有互斥锁时信任它们。
通过符号链接进行项目聚合
configfs 通过 group->item 父/子关系提供一个简单的组。然而,更大的环境通常需要在父/子连接之外进行聚合。这是通过 symlink(2) 实现的。
config_item 可以提供 ct_item_ops->allow_link() 和 ct_item_ops->drop_link() 方法。如果 ->allow_link() 方法存在,则可以调用 symlink(2),并以 config_item 作为链接源。这些链接仅允许在 configfs config_items 之间进行。任何在 configfs 文件系统之外的 symlink(2) 尝试都将被拒绝。
调用 symlink(2) 时,源 config_item 的 ->allow_link() 方法将使用其自身和目标项进行调用。如果源项允许链接到目标项,则返回 0。如果源项只想链接到特定类型的对象(例如,在其自己的子系统中),则可能希望拒绝链接。
当在符号链接上调用 unlink(2) 时,将通过 ->drop_link() 方法通知源项。与 ->drop_item() 方法一样,这是一个 void 函数,不能返回失败。子系统负责响应更改。
当 config_item 链接到其他项时,无法将其删除;当有项链接到 config_item 时,也无法将其删除。configfs 中不允许有悬空符号链接。
自动创建子组
新的 config_group 可能希望拥有两种类型的子 config_items。虽然这可以通过 ->make_item() 中的魔法名称来编码,但更明确的做法是使用一种用户空间可以发现这种分歧的方法。
configfs 提供了一种方法,在创建父组时,会自动在父组中创建一个或多个子组,而不是让其中某些项目的行为与其他项目不同。因此,mkdir(“parent”) 会生成“parent”、“parent/subgroup1”直至“parent/subgroupN”。现在可以在“parent/subgroup1”中创建类型 1 的项目,在“parent/subgroupN”中创建类型 N 的项目。
这些自动子组或默认组不会排除父组的其他子组。如果 ct_group_ops->make_group() 存在,则可以直接在父组上创建其他子组。
configfs 子系统通过使用 configfs_add_default_group() 函数将默认组添加到父 config_group 结构来指定默认组。每个添加的组都会与父组同时填充到 configfs 树中。同样,它们也会与父组同时被删除。不提供额外通知。当 ->drop_item() 方法调用通知子系统父组即将消失时,这也意味着与该父组关联的每个默认组子组都会消失。
因此,无法通过 rmdir(2) 直接删除默认组。当父组上的 rmdir(2) 检查子组时,也不会考虑默认组。
依赖子系统
有时其他驱动程序依赖于特定的 configfs 项。例如,ocfs2 挂载依赖于心跳区域项。如果使用 rmdir(2) 删除该区域项,则 ocfs2 挂载必须出现 BUG 或变为只读。不开心。
configfs 提供了两个额外的 API 调用:configfs_depend_item() 和 configfs_undepend_item()。客户端驱动程序可以对现有项调用 configfs_depend_item() 来告知 configfs 它被依赖。然后 configfs 将从 rmdir(2) 返回该项的 -EBUSY。当该项不再被依赖时,客户端驱动程序将对其调用 configfs_undepend_item()。
这些 API 不能在任何 configfs 回调下调用,因为它们会发生冲突。它们可以阻塞和分配。客户端驱动程序可能不应该出于自己的意愿调用它们。相反,它应该提供外部子系统调用的 API。
这是如何工作的?想象一下 ocfs2 挂载过程。挂载时,它会请求一个心跳区域项。这是通过调用心跳代码来完成的。在心跳代码中,会查找区域项。在这里,心跳代码会调用 configfs_depend_item()。如果成功,那么心跳就知道该区域可以安全地提供给 ocfs2。如果失败,它无论如何都会被拆除,心跳可以优雅地传递错误。