關於 kobject、kset 和 ktype 的所有你不想知道的事情¶
- 作者:
Greg Kroah-Hartman <gregkh@linuxfoundation.org>
- 最後更新:
2007 年 12 月 19 日
基於 Jon Corbet 於 2003 年 10 月 1 日為 lwn.net 撰寫的原始文章,位於 https://lwn.net/Articles/51437/
理解驅動模型(以及其所基於的 kobject 抽象)的困難之一在於,沒有一個明顯的切入點。處理 kobject 需要理解幾種不同的型別,它們都相互引用。為了簡化問題,我們將採用多遍方法,從模糊的術語開始,然後逐步新增細節。為此,這裡對我們將要用到的一些術語進行快速定義。
kobject 是
struct kobject型別的一個物件。Kobject 具有名稱和引用計數。Kobject 還具有一個父指標(允許物件排列成層次結構)、一個特定型別,並且通常在sysfs虛擬檔案系統中有一個表示。Kobject 通常本身並不有趣;相反,它們通常嵌入在其他結構中,這些結構包含程式碼真正感興趣的內容。
任何結構都絕不能嵌入一個以上的 kobject。如果嵌入了多個,物件的引用計數肯定會出錯和不正確,您的程式碼將存在缺陷。所以不要這樣做。
ktype 是嵌入 kobject 的物件型別。每個嵌入 kobject 的結構都需要一個相應的 ktype。ktype 控制 kobject 在建立和銷燬時會發生什麼。
kset 是一組 kobject。這些 kobject 可以是相同的 ktype,也可以屬於不同的 ktype。kset 是 kobject 集合的基本容器型別。Kset 包含它們自己的 kobject,但您可以安全地忽略該實現細節,因為 kset 核心程式碼會自動處理此 kobject。
當您看到一個充滿其他目錄的
sysfs目錄時,通常每個目錄都對應於同一 kset 中的一個 kobject。
我們將瞭解如何建立和操作所有這些型別。我們將採用自下而上的方法,所以我們將回到 kobject。
嵌入 kobject¶
核心程式碼很少建立獨立的 kobject,下面將解釋一個主要例外。相反,kobject 用於控制對更大的、特定領域物件的訪問。為此,kobject 將嵌入在其他結構中。如果您習慣於用面向物件的術語思考問題,kobject 可以被視為一個頂層抽象類,其他類由此派生。kobject 實現了一組本身並不特別有用,但在其他物件中卻很好的功能。C 語言不允許直接表達繼承,因此必須使用其他技術,例如結構體嵌入。
(順便說一句,對於熟悉核心連結串列實現的人來說,這類似於“list_head”結構本身很少有用,但總是嵌入在更大型的物件中。)
因此,例如,drivers/uio/uio.c 中的 UIO 程式碼有一個結構體,用於定義與 uio 裝置關聯的記憶體區域。
struct uio_map {
struct kobject kobj;
struct uio_mem *mem;
};
如果您有一個 struct uio_map 結構體,查詢其嵌入的 kobject 只需要使用 kobj 成員即可。然而,處理 kobject 的程式碼通常會遇到相反的問題:給定一個 struct kobject 指標,如何獲取指向包含結構的指標?您必須避免技巧(例如假設 kobject 位於結構體的開頭),而應使用在 <linux/kernel.h> 中找到的 container_of() 宏。
container_of(ptr, type, member)
其中
ptr是嵌入 kobject 的指標,
type是包含結構體的型別,並且
member是pointer指向的結構體欄位的名稱。
container_of() 的返回值是指向相應容器型別的指標。因此,例如,一個指向嵌入在 struct uio_map 內的 struct kobject 的指標 kp 可以透過以下方式轉換為指向包含 uio_map 結構體的指標:
struct uio_map *u_map = container_of(kp, struct uio_map, kobj);
為了方便起見,程式設計師通常會定義一個簡單的宏,用於將 kobject 指標反向轉換為包含型別。在之前的 drivers/uio/uio.c 中正是如此,您可以在此處看到:
struct uio_map {
struct kobject kobj;
struct uio_mem *mem;
};
#define to_map(map) container_of(map, struct uio_map, kobj)
其中宏引數 “map” 是指向相關 struct kobject 的指標。該宏隨後透過以下方式呼叫:
struct uio_map *map = to_map(kobj);
kobject 的初始化¶
建立 kobject 的程式碼當然必須初始化該物件。某些內部欄位是透過(強制性的)呼叫 kobject_init() 來設定的。
void kobject_init(struct kobject *kobj, const struct kobj_type *ktype);
ktype 是正確建立 kobject 所必需的,因為每個 kobject 都必須有一個關聯的 kobj_type。在呼叫 kobject_init() 之後,要向 sysfs 註冊該 kobject,必須呼叫函式 kobject_add()。
int kobject_add(struct kobject *kobj, struct kobject *parent,
const char *fmt, ...);
這會正確設定 kobject 的父級和 kobject 的名稱。如果 kobject 要與特定的 kset 關聯,則必須在呼叫 kobject_add() 之前分配 kobj->kset。如果 kobject 與 kset 關聯,則可以在呼叫 kobject_add() 時將 kobject 的父級設定為 NULL,然後 kobject 的父級將是 kset 本身。
由於 kobject 的名稱是在將其新增到核心時設定的,因此 kobject 的名稱絕不應直接操作。如果必須更改 kobject 的名稱,請呼叫 kobject_rename()。
int kobject_rename(struct kobject *kobj, const char *new_name);
kobject_rename() 不執行任何鎖定操作,也沒有關於哪些名稱有效的明確概念,因此呼叫者必須提供自己的健全性檢查和序列化。
有一個名為 kobject_set_name() 的函式,但那是遺留的冗餘程式碼,正在被移除。如果您的程式碼需要呼叫此函式,則它是錯誤的,需要修復。
要正確訪問 kobject 的名稱,請使用函式 kobject_name()。
const char *kobject_name(const struct kobject * kobj);
有一個輔助函式可以同時初始化 kobject 並將其新增到核心,令人驚訝的是它被稱為 kobject_init_and_add()。
int kobject_init_and_add(struct kobject *kobj, const struct kobj_type *ktype,
struct kobject *parent, const char *fmt, ...);
引數與上面描述的單獨的 kobject_init() 和 kobject_add() 函式相同。
Uevent 事件¶
kobject 註冊到 kobject 核心後,您需要向外界宣佈它已被建立。這可以透過呼叫 kobject_uevent() 來完成。
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
當 kobject 首次新增到核心時,使用 KOBJ_ADD 操作。這應該僅在 kobject 的任何屬性或子級已正確初始化之後進行,因為此呼叫發生時,使用者空間會立即開始查詢它們。
當 kobject 從核心中移除時(詳細資訊如下),KOBJ_REMOVE 的 uevent 將由 kobject 核心自動建立,因此呼叫者無需擔心手動執行此操作。
引用計數¶
kobject 的關鍵功能之一是作為其嵌入物件的引用計數器。只要存在對物件的引用,物件(以及支援它的程式碼)就必須繼續存在。用於操作 kobject 引用計數的底層函式是:
struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);
成功呼叫 kobject_get() 會遞增 kobject 的引用計數器並返回指向該 kobject 的指標。
當一個引用被釋放時,呼叫 kobject_put() 將遞減引用計數,並可能釋放物件。請注意,kobject_init() 將引用計數設定為一,因此設定 kobject 的程式碼最終需要執行一次 kobject_put() 以釋放該引用。
由於 kobject 是動態的,它們不能被宣告為靜態的或在棧上,而應始終動態分配。核心的未來版本將包含對靜態建立的 kobject 的執行時檢查,並將警告開發者這種不當用法。
如果您只想使用 kobject 為您的結構提供引用計數器,請改用 struct kref;使用 kobject 將會是多餘的。有關如何使用 struct kref 的更多資訊,請參閱 Linux 核心原始碼樹中的檔案 為核心物件新增引用計數器 (krefs)。
建立“簡單” kobject¶
有時,開發人員只是想在 sysfs 層次結構中建立一個簡單的目錄,而無需處理 kset、show 和 store 函式以及其他細節的複雜性。這是應該建立單個 kobject 的一個例外情況。要建立這樣一個條目,請使用函式:
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent);
此函式將建立一個 kobject,並將其放置在 sysfs 中指定父 kobject 下的位置。要建立與此 kobject 關聯的簡單屬性,請使用:
int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
或
int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp);
這裡使用的兩種屬性型別,對於透過 kobject_create_and_add() 建立的 kobject,都可以是 kobj_attribute 型別,因此無需建立特殊的自定義屬性。
有關簡單 kobject 和屬性的實現,請參閱示例模組 samples/kobject/kobject-example.c。
ktype 和 release 方法¶
討論中仍缺少一個重要事項,即當 kobject 的引用計數達到零時會發生什麼。建立 kobject 的程式碼通常不知道何時會發生這種情況;如果知道,那麼一開始使用 kobject 就沒有什麼意義了。當 sysfs 介入時,即使是可預測的物件生命週期也會變得更加複雜,因為核心的其他部分可以獲取系統中註冊的任何 kobject 的引用。
最終結果是,受 kobject 保護的結構在其引用計數歸零之前不能被釋放。引用計數不受建立 kobject 的程式碼的直接控制。因此,當其 kobject 的最後一個引用消失時,必須非同步通知該程式碼。
一旦您透過 kobject_add() 註冊了您的 kobject,就絕不能直接使用 kfree() 來釋放它。唯一安全的方法是使用 kobject_put()。在呼叫 kobject_init() 之後始終使用 kobject_put() 是一個好習慣,以避免錯誤悄然出現。
此通知是透過 kobject 的 release() 方法完成的。通常此類方法的形式如下:
void my_object_release(struct kobject *kobj)
{
struct my_object *mine = container_of(kobj, struct my_object, kobj);
/* Perform any additional cleanup on this object, then... */
kfree(mine);
}
一個重要的點怎麼強調都不為過:每個 kobject 都必須有一個 release() 方法,並且 kobject 必須持續存在(處於一致狀態)直到該方法被呼叫。如果這些約束未滿足,則程式碼存在缺陷。請注意,如果您忘記提供 release() 方法,核心會警告您。不要試圖透過提供一個“空”的釋放函式來消除此警告。
如果您所有的清理函式只需要呼叫 kfree(),那麼您必須建立一個包裝函式,該函式使用 container_of() 向上轉換為正確的型別(如上例所示),然後對整個結構體呼叫 kfree()。
請注意,kobject 的名稱在 release 函式中可用,但在此回撥中絕不能更改它。否則,kobject 核心將出現記憶體洩漏,這將令人不悅。
有趣的是,release() 方法並非儲存在 kobject 本身中;相反,它與 ktype 相關聯。因此,讓我們介紹一下 struct kobj_type:
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops;
const struct attribute_group **default_groups;
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
void (*get_ownership)(struct kobject *kobj, kuid_t *uid, kgid_t *gid);
};
此結構用於描述特定型別的 kobject(或更準確地說,是包含物件)。每個 kobject 都需要有一個關聯的 kobj_type 結構;當您呼叫 kobject_init() 或 kobject_init_and_add() 時,必須指定指向該結構體的指標。
struct kobj_type 中的 release 欄位當然是指向此類 kobject 的 release() 方法的指標。另外兩個欄位(sysfs_ops 和 default_groups)控制此類物件在 sysfs 中的表示方式;它們超出了本文件的範圍。
default_groups 指標是一個預設屬性列表,這些屬性將為任何註冊到此 ktype 的 kobject 自動建立。
kset¶
kset 僅僅是希望相互關聯的 kobject 的集合。沒有規定它們必須是相同的 ktype,但如果不是,請務必小心。
kset 具有以下功能:
它作為一個包含一組物件的“袋子”。核心可以使用 kset 來跟蹤“所有塊裝置”或“所有 PCI 裝置驅動程式”。
kset 也是
sysfs中的一個子目錄,與 kset 關聯的 kobject 可以在其中顯示。每個 kset 都包含一個 kobject,該 kobject 可以設定為其他 kobject 的父級;sysfs層次結構的頂級目錄就是以這種方式構建的。Kset 可以支援 kobject 的“熱插拔”,並影響 uevent 事件如何報告給使用者空間。
用面向物件的術語來說,“kset”是頂層容器類;kset 包含它們自己的 kobject,但該 kobject 由 kset 程式碼管理,不應由任何其他使用者操作。
kset 將其子項儲存在標準的核心連結串列中。Kobject 透過其 kset 欄位指向其包含的 kset。在幾乎所有情況下,屬於 kset 的 kobject 都將其 kset(或嚴格來說,其嵌入的 kobject)作為其父級。
由於 kset 內部包含一個 kobject,因此它應該始終動態建立,而絕不能靜態宣告或在棧上宣告。要建立一個新的 kset,請使用:
struct kset *kset_create_and_add(const char *name,
const struct kset_uevent_ops *uevent_ops,
struct kobject *parent_kobj);
當您使用完 kset 後,請呼叫:
void kset_unregister(struct kset *k);
來銷燬它。這會將 kset 從 sysfs 中移除並遞減其引用計數。當引用計數變為零時,kset 將被釋放。由於可能仍然存在對 kset 的其他引用,因此釋放可能在 kset_unregister() 返回之後發生。
在核心樹中的 samples/kobject/kset-example.c 檔案中可以看到使用 kset 的示例。
如果 kset 希望控制與其關聯的 kobject 的 uevent 操作,它可以使用 struct kset_uevent_ops 來處理:
struct kset_uevent_ops {
int (* const filter)(struct kobject *kobj);
const char *(* const name)(struct kobject *kobj);
int (* const uevent)(struct kobject *kobj, struct kobj_uevent_env *env);
};
filter 函式允許 kset 阻止為特定 kobject 向用戶空間發出 uevent。如果函式返回 0,則不會發出 uevent。
name 函式將被呼叫以覆蓋 uevent 傳送到使用者空間的 kset 的預設名稱。預設情況下,名稱將與 kset 本身相同,但如果此函式存在,則可以覆蓋該名稱。
uevent 函式將在 uevent 即將傳送到使用者空間時被呼叫,以允許向 uevent 新增更多環境變數。
有人可能會問,既然沒有介紹執行該功能的函式,kobject 是如何精確地新增到 kset 的。答案是此任務由 kobject_add() 處理。當 kobject 傳遞給 kobject_add() 時,其 kset 成員應指向 kobject 將所屬的 kset。kobject_add() 將處理其餘部分。
如果屬於 kset 的 kobject 沒有設定父 kobject,它將被新增到 kset 的目錄中。並非 kset 的所有成員都必須存在於 kset 目錄中。如果在新增 kobject 之前分配了一個顯式的父 kobject,則 kobject 將註冊到 kset,但新增在父 kobject 下。
kobject 移除¶
kobject 成功註冊到 kobject 核心後,程式碼使用完畢時必須對其進行清理。為此,請呼叫 kobject_put()。透過這樣做,kobject 核心將自動清理此 kobject 分配的所有記憶體。如果已為該物件傳送了 KOBJ_ADD uevent,則將傳送相應的 KOBJ_REMOVE uevent,並且將為呼叫者正確處理任何其他 sysfs 清理工作。
如果您需要對 kobject 進行兩階段刪除(例如,當您需要銷燬物件時不允許休眠),那麼請呼叫 kobject_del(),它將從 sysfs 中登出 kobject。這使得 kobject “不可見”,但它並未被清理,並且物件的引用計數仍然相同。稍後呼叫 kobject_put() 以完成與 kobject 關聯的記憶體清理。
如果構建了迴圈引用,可以使用 kobject_del() 來解除對父物件的引用。在某些情況下,父物件引用子物件是有效的。迴圈引用必須透過顯式呼叫 kobject_del() 來解除,以便呼叫釋放函式,並且先前迴圈中的物件相互釋放。
可供複製的示例程式碼¶
要檢視正確使用 kset 和 kobject 的更完整示例,請參閱示例程式 samples/kobject/{kobject-example.c,kset-example.c},如果您選擇了 CONFIG_SAMPLE_KOBJECT,它們將作為可載入模組構建。