Configfs - 用户空间驱动的内核对象配置

Joel Becker <joel.becker@oracle.com>

更新日期: 2005年3月31日

版权所有 (c) 2005 Oracle Corporation,

Joel Becker <joel.becker@oracle.com>

什么是 configfs?

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

使用 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 文本文件,最大大小为一页 (PAGE_SIZE, i386 上为 4096 字节)。最好每个文件只使用一个值,并且适用 sysfs 的相同注意事项。Configfs 期望 write(2) 一次性存储整个缓冲区。当写入普通 configfs 属性时,用户空间进程应首先读取整个文件,修改它们希望更改的部分,然后将整个缓冲区写回。

  • 二进制属性,它们与 sysfs 二进制属性有些相似,但语义上有一些细微变化。PAGE_SIZE 限制不适用,但整个二进制项必须适合单个内核 vmalloc 分配的缓冲区。来自用户空间的 write(2) 调用被缓冲,并且属性的 write_bin_attribute 方法将在最终关闭时被调用,因此用户空间检查 close(2) 的返回值以验证操作是否成功完成至关重要。为了避免恶意用户耗尽内核内存 (OOM),每个二进制属性都有一个最大缓冲区值。

当一个项目需要被销毁时,使用 rmdir(2) 移除它。如果任何其他项目通过 symlink(2) 链接到它,则该项目不能被销毁。链接可以通过 unlink(2) 移除。

配置 FakeNBD:一个例子

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

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

# ls /config
fakenbd

FakeNBD 连接可以使用 mkdir(2) 创建。名称是任意的,但工具可能会利用该名称。也许它是一个 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 处理该对象及其属性的文件系统表示,允许子系统忽略除了基本的 show/store 交互之外的所有内容。

项目在 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 中出现之外,做不了太多。通常,子系统希望项目显示和/或存储属性,以及其他事项。为此,它需要一个类型。

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;
};

当需要使用二进制数据块作为文件内容出现在项的 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_item 的地方。对于一个子系统来说,这个组通常是静态定义的。在调用 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 结构体成员进行镜像。

子系统在新的分配项尚未链接到此层次结构时,将被阻止获取互斥锁。同样,在正在删除的项尚未取消链接时,它也无法获取互斥锁。这意味着,当项存在于 configfs 中时,其 ci_parent 指针永远不会为 NULL,并且项只会在其父级的 cg_children 列表中出现相同的持续时间。这允许子系统在持有互斥锁时信任 ci_parent 和 cg_children。

子系统在新的分配项尚未链接到此层次结构时,将被阻止获取互斥锁。同样,在正在删除的项尚未取消链接时,它也无法获取互斥锁。这意味着,当项存在于 configfs 中时,其 ci_parent 指针永远不会为 NULL,并且项只会在其父级的 cg_children 列表中出现相同的持续时间。这允许子系统在持有互斥锁时信任 ci_parent 和 cg_children。

自动创建子组

一个新的 config_group 可能希望拥有两种类型的子 config_item。虽然这可以通过 ->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。如果失败,它反正也在被拆除,心跳可以优雅地传递一个错误。