Linux裝置模型(底層原理簡介)
以《LDD3》的說法:Linux裝置模型這部分內容可以認為是進階教材,對於多數程式作者來說是不必要的。但是我個人認為:對於一個嵌入式Linux的底層程式員來說,這部分內容是很重要的。以我學習的ARM9為例,有很多匯流排(如SPI、IIC、IIS等等)在Linux下已經被編寫成了子系統,無需自己寫驅動;而這些匯流排又不像PCI、USB等在《LDD3》上有教程,有時還要自己研究它的子系統構架,甚至要自己添加一個新的匯流排類型。
對於這方面的學習,我推薦幾個網頁,這些也是我這部分文章的參考資料:
(1)《 Linux那些事兒 之 我是Sysfs》來源於複旦和交大三個牛人的Linux技術部落格:http://blog.csdn.net/fudan_abc (複旦_abc)他們還分析了很多Linux的驅動,值得珍藏!
(2)《linux裝置模型詳解》也是一個牛人的部落格文章,部落格網址:http://hi.baidu.com/csdeny/blog
(3)《s3c2410裝置的註冊》是一篇關於2410中linux核心實現裝置模型的不可多得的好資料。網址:http://blog.chinaunix.net/u1/41638/showart_438078.html
(4)luofuchong的部落格 ,此人分析了一些2410中的Linux子系統(如SPI,input等),實力不凡,值得關注。網址:http://www.cnitblog.com/luofuchong/
在這部分的學習中,將會先研究linux裝置模型的每個元素,最後將其一步一步整合,至底向上地分析。一開始會比較摸不著頭腦,到了整合階段就柳暗花明了。我之所以沒有先介紹整體,再分析每個部分是因為如果不對每個元素做認真分析,看了整體也會雲裡霧裡(我試過了,恕小生愚鈍)。所以一開始要耐著性子看,到整合階段就會豁然開朗。
Linux裝置模型的目的是:為核心建立起一個統一的裝置模型,從而有一個對系統結構的一般性抽象描述。
現在核心使用裝置模型支援多種不同的任務:
電源管理和系統關機 :這些需要對系統結構的理解,裝置模型使OS能以正確順序遍曆系統硬體。
與使用者空間的通訊 :sysfs 虛擬檔案系統的實現與裝置模型的緊密相關, 並向外界展示它所表述的結構。向使用者空間提供系統資訊、改變巨集指令引數的介面正越來越多地通過 sysfs , 也就是裝置模型來完成。
熱插拔裝置
裝置類型:裝置模型包括了將裝置分類的機制,在一個更高的功能層上描述這些裝置, 並使裝置對使用者空間可見。
對象生命週期:裝置模型的實現需要建立一系列機制來處理對象的生命週期、對象間的關係和對象在使用者空間的表示。
Linux 裝置模型是一個複雜的資料結構。但對模型的大部分來說, Linux 裝置模型代碼會處理好這些關係, 而不是把他們強加於驅動作者。模型隱藏於互動的背後,與裝置模型的直接互動通常由匯流排級的邏輯和其他的核心子系統處理。所以許多驅動作者可完全忽略裝置模型, 並相信裝置模型能處理好他所負責的事。
在此之前請先瞭解一下sysfs,請看 Linux那些事兒之我是Sysfs(1)sysfs初探 我就不在這裡廢話了!這裡還建議先看看 sysfs 的核心文檔/Documentation/filesystems/sysfs.txt,我將其翻譯好做成PDF,:http://blogimg.chinaunix.net/blog/upfile2/071229162826.pdf
如有錯誤歡迎指正!
Kobject、Kset 和 Subsystem
Kobjects
kobject是一種資料結構,定義在 。
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的指標,關係如下:
圖表中的所有的被包含的 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 目錄中以檔案的形式出現, 其中的內容由核心產生。 包含 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 的宏定義在:例如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》。