1. 程式人生 > >Linux裝置驅動程式學習(12) -Linux裝置模型(底層原理簡介)

Linux裝置驅動程式學習(12) -Linux裝置模型(底層原理簡介)

以《LDD3》的說法:Linux裝置模型這部分內容可以認為是高階教材,對於多數程式作者來說是不必要的。但是我個人認為:對於一個嵌入式Linux的底層程式設計師來說,這部分內容是很重要的。 以我學習的ARM9為例,有很多匯流排(如SPI、IIC、IIS等等)在Linux下已經被編寫成了子系統,無需自己寫驅動;而這些匯流排又不像PCI、USB等在《LDD3》上有教程,有時還要自己研究它的子系統構架,甚至要自己新增一個新的匯流排型別。

對於這方面的學習,我推薦幾個網頁,這些也是我這部分文章的參考資料:

(1)《 Linux 那些事兒 之 我是Sysfs 》來源於復旦和交大三個牛人的Linux技術部落格: (復旦_abc)他們還分析了很多Linux的驅動,值得珍藏!

(4)luofuchong的部落格  ,此人分析了一些2410中的Linux子系統(如SPI,input等),實力不凡,值得關注。網址:http://www.cnitblog.com/luofuchong/

在這部分的學習中,將會先研究linux裝置模型的每個元素,最後將其一 步一步整合,至底向上地分析。一開始會比較摸不著頭腦,到了整合階段就柳暗花明了。我之所以沒有先介紹整體,再分析每個部分是因為如果不對每個元素做認真 分析,看了整體也會雲裡霧裡(我試過了,恕小生愚鈍)。所以一開始要耐著性子看,到整合階段就會豁然開朗。

Linux裝置模型的目的是: 為核心建立起一個統一的裝置模型,從而有一個對系統結構的一般性抽象描述。

現在核心使用裝置模型支援多種不同的任務:
電源管理和系統關機 :這些需要對系統結構的理解,裝置模型使OS能以正確順序遍歷系統硬體。

與使用者空間的通訊 :sysfs 虛擬檔案系統的實現與裝置模型的緊密相關, 並向外界展示它所表述的結構。向用戶空間提供系統資訊、改變操作引數的介面正越來越多地通過 sysfs , 也就是裝置模型來完成。

熱插拔裝置

裝置型別:裝置模型包括了將裝置分類的機制,在一個更高的功能層上描述這些裝置, 並使裝置對使用者空間可見。

物件生命週期 :裝置模型的實現需要建立一系列機制來處理物件的生命週期、物件間的關係和物件在使用者空間的表示。

Linux 裝置模型是一個複雜的資料結構。但對模型的大部分來說, Linux 裝置模型程式碼會處理好這些關係, 而不是把他們強加於驅動作者。模型隱藏於互動的背後,與裝置模型的直接互動通常由匯流排級的邏輯和其他的核心子系統處理。所以許多驅動作者可完全忽略裝置模 型, 並相信裝置模型能處理好他所負責的事。

Kobject、Kset 和 Subsystem

Kobjects

kobject是一種資料結構,定義在 <linux/kobject.h>

struct kobject {
    const char     * k_name; /*kobject 的名字陣列(sysfs 入口使用的名字)指標;如果名字陣列大小小於KOBJ_NAME_LEN,它指向本陣列的name,否則指向另外分配的一個名字陣列空間 */
    char             name[ KOBJ_NAME_LEN] ; /*kobject 的名字陣列,若名字陣列大小不小於KOBJ_NAME_LEN,只儲存前KOBJ_NAME_LEN個字元*/
    struct kref        kref; /*kobject 的引用計數*/
    struct list_head    entry; /*kobject 之間的雙向連結串列,與所屬的kset形成環形連結串列 */
    struct kobject        * parent; /*在sysfs分層結構中定位物件,指向上一級kset中的struct kobject kobj*/
    struct kset        * kset; /*指向所屬的kset*/
    struct kobj_type    * ktype; /*負責對該kobject型別進行跟蹤的struct kobj_type的指標*/
    struct dentry        * dentry; /*sysfs檔案系統中與該物件對應的檔案節點路徑指標*/
    wait_queue_head_t    poll; /*等待佇列頭*/
} ;


kobject 是組成裝置模型的基本結構,初始它只被作為一個簡單的引用計數, 但隨時間的推移,其任務越來越多。現在kobject 所處理的任務和支援程式碼包括:

物件的引用計數 :跟蹤物件生命週期的一種方法是使用引用計數。當沒有核心程式碼持有該物件的引用時, 該物件將結束自己的有效生命期並可被刪除。

sysfs 表述 :在 sysfs 中出現的每個物件都對應一個 kobject, 它和核心互動來建立它的可見表述。

資料結構關聯 :整體來看, 裝置模型是一個極端複雜的資料結構,通過其間的大量連結而構成一個多層次的體系結構。kobject 實現了該結構並將其聚合在一起。

熱插拔事件處理 :kobject 子系統將產生的熱插拔事件通知使用者空間。

一個kobject對自身並不感興趣,它存在的意義在於把高階物件連線到裝置模型上。因此核心程式碼很少(甚至不知道)創 建一個單獨的 kobject;而kobject 被用來控制對大型域(domain)相關物件的訪問,所以kobject 被嵌入到其他結構中。kobject 可被看作一個最頂層的基類,其他類都它的派生產物。 kobject 實現了一系列方法,對自身並沒有特殊作用,而對其他物件卻非常有效。

 對於給定的kobject指標,可使用container_of 巨集得到包含它的結構體的指標。

kobject 初始化

kobject的初始化較為複雜,但是必須的步驟 如下:

(1)將整個kobject清零,通常使用memset函式。

(2)呼叫kobject_init()函式,設定結構內部一些成員。所做的一件事情是設定kobject的引用計數為1。具體的原始碼如下:

void kobject_init( struct kobject * kobj)/*in kobject.c */
{
    if ( ! kobj)
        return ;
    kref_init( & kobj- > kref) ; /*設定引用計數為1*/
    INIT_LIST_HEAD( & kobj- > entry) ; /*初始化kobject 之間的雙向連結串列*/
    init_waitqueue_head( & kobj- > poll) ; /*初始化等待佇列頭*/
    kobj- > kset = kset_get( kobj- > kset) ; /*增加所屬kset的引用計數(若沒有所屬的kset,則返回NULL)*/
}

void kref_init( struct kref * kref)/*in kobject.c */
{
    atomic_set( & kref- > refcount, 1) ;
    smp_mb( ) ;
}

static inline struct kset * to_kset( struct kobject * kobj)/*in kobject.h */
{
    return kobj ? container_of( kobj, struct kset, kobj) : NULL ;
}

static inline struct kset * kset_get( struct kset * k)/*in kobject.h */
{
    return k ? to_kset( kobject_get( & k- > kobj) ) : NULL ;/*增加引用計數*/
}

(3)設定kobject的名字

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

(4)直接或間接設定其它成員:ktype、kset和parent。 (重要)

對引用計數的操作

kobject 的一個重要函式是為包含它的結構設定引用計數。只要對這個物件的引用計數存在, 這個物件( 和支援它的程式碼) 必須繼續存在。底層控制 kobject 的引用計數的函式有:

struct kobject * kobject_get( struct kobject * kobj) ; /*若成功,遞增 kobject 的引用計數並返回一個指向 kobject 的指標,否則返回 NULL。必須始終測試返回值以免產生競態*/
void kobject_put( struct kobject * kobj) ; /*遞減引用計數並在可能的情況下釋放這個物件*/

注意:kobject _init 設定這個引用計數為 1,因此建立一個 kobject時, 當這個初始化引用不再需要,應當確保採取 kobject_put 呼叫。同理:struct cdev 的引用計數實現如下:

struct kobject * cdev_get( struct cdev * p)
{
 struct module * owner = p- > owner;
 struct kobject * kobj;
 if ( owner & & ! try_module_get( owner) )
 return NULL ;
 kobj = kobject_get( & p- > kobj) ;
 if ( ! kobj)
 module_put( owner) ;
 return kobj;
}

建立一個對 cdev 結構的引用時,還需要建立包含它的模組的引用。因此, cdev_get 使用 try_module_get 來試圖遞增這個模組的使引用計數。如果這個操作成功, kobject_get 被同樣用來遞增 kobject 的引用計數。kobject_get 可能失敗, 因此這個程式碼檢查 kobject_get 的返回值,如果呼叫失敗,則釋放它的對模組的引用計數。

release 函式和 kobject 型別

引用計數不由建立 kobject 的程式碼直接控制,當 kobject 的最後引用計數消失時,必須非同步通知,而後kobject中ktype所指向的kobj_type結構體包含的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函式被呼叫前保持不變( 穩定狀態 ) 。這樣,每一個 kobject 需要有一個關聯的 kobj_type 結構,指向這個結構的指標能在 2 個不同的地方找到:

(1)kobject 結構自身包含一個成員(ktype)指向kobj_type ;

(2)如果這個 kobject 是一個 kset 的成員, kset 會提供kobj_type 指標。

struct kset {
    struct kobj_type    * ktype; /*指向該kset物件型別的指標*/
    struct list_head    list ;/*用於連線該kset中所有kobject以形成環形連結串列的連結串列頭*/
    spinlock_t        list_lock;/*用於避免競態的自旋鎖*/
    struct kobject        kobj; /*嵌入的kobject*/
    struct kset_uevent_ops    * uevent_ops ;

/*原有的struct kset_hotplug_ops * hotplug_ops;已經不存在,被kset_uevent_ops 結構體替換,在熱插拔操作中會介紹 */
} ;

以下巨集用以查詢指定kobject的kobj_type 指標:

struct kobj_type * get_ktype( struct kobject * kobj) ;

這個函式其實就是從以上提到的這兩個地方返回kobj_type指標,原始碼如下:

static inline struct kobj_type * get_ktype( struct kobject * k)
{
    if ( k- > kset & & k- > kset- > ktype)
        return k- > kset- > ktype;
    else
        return k- > ktype;
}

kobject 層次結構、kset 和子系統

核心通常用kobject 結構將各個物件連線起來組成一個分層的結構體系,與模型化的子系統相匹配。有 2 個獨立的機制用於連線: parent 指標和 kset

parent 是指向另外一個kobject 結構(分層結構中上一層的節點)的指標,主要用途是在 sysfs 層次中定位物件.

kset

kset 象 kobj_type 結構的擴充套件; 一個 kset 是嵌入到相同型別結構的 kobject 的集合。但 struct kobj_type 關注的是物件的型別,而struct kset 關心的是物件的聚合和集合,其主要功能是包容,可認為是kobjects 的頂層容器類。每個 kset 在內部包含自己的 kobject, 並可以用多種處理kobject 的方法處理kset。 ksets 總是在 sysfs 中出現; 一旦設定了 kset 並把它新增到系統中, 將在 sysfs 中建立一個目錄;kobjects 不必在 sysfs 中表示, 但kset中的每一個 kobject 成員都在sysfs中得到表述。

增加 kobject 到 kset 中去,通常是在kobject 建立時完成,其過程分為2步:

(1)完成kobject的初始化,特別注意mane和parent和初始化。

(2)把kobject 的 kset 成員指向目標kset。

(3)將kobject 傳遞給下面的函式:

int kobject_add( struct kobject * kobj) ; /*函式可能失敗(返回一個負錯誤碼),程式應作出相應地反應*/

核心提供了一個組合函式:

extern int kobject_register( struct kobject * kobj) ; /*僅僅是一個 kobject_init 和 kobject_add 的結合,其他成員的初始化必須在之前手動完成 */

當把一個kobject從kset中刪除以清除引用時使用:

void kobject_del( struct kobject * kobj) ; /*是 kobject_del 和 kobject_put 的結合*/

kset 在一個標準的核心連結串列中儲存了它的子節點,在大部分情況下, 被包含的 kobjects 在它們的 parent 成員中儲存指向 kset內嵌的 kobject的指標,關係如下:一個簡單的 kset 層次

圖表中的所有的被包含的 kobjects 實際上被嵌入在一些其他型別中, 甚至可能其他的 kset。

kset 上的操作

ksets 有類似於kobjects初始化和設定介面:

void kset_init( struct kset * kset) ;
int kset_add( struct kset * kset) ;
int kset_register( struct kset * kset) ;
void kset_unregister( struct kset * kset) ;

/*管理 ksets 的引用計數:*/
struct kset * kset_get( struct kset * kset) ;
void kset_put( struct kset * kset) ;

/* kset 也有一個名字,儲存於嵌入的 kobject,因此設定它的名字用:*/
kobject_set_name( & my_set- > kobj, "The name" ) ;

ksets 還有一個指標指向 kobj_type 結構來描述它包含的 kobject,這個型別優先於 kobject 自身中的 ktype 。因此在典型的應用中, 在 struct kobject 中的 ktype 成員被設為 NULL, 而 kset 中的ktype是實際被使用的。
在新的核心裡, kset 不再包含一個子系統指標struct subsystem * subsys, 而且subsystem已經被kset取代。

子系統

子系統是對整個核心中一些高階部分的表述。子系統通常(但不一定)出現在 sysfs分層結構中的頂層,核心子系統包括 block_subsys(/sys/block 塊裝置)、 devices_subsys(/sys/devices 核心裝置層)以及核心已知的用於各種匯流排的特定子系統。

對於新的核心已經不再有 subsystem資料結構 了,用kset代替了。每個 kset 必須屬於一個子系統,子系統成員幫助核心在分層結構中定位 kset 。

/*子系統通常用以下的巨集宣告:*/
decl_subsys( name, struct kobj_type * type, struct kset_uevent_ops * uevent_ops) ;

/*子系統的操作函式:*/
void subsystem_init( struct kset * s) ;
int subsystem_register( struct kset * s) ;
void subsystem_unregister( struct kset * s) ;
struct subsystem * subsys_get( struct kset * s)
void subsys_put( struct kset * s) ;

/*這些函式基本上是kset操作函式的包裝,以實現子系統的操作*/

底層sysfs操作

kobject 是在 sysfs 虛擬檔案系統後的機制。對每個在 sysfs 中的目錄 , 在核心中都會有一個 kobject 與之對應。每個 kobject 都輸出一個或多個屬性, 它在 kobject 的 sysfs 目錄中以檔案 的形式出現, 其中的內容由核心產生。 <linux/sysfs.h>   包含 sysfs 的工作程式碼。
在 sysfs 中建立kobject的入口是kobject_add的工作的一部分,只要呼叫 kobject_add 就會在sysfs 中顯示,還有些知識值得記住:
(1)kobjects 的 sysfs 入口始終為目錄, kobject_add 的呼叫將在sysfs 中建立一個目錄,這個目錄包含一個或多個屬性(檔案);
(2) 分配給 kobject 的名字( 用 kobject_set_name ) 是 sysfs 中的目錄名,出現在 sysfs 層次的相同部分的 kobjects 必須有唯一的名字. 分配給 kobjects 的名字也應當是合法的檔名字: 它們不能包含非法字元(如:斜線)且不推薦使用空白。
(3)sysfs 入口位置對應 kobject 的 parent 指標。若 parent 是 NULL ,則它被設定為嵌入到新 kobject 的 kset 中的 kobject;若 parent 和 kset 都是 NULL, 則sysfs 入口目錄在頂層,通常不推薦。

預設屬性

當建立kobject 時, 每個 kobject 都被給定一系列預設屬性。這些屬性儲存在 kobj_type 結構中:

struct kobj_type {
 void ( * release) ( struct kobject * ) ;
 struct sysfs_ops * sysfs_ops; /*提供實現以下屬性的方法*/
 struct attribute * * default_attrs; /*用於儲存型別屬性列表(指標的指標) */
} ;

struct attribute {
 char * name; /*屬性的名字( 在 kobject 的 sysfs 目錄中顯示)*/
 struct module * owner; /*指向模組的指標(如果有), 此模組負責實現這個屬性*/
 mode_t mode; /*屬性的保護位,modes 的巨集定義在 <linux/stat.h>:例如S_IRUGO 為只讀屬性等等*/
} ; /*default_attrs 列表中的最後一個元素必須用 0 填充*/

 sysfs 讀寫這些屬性是由 kobj_type->sysfs_ops 成員中的函式完成的:

struct sysfs_ops {
 ssize_t ( * show) ( struct kobject * kobj, struct attribute * attr, char * buffer) ;
 ssize_t ( * store) ( struct kobject * kobj, struct attribute * attr, const char * buffer, size_t size) ;
} ;

當用戶空間讀取一個屬性時,核心會使用指向 kobject 的指標(kobj)和正確的屬性結構(*attr)來呼叫show 方法,該方法將給定屬性值編碼進緩衝(buffer)(注意不要越界( PAGE_SIZE 位元組)), 並返回實際資料長度。sysfs 的約定要求每個屬性應當包含一個單個人眼可讀值; 若返回大量資訊,需將它分為多個屬性.

也可對所有 kobject 關聯的屬性使用同一個 show 方法,用傳遞到函式的 attr 指標來判斷所請求的屬性。有的 show 方法包含對屬性名字的檢查。有的show 方法會將屬性結構嵌入另一個結構, 這個結構包含需要返回屬性值的資訊,這時可用container_of 獲得上層結構的指標以返回屬性值的資訊。

store 方法將存在緩衝(buffer)的資料( size 為資料的長度,不能超過 PAGE_SIZE )解碼並儲存新值到屬性(*attr), 返回實際解碼的位元組數。store 方法只在擁有屬性的寫許可權時才能被呼叫。此時注意:接收來自使用者空間的資料一定要驗證其合法性。如果到資料不匹配, 返回一個負的錯誤值。

非預設屬性

雖然 kobject 型別的 default_attrs 成員描述了所有的 kobject 會擁有的屬性,倘若想新增新屬性到 kobject 的 sysfs 目錄屬性只需簡單地填充一個attribute結構並傳遞到以下函式:

int sysfs_create_file( struct kobject * kobj, struct attribute * attr) ;
/*若成功,檔案以attribute結構中的名字建立並返回 0; 否則, 返回負錯誤碼*/
/*注意:核心會呼叫相同的 show() 和 store() 函式來實現對新屬性的操作,所以在新增一個新非預設屬性前,應採取必要的步驟確保這些函式知道如何實現這個屬性*/

若要刪除屬性,呼叫:

int sysfs_remove_file( struct kobject * kobj, struct attribute * attr) ;
/*呼叫後, 這個屬性不再出現在 kobject 的 sysfs 入口。若一個使用者空間程序可能有一個開啟的那個屬性的檔案描述符,在這個屬性已經被刪除後,show 和 store 仍然可能被呼叫*/

二進位制屬性

sysfs 通常要求所有屬性都只包含一個可讀文字格式的值,很少需要建立能夠處理大量二進位制資料的屬性。但當在使用者空間和裝置間傳遞不可改變的資料時(如上傳韌體到裝置 )就需要這個特性。二進位制屬性使用一個 bin_attribute 結構來描述:

struct bin_attribute {
    struct attribute    attr; /*屬性結構體*/
    size_t             size; /*這個二進位制屬性的最大大小(若無最大值則為0)*/
    void             * private ;
    ssize_t ( * read ) ( struct kobject * , char * , loff_t, size_t ) ;
    ssize_t ( * write ) ( struct kobject * , char * , loff_t, size_t ) ;
/*read 和 write 方法類似字元驅動的讀寫方法;,在一次載入中可被多次呼叫,每次呼叫最大操作一頁資料,且必須能以其他方式判斷操作資料的末尾*/
    int ( * mmap) ( struct kobject * , struct bin_attribute * attr,
         struct vm_area_struct * vma) ;
} ;

/*二進位制屬性必須顯式建立,不能以預設屬性被建立,建立一個二進位制屬性呼叫:*/
int sysfs_create_bin_file( struct kobject * kobj, struct bin_attribute * attr) ;

/*刪除二進位制屬性呼叫:*/
int sysfs_remove_bin_file( struct kobject * kobj, struct bin_attribute * attr) ;

符號連結

sysfs 檔案系統具有樹型結構, 反映 kobject之間的組織層次關係。為了表示驅動程式和所管理的裝置間的關係,需要額外的指標,其在 sysfs 中通過符號連結實現。

/*在 sysfs 建立一個符號連結:*/
int sysfs_create_link( struct kobject * kobj, struct kobject * target, char * name) ;
/*函式建立一個連結(name)指向target的 sysfs 入口作為 kobj 的一個屬性,是一個相對連線,與它在sysfs 系統中的位置無關*/

/*刪除符號連線呼叫:*/
void sysfs_remove_link( struct kobject * kobj, char * name) ;

熱插拔事件產生

一個熱插拔事件是一個從核心空間傳送到使用者空間的通知, 表明系統配置已經改變. 無論 kobject 被建立或刪除,都會產生這種事件。熱插拔事件會導致對 /sbin/hotplug 的呼叫, 它通過載入驅動程式, 建立裝置節點, 掛載分割槽或其他正確動作響應事件。
熱插拔事件的實際控制是通過一套儲存於 kset_uevent_ops (《LDD3》中介紹的struct kset_hotplug_ops * hotplug_ops;在2.6.22.2中已經被kset_uevent_ops 結構體替換 結構的方法完成:

struct kset_uevent_ops {
    int ( * filter) ( struct kset * kset, struct kobject * kobj) ;
    const char * ( * name) ( struct kset * kset, struct kobject * kobj) ;
    int ( * uevent) ( struct kset * kset, struct kobject * kobj, char * * envp,
            int num_envp, char * buffer, int buffer_size) ;
} ;

可以在 kset 結構的uevent_ops 成員中找到指向kset_uevent_ops結構的指標。

若在 kobject 中不包含指定的 kset , 核心將通過 parent 指標在分層結構中進行搜尋,直到發現一個包含有kset的 kobject ; 接著使用這個 kset 的熱插拔操作。

kset_uevent_ops 結構中的三個方法作用如下:
(1) filter 函式讓 kset 程式碼決定是否將事件傳遞給使用者空間。如果 filter 返回 0,將不產生事件。以磁碟的 filter 函式為例,它只允許kobject產生磁碟和分割槽的事件,原始碼如下:

static int block_hotplug_filter( struct kset * kset, struct kobject * kobj)
{
 struct kobj_type * ktype = get_ktype( kobj) ;
    return ( ( ktype = = & ktype_block) | | ( ktype = = & ktype_part) ) ;
}

(2) 當呼叫使用者空間的熱插拔程式時,相關子系統的名字將作為唯一的引數傳遞給它。name 函式負責返回合適的字串傳遞給使用者空間的熱插拔程式。

(3)熱插拔指令碼想得到的任何其他引數都通過環境變數傳遞。uevent 函式的作用是在呼叫熱插拔指令碼之前將引數新增到環境變數中。函式原型:

int ( * uevent) ( struct kset * kset, struct kobject * kobj, /*產生事件的目標物件*/
 char * * envp, /*一個儲存其他環境變數定義(通常為 NAME=value 的格式)的陣列*/
 int num_envp, /*環境變數陣列中包含的變數個數(陣列大小)*/
 char * buffer, int buffer_size/*環境變數被編碼後放入的緩衝區的指標和位元組數(大小)*/
/*若需要新增任何環境變數到 envp, 必須在最後的新增項後加一個 NULL 入口,使核心知道陣列的結尾*/
        ) ;
/*返回值正常應當是 0,若返回非零值將終止熱插拔事件的產生*/

熱插拔事件的產生通常是由在匯流排驅動程式層的邏輯所控制。

以上是Linux裝置模型的底層原理簡介,具體的細節應該參閱核心原始碼和《ULK3》。