Configfs - 用户空间驱动的内核对象配置¶
Joel Becker <joel.becker@oracle.com>
更新时间:2005 年 3 月 31 日
- 版权所有 (c) 2005 Oracle Corporation,
Joel Becker <joel.becker@oracle.com>
什么是 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 类似,configfs 树始终存在,无论是否挂载在 /config 上。
通过 mkdir(2) 创建项目。该项目的属性也会在此时间出现。readdir(3) 可以确定属性是什么,read(2) 可以查询其默认值,而 write(2) 可以存储新值。不要在一个属性文件中混合多个属性。
configfs 属性有两种类型
普通属性,与 sysfs 属性类似,是小的 ASCII 文本文件,最大大小为一页(i386 上为 PAGE_SIZE,4096)。最好每个文件只使用一个值,并应用 sysfs 中的相同注意事项。configfs 期望 write(2) 一次存储整个缓冲区。写入普通 configfs 属性时,用户空间进程应首先读取整个文件,修改它们希望更改的部分,然后将整个缓冲区写回。
二进制属性,与 sysfs 二进制属性有些相似,但语义略有变化。PAGE_SIZE 限制不适用,但整个二进制项目必须适合单个内核 vmalloc 缓冲区。用户空间发出的 write(2) 调用会被缓冲,并且属性的 write_bin_attribute 方法将在最终关闭时被调用,因此用户空间必须检查 close(2) 的返回码,以验证操作是否成功完成。为了避免恶意用户耗尽内核内存,每个二进制属性都有一个最大缓冲区值。
当需要销毁项目时,使用 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 创建,都必须对其调用其中一个 _init() 函数。这将初始化引用计数并设置适当的字段。
config_item 的所有用户都应该通过 config_item_get() 获得对它的引用,并在完成操作后通过 config_item_put() 删除引用。
config_item 本身除了出现在 configfs 中之外,不能做太多事情。通常,子系统希望该项目显示和/或存储属性,以及其他内容。为此,它需要一个类型。
struct 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 的引用计数达到零时,将调用此方法。
struct 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) 时,将调用该方法。
struct 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_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,
const char *name,
struct config_item_type *type);
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()。其他所有操作都相同,在该组上使用组 _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) 将失败,因为目录不是空的。
struct 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(struct configfs_subsystem *subsys);
子系统由一个顶层 config_group 和一个互斥锁组成。该组是创建子 config_items 的地方。对于子系统,该组通常是静态定义的。在调用 configfs_register_subsystem() 之前,子系统必须通过通常的组 _init() 函数初始化该组,并且还必须初始化互斥锁。
当注册调用返回时,子系统就处于活动状态,并且它将通过 configfs 可见。此时,可以调用 mkdir(2),并且子系统必须为此做好准备。
一个示例¶
这些基本概念的最佳示例是 samples/configfs/configfs_sample.c 中的 simple_children 子系统/组和 simple_child 项。它展示了一个显示和存储属性的简单对象,以及一个创建和销毁这些子项的简单组。
通过 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_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。如果失败,则它正在被拆除,并且心跳可以优雅地传递一个错误。