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 项。它展示了一个显示和存储属性的简单对象,以及一个创建和销毁这些子项的简单组。

层级导航和子系统互斥锁

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。

自动创建的子组

新的 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。如果失败,则它正在被拆除,并且心跳可以优雅地传递一个错误。