1. 程式人生 > >Linux核心中的kobject和kset介紹

Linux核心中的kobject和kset介紹

本文會圍繞kobject、ktype和kset三個概念進行介紹,我們先大概瞭解一下相關概念以及它們之間的關係:

kobject在核心中應用最多的就是裝置驅動模型————匯流排、裝置、驅動、類的管理都使用了kobject,但是kobject並不只為裝置驅動模型服務,它是核心中的通用物件模型,用來為核心中各部分的物件管理提供統一檢視,其實現在核心的lib/目錄下。
kobject一般都不會單獨使用,這樣是沒有意義的,它總是內嵌到其他結構體中,例如字元裝置的定義:

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const
struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; };

我們說kobject只是通用物件的表示,其自身不包含任何特定於某個模組的屬性,因此我們更關心的是kobject的外圍結構。每個外圍結構體所在的模組都應該定義一個ktype,這個ktype就完成了由通用向特殊的過渡。因此kobject可看做其他所有外圍結構的基類,它抽象出一些通用的屬性(如引用計數)以及通用介面,而每個外圍結構各自完成這些介面的內部實現,這裡使用內嵌的方式來體現這種繼承關係。
使用內嵌而不用指標也是為了方便通過kobject來反向定位其外圍結構的例項,通過我們熟知的container_of()巨集便可以做到。由於kobject是系統統一管理的,因此先找到kobject物件進而跟蹤到其代表的具體物件是很常見的做法。

kset是一個基本的容器類,它是一組kobject的集合。當我們想統一管理某些有類似屬性的kobjects時,可以將它們加入到一個集合中,這個集合的作用是,當一個事件發生時,可以同時通知到集合中的所有kobjects。

本文基於Linux核心版本3.10.27(kobject在2.6核心中就被引入了)。

kobject

kobject在核心中不會單獨出現,並且散落在核心的各個角落,因此很難追蹤到每一個kobject物件。好在 Linux核心將所有的kobjects與虛擬檔案系統sysfs緊密結合起來,這樣就讓所有kobjects視覺化和層次化。kobject描述了sysfs虛擬檔案系統中的層級結構,一個kobject物件就對應了sysfs中的一個目錄,而sysfs中的目錄結構也體現在各個kobjects之間的父子關係。
kobject由struct kobject定義:

#include <linux/kobject.h>
struct kobject {
    const char      *name; /* kobject物件的名字,對應sysfs中的目錄名 */
    struct list_head    entry; /* 在kset中的連結串列節點 */
    struct kobject      *parent; /* 用於構建sysfs中kobjects的層次結構,指向父目錄 */
    struct kset     *kset; /* 所屬kset */
    struct kobj_type    *ktype; /* 特定物件型別相關,用於跟蹤object及其屬性 */
    struct sysfs_dirent *sd; /* 指向該目錄的dentry私有資料 */
    struct kref     kref; /* kobject的引用計數,初始值為1 */
    unsigned int state_initialized:1; /* kobject是否初始化,由kobject_init()設定 */
    unsigned int state_in_sysfs:1; /* 是否已新增到sysfs層次結構中 */
    unsigned int state_add_uevent_sent:1;
    unsigned int state_remove_uevent_sent:1;
    unsigned int uevent_suppress:1; /* 是否忽略uevent事件 */
};

新建一個kobject物件需要兩步:初始化struct kobject結構,新增到sysfs目錄結構中。
1) 初始化一個kobject物件:

void kobject_init(struct kobject *kobj, struct kobj_type *ktype);

這個函式需要傳入兩個引數,kobj和ktype必須不為NULL,由於kobj都是嵌入到其他結構體,所以一般傳kobj引數的方式形如&pcdev->kobj。
該函式即完成對kobj的初始化:
初始化kobj->kref引用計數為初始值1;
初始化kobj->entry空連結串列頭;
kobj->ktype = ktype;
然後將kobj->state_initialized置為1,表示該kobject已初始化。

2) 初始化之後,通過kobject_add()將kobj新增到系統中:

int kobject_add(struct kobject *kobj, struct kobject *parent,
        const char *fmt, ...);

這個函式給kobj指定一個名字,這個名字也就是其在sysfs中的目錄名,parent用來指明kobj的父節點,即指定了kobj的目錄在sysfs中建立的位置。如果這個kobj要加入到一個特定的kset中,則在kobject_add()必須給kobj->kset賦值,此時parent可以設定為NULL,這樣kobj會自動將kobj->kset對應的物件作為自己的parent。如果parent設定為NULL,且沒有加入到一個kset中,kobject會被建立到/sys頂層目錄下。
設定物件的名字是通過下面的介面完成的:

int kobject_set_name(struct kobject *kobj, const char *fmt, ...);

這個函式給kobj的name成員賦值。注意這裡面是通過kmalloc給name分配記憶體的。相應的獲取一個kobject物件的名字的介面為:

const char *kobject_name(const struct kobject * kobj);

如果在新增kobj之後要給kobj改名字,則使用kobject_rename()介面,而不要直接去操作kobj->name,因為涉及到記憶體重新分配以及sysfs中目錄的改變。

也可以通過下面的介面來一次性完成kobject_init()和kobject_add()過程:

int kobject_init_and_add(struct kobject *kobj,
             struct kobj_type *ktype, struct kobject *parent,
             const char *fmt, ...);

引數含義和上述介面相同。

ktype

ktype是由struct kobj_type來定義的,

#include <linux/kobject.h>
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
};

kobj_type都必須實現release方法,該方法由kobject的外層結構來定義,用來釋放特定模組相關的kobject資源。

default_attrs定義了一系列預設屬性,default_attrs是一個二級指標,可以對每個kobject設定多個預設屬性(最後一個屬性用NULL填充)。

struct attribute {
    const char      *name; /* 屬性名字 */
    umode_t         mode; /* 使用者訪問模式,在<linux/stat.h>中定義 */
};

例如:

static struct attribute *i2c_adapter_attrs[] = {
    &dev_attr_name.attr,
    &dev_attr_new_device.attr,
    &dev_attr_delete_device.attr,
    NULL
};

可見,kobj_type是由具體模組定義的,每一個屬性都對應著kobject目錄下的一個檔案,這樣可以在使用者態通過讀寫屬性檔案,來完成對該屬性值的讀取和更改。

而sysfs_ops定義了屬性的操作:

struct sysfs_ops {
    ssize_t (*show)(struct kobject *, struct attribute *,char *);
    ssize_t (*store)(struct kobject *,struct attribute *,const char *, size_t);
    const void *(*namespace)(struct kobject *, const struct attribute *);
};

也就是說,default_attrs陣列中所有屬性的操作都是由sysfs_ops來完成的。
在kobject的容器(外圍結構)中,一般會定義xxx_add_attrs()和xxx_remove_attrs()提供增加和刪除額外屬性的介面。例如對於bus_ktype,預設屬性是空的,因此我們看到/sys/bus/目錄下是沒有屬性檔案的,而每個子目錄的屬性可能不同,因此都需要動態地去定義屬性列表並據此建立屬性檔案。動態屬性一般通過__ATTR()巨集來定義。

建立屬性檔案最終都是通過sysfs_create_file()或sysfs_create_group()來完成的。其中sysfs_create_group(kobj, grp)用來建立一組屬性檔案,需要定義struct attribute_group結構的屬性集,這裡需要說明的是,其中name成員如果為NULL,則直接在kobj目錄下建立各個屬性檔案,如果name不為NULL,則會建立一個名為name的目錄,然後在該目錄下建立各個屬性檔案。

struct attribute_group {
    const char      *name;
    umode_t         (*is_visible)(struct kobject *,
                          struct attribute *, int);
    struct attribute    **attrs;
};

引用計數 —— kref

kref成員是object物件的引用計數,初始值為1,通過kref_get()和kref_put()可對該計數進行增減操作。kref_get()和kref_put()是核心中通用的引用計數的操作,針對kobject,使用下面兩個封裝函式:

struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);

為方便跟蹤一個kobject,在使用kobject之前,都要使用kobject_get()將引數kobj的kref加1,該函式返回kobj自身的指標。kobject_put()則將kref減1,當kref減到0時,則kobject_release()被呼叫來釋放kobject資源。
kobject_release()會呼叫ktype的release方法做實際的釋放操作,並釋放為name分配的記憶體。如果有必要,還會向用戶態傳送KOBJ_REMOVE事件,並通過kobject_del()將kobject從sysfs中刪除。

與sysfs關聯 —— sysfs_dirent

讀寫sysfs中的檔案(即每個attribute)的操作函式集為sysfs_file_operations:

const struct file_operations sysfs_file_operations = {
    .read       = sysfs_read_file,
    .write      = sysfs_write_file,
    .llseek     = generic_file_llseek,
    .open       = sysfs_open_file,
    .release    = sysfs_release,
    .poll       = sysfs_poll,
};

在讀寫sysfs的檔案時,是通過檔案的dentry找到和sysfs關聯的struct sysfs_dirent結構,sysfs中每一個目錄或檔案都有其對應的一個struct sysfs_dirent結構的例項,而sysfs_dirent的s_parent成員用於構建sysfs中的層次結構。
那麼,對於一個屬性檔案的sysfs_dirent結構,可通過其s_parent找到其所在目錄的kobject例項,進而找到檔案操作對應的kobj->ktype->sysfs_ops方法集合。相應的,kobject的sd成員即指向該目錄對應的sysfs_dirent例項。

在讀寫一個sysfs裡的屬性檔案時,其中read操作會去呼叫其所在目錄對應的kobject的kobj->ktype->sysfs_ops的show方法完成屬性讀取,write操作會去呼叫kobj->ktype->sysfs_ops的store方法完成屬性寫入並使其生效。open/release方法用於開啟/關閉檔案。poll用來探測檔案內容是否改變(如果相應kobject實現了poll/select的事件通知),poll方法返回POLLERR|POLLPRI事件,如果發現檔案內容有變則需要重新開啟檔案或seek到檔案頭並重新讀取才能讀到新的內容。
實際上,上述sysfs_ops的show和store方法也是簡單的包裹函式,實際呼叫了具體模組的xxx_attribute的show和store方法(在實現一個屬性的操作時,如果是很簡單的屬性讀取和配置,可以複用struct kobj_attribute,這種屬性一般只是一個整型或字串,如果是比較複雜的模組和屬性,則可自定義形如xxx_attribute的結構及其show/store方法)。

Uevents

在一個kobject的狀態變化時(新註冊、登出、重新命名等),都會廣播出一個對應的事件通知(通常使用者態會去接收並處理),通知事件的介面為:

int kobject_uevent(struct kobject *kobj, enum kobject_action action);

int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp_ext[]);

第二個介面可以傳遞額外的環境變數(“額外”的意思是說,即使envp_ext為NULL,也會傳遞基本的”ACTION=%s”、”DEVPATH=%s”、”SUBSYSTEM=%s”、”SEQNUM=%llu”)。action的可取值如下定義:

enum kobject_action {
    KOBJ_ADD,
    KOBJ_REMOVE,
    KOBJ_CHANGE,
    KOBJ_MOVE,
    KOBJ_ONLINE,
    KOBJ_OFFLINE,
    KOBJ_MAX
};

這些動作在傳送到使用者態時是通過字串來表達的,其對應關係為:

static const char *kobject_actions[] = {
    [KOBJ_ADD] =        "add",
    [KOBJ_REMOVE] =     "remove",
    [KOBJ_CHANGE] =     "change",
    [KOBJ_MOVE] =       "move",
    [KOBJ_ONLINE] =     "online",
    [KOBJ_OFFLINE] =    "offline",
};

kset

除了sysfs中的目錄層次結構之外,還有一個集合的概念,核心將相互關聯的kobjects放到一個kset中統一管理(一個kset中並沒有要求其中每個kobject的ktype必須相同,但正常情況下總是相同的)。

struct kset {
    struct list_head list; /* 其成員列表 */
    spinlock_t list_lock;
    struct kobject kobj;
    const struct kset_uevent_ops *uevent_ops; /* 擴充套件的事件處理 */
};

使用kset可以統一管理某些kobjects,方便查詢和遍歷,kobject的entry成員將所有的同一集合中的成員連線起來。
另外我們看到,一個kset自身在核心中也是一個kobject物件,因此,一個kset在sysfs中也對應著一個目錄,這個kset的kobject可以作為其子目錄的parent,sysfs頂層目錄的bus/、devices/等目錄就是這樣建立的。通常,一個目錄下的所有子目錄都是屬於同一個kset的,例如/sys/bus/目錄下的所有子目錄都屬於全域性的bus_kset。

kset的作用還在於可以對kobject的uevent事件的預設動作做一些擴充套件,上面講到的kobject_uevent_env()函式中,如果發現kobj屬於某個kset,則還會繼續呼叫其kset的uevent_ops,

struct kset_uevent_ops {
    int (* const filter)(struct kset *kset, struct kobject *kobj);
    const char *(* const name)(struct kset *kset, struct kobject *kobj);
    int (* const uevent)(struct kset *kset, struct kobject *kobj,
              struct kobj_uevent_env *env);
};

這個結構體中,filter方法是一個過濾規則,用於判斷是否將uevent發出去,可使使用者態忽略某些事件。name方法用於獲得特定子系統的名字傳遞給使用者態,可以用來覆蓋預設的名字(預設為kset的名字)。uevent方法即用於完成擴充套件的事件通知動作,例如對於”裝置”的kset,事件中除了攜帶通用的環境變數,還需要攜帶MAJOR、MIMOR、DEVNAME等變數發往使用者態。

建立一個新的kset的介面如下:

struct kset *kset_create_and_add(const char *name,
                 const struct kset_uevent_ops *uevent_ops,
                 struct kobject *parent_kobj)
{
    struct kset *kset;
    int error;

    /* 建立一個kset */
    kset = kset_create(name, uevent_ops, parent_kobj);
    if (!kset)
        return NULL;

    /* 將kset->obj註冊到sysfs中 */
    error = kset_register(kset);
    if (error) {
        kfree(kset);
        return NULL;
    }
    return kset;
}

其過程有兩步,首先通過kset_create()建立kset物件,為其成員賦初始值,此時這個kset的kobj未加入任何kset,kobj的ktype也是預設的kset_ktype,值得一提的是,kset的名字也是其kobject物件的名字。然後通過kset_register()將kset->kobj註冊到sysfs中(建立目錄和屬性檔案),並廣播KOBJ_ADD訊息。
如果要使新建立的kset加入一個存在的kset,或使用自定義的ktype,則需要在外層模組為kset->kobj初始化好之後,直接呼叫kset_register()。
相應的,銷燬一個kset的函式為kset_unregister(struct kset *kset);

如何將一個kobject加入到一個kset中呢?kobj_kset_join()和kobj_kset_leave()完成加入和移除的操作,但這兩個介面不對外開放,實際上,kobj從一個kset中加入和移除的操作包含在kobject_add()和kobject_del()中,因此在建立一個kobject物件時,如果想讓其加入某個kset,就要在kobject_add()之前指定。

還有兩點需要注意:
1) 如果一個kobject的parent為NULL,那麼,如果其加入了某個kset,則指定其父物件為kset的object,此時,這個kobject建立在kset的object目錄下。
2) 如果一個kobject的parent不為NULL,又加入了某個kset,此時,這個kobject建立在parent的目錄下。這樣一來,就可能出現一個物件加入了某個kset,但不在這個kset的目錄(或子目錄)下。

下圖展示了在sysfs檢視中,kset、kobject、attribute的關係。圖中綠色線條構成了sysfs中的目錄結構,可見圖中有5層目錄。黑色線條體現集合的包含關係,圖中有兩個kset。橙色實體是一個目錄下的屬性檔案。
這裡寫圖片描述

kobject和kset的demo可參考核心原始碼中的samples/kobject/。