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。如果失敗,它反正也在被拆除,心跳可以優雅地傳遞一個錯誤。