天天看點

Linux裝置模型剖析系列一(基本概念、kobject、kset、kobj_type)

一、基本概念

1. 前言

  • 在蝸蝸的“​​Linux核心的整體架構​​”一文中有提到,由于Linux支援世界上幾乎所有的、不同功能的硬體裝置(這是Linux的優點),導緻Linux核心中有一半的代碼是裝置驅動,而且随着硬體的快速疊代,裝置驅動的代碼量也在快速增長。它導緻Linux核心非常臃腫、雜亂、不易維護。為了降低裝置多樣性帶來的Linux驅動開發的複雜度,以及裝置熱拔插處理、電源管理等,Linux核心提出了裝置模型(也稱作driver Model)的概念。
  • 裝置模型将硬體裝置歸納、分類,然後抽象出一套标準的資料結構和接口。驅動的開發,就簡化為對核心基本資料結構的填充和實作。本文将會從裝置模型的基本概念開始,通過分析核心相應的代碼,逐漸解析Linux裝置模型的實作及使用方法。
  • 核心2.4之前沒有統一的裝置驅動模型,核心2.4到2.6之間使用devfs,裝置檔案挂載在/dev目錄。但它需要在驅動代碼中調用​

    ​devfs_register​

    ​來對裝置檔案進行命名,使用者空間不可改變。核心2.6版本後使用sysfs,裝置檔案挂載在/sys目錄。
  • sysfs是一個虛拟檔案系統,類似proc檔案系統sysfs下一個目錄對應一個kobject對象sysfs下每一個目錄所對應的inode節點會記錄基本驅動對象kobject,進而将系統中的裝置組成層次結構使用者可以通過修改這些目錄下的不同檔案來配置驅動對象kobject的不同屬性。
  • 将裝置分類、分層統一進行管理配合守護程序udev或者mdev,系統可以動态建立裝置檔案,命名規則可由使用者自定義。

2. Linux裝置模型的基本概念

2.1 Bus, class, device和device_driver的概念

下圖是嵌入式系統常見的硬體拓撲的一個示例:

Linux裝置模型剖析系列一(基本概念、kobject、kset、kobj_type)
  • 硬體拓撲描述Linux裝置模型中四個重要概念中的三個:Bus,class和device(第四個為device driver,後面會說):
  • Bus(總線):Linux認為(可以參考include/linux/device.h中struct bus_type的注釋),總線是CPU和一個或多個裝置之間資訊互動的通道。而為了友善裝置模型的抽象,所有的裝置都應連接配接到總線上(無論是CPU内部總線、虛拟的總線還是platform Bus)。
  • class(分類):在Linux裝置模型中,class的概念非常類似面向對象程式設計中的class(類),它主要是集合具有相似功能或屬性的裝置,這樣就可以抽象出一套可以在多個裝置之間共用的資料結構和接口函數。因而從屬于相同class的裝置的驅動程式,就不再需要重複定義這些公共資源,直接從class中繼承即可。
  • device(裝置):抽象系統中所有的硬體裝置,描述它的名字、屬性、從屬的Bus、從屬的class等資訊。
  • device driver(驅動):Linux裝置模型用driver抽象硬體裝置的驅動程式,它包含裝置初始化、電源管理相關的接口實作。而Linux核心中的驅動開發,基本都圍繞該抽象進行(實作所規定的接口函數)。

什麼是platform Bus?

在計算機中有這樣一類裝置,它們通過各自的裝置控制器,直接和CPU連接配接,CPU可以通過正常的尋址操作通路它們(或者說通路它們的控制器)。這種連接配接方式,并不屬于傳統意義上的總線連接配接。但裝置模型應該具備普适性,是以Linux就虛構了一條platform Bus,供這些裝置挂靠。

2.2 裝置模型的核心思想

  • Linux裝置模型的核心思想是通過xxx手段,實作xxx目的:
  1. 用​

    ​struct device​

    ​​和​

    ​struct device_driver​

    ​兩個資料結構,分别從“有什麼用”和“怎麼用”兩個角度描述硬體裝置。這樣就統一了編寫裝置驅動的格式,使驅動開發從論述題變為填空體,進而簡化了裝置驅動的開發。
  2. 同樣使用這兩個資料結構,實作硬體裝置的即插即用(熱拔插)。
  • 因為在Linux核心中,隻要任何device和device driver具有相同的名字,核心就會執行​

    ​device_driver​

    ​​結構中的初始化函數​

    ​probe​

    ​,該函數會初始化裝置,使其為可用狀态。
  • 對大多數熱拔插裝置而言,它們的device driver一直存在核心中。當裝置沒有插入時,其device結構不存在,因而其driver也就不執行初始化操作。當裝置插入時,核心會建立一個device結構(名稱和driver相同),此時就會觸發driver的執行。這就是即插即用的概念。
  1. 通過"Bus–>device”類型的樹狀結構(見2.1章節的圖例)解決裝置之間的依賴,而這種依賴在開關機、電源管理等過程中尤為重要。
  • 試想,一個裝置挂載在一條總線上,要啟動這個裝置,必須先啟動它所挂載的總線。很顯然,如果系統中裝置非常多、依賴關系非常複雜的時候,無論是核心還是驅動的開發人員,都無力維護這種關系。
  • 裝置模型中的這種樹狀結構,可以自動處理這種依賴關系。啟動某一個裝置前,核心會檢查該裝置是否依賴其它裝置或者總線,如果依賴,則檢查所依賴的對象是否已經啟動,如果沒有,則會先啟動它們,直到啟動該裝置的條件具備為止。而驅動開發人員需要做的,就是在編寫裝置驅動時,告知核心該裝置的依賴關系即可。
  1. 使用class結構,在裝置模型中引入面向對象的概念,這樣可以最大限度地抽象共性,減少驅動開發過程中的重複勞動,降低工作量。

二、kobject、kset、kobj_type

1. 前言

  • kobject是Linux裝置模型的基本單元,也是裝置模型中最難了解的一部分(可參考核心文檔Documentation/kobject.txt的表述)。是以有必要先把它分析清楚。

2. 基本概念

  • 由前文可知,Linux裝置模型的核心是使用Bus、class、device、driver四個核心資料結構,将大量不同功能的硬體裝置及其驅動,以樹狀結構的形式,進行歸納、抽象,進而友善kernel的統一管理。而硬體裝置的數量、種類是非常多的,這就決定了kernel中将會有大量的有關裝置模型的資料結構。這些資料結構一定有一些共同的功能,需要抽象出來統一實作,否則就會不可避免的産生備援代碼。這就是kobject誕生的背景。
  • 目前為止,kobject主要提供如下功能:
  1. 通過parent指針,可以将所有kobject以層次結構的形式組合起來。
  2. 使用一個引用計數(reference count),來記錄kobject被引用的次數,并在引用次數變為0時把它釋放(這是kobject誕生時的唯一功能)。
  3. 和sysfs虛拟檔案系統配合,将每一個kobject及其特性,以檔案的形式顯示到使用者空間。

注1:在Linux中,kobject幾乎不會單獨存在。它的主要功能,就是内嵌在一個大型的資料結構中,為這個資料結構提供一些底層的功能實作。

注2:Linux driver開發者,很少會直接使用kobject以及它提供的接口,而是使用建構在kobject之上的裝置模型接口。

3. 代碼解析

3.1 在Linux kernel 中的位置

在kernel源代碼中,kobject由如下兩個檔案實作:

  • include/linux/kobject.h
  • lib/kobject.c

其中kobject.h為kobject的頭檔案,包含所有的資料結構定義和接口聲明。kobject.c為核心功能的實作。

3.2 主要資料結構及其邏輯關系

  • 在描述資料結構之前,有必要說明一下​

    ​kobject​

    ​​, ​

    ​Kset​

    ​​和​

    ​ktype​

    ​這三個概念:
  1. kobject是基本資料類型,每個kobject都會在"/sys/“檔案系統中以目錄的形式出現。
  2. ktype代表kobject(嚴格地講,是包含了kobject的資料結構)的屬性操作集合。由于通用性,多個kobject可能共用同一個屬性操作集,是以把ktype獨立出來了。
  3. Kset是一個特殊的kobject(是以它也會在"/sys/“檔案系統中以目錄的形式出現),它用來集合相似的kobject(這些kobject可以是相同屬性的,也可以不同屬性的)。
  4. 他們的邏輯關系如下圖所示:
  • kset可批量管理kobject
  • kset繼承自kobject,且将一組kobject串連成一個連結清單進行統一管理
  • kobject既可以通過parent指針找到上層kobject,也可以通過kset指針找到其上層kobject。但上層kobject對象無法周遊到下層,是以較少使用。

3.2.1 kobject的原型

/* kobject: include/linux/kobject.h line 60 */
  struct kobject {
    const char      *name;
    struct list_head    entry;
    struct kobject      *parent;
    struct kset       *kset;
    struct kobj_type    *ktype;
    struct sysfs_dirent *sd;
    struct kref       kref;
  #ifdef CONFIG_DEBUG_KOBJECT_RELEASE
      struct delayed_work release;
  #endif
    unsigned int    state_initialized:1;
    unsigned int    state_in_sysfs:1;
    unsigned int    state_add_uevent_sent:1;
    unsigned int    state_remove_uevent_sent:1;
    unsigned int    uevent_suppress:1;
  };      
  • name:該kobject的名稱,同時也是sysfs中的目錄名稱。由于kobject添加到kernel時,需要根據名字注冊到sysfs中,之後就不能再直接修改該字段。如果需要修改kobject的名字,需要調用​

    ​kobject_rename​

    ​接口,該接口會主動處理sysfs的相關事宜。
  • entry:用于将kobject加入到Kset中的​

    ​list_head​

    ​。
  • parent:指向parent kobject,以此形成層次結構(在sysfs就表現為目錄結構)。
  • kset:該kobject屬于的Kset(可以為NULL)。如果沒有指定parent,則會把Kset作為parent(别忘了Kset是一個特殊的kobject)。
  • ktype:該kobject屬于的kobj_type。每個kobject必須有一個ktype,否則kernel會提示錯誤。
  • sd:該kobject在sysfs中的表示。
  • kref:​

    ​struct kref​

    ​類型(在include/linux/kref.h中定義)的變量,是一個可用于原子操作的引用計數。
struct kref {
  atomic_t refcount;
};      
  • state_initialized:訓示該kobject是否已經初始化,以在kobject的Init,Put,Add等操作時進行異常校驗。
  • state_in_sysfs:訓示該kobject是否已在sysfs中呈現,以便在自動登出時從sysfs中移除。
  • state_add_uevent_sent:記錄是否已經向使用者空間發送ADD uevent。
  • state_remove_uevent_sent:記錄是否已經向使用者空間發送REMOVE uevent,
  • uevent_suppress:如果該字段為1,則表示忽略所有上報的uevent事件。
注意:uevent提供了“使用者空間通知”的功能實作,通過該功能,當核心中有kobject的增删改等動作時,會通知使用者空間。

3.2.2 kset的原型

/* include/linux/kobject.h, line 159 */
    /*
    * struct kset - a set of kobjects of a specific type, belonging to a specific subsystem.
    *
    * A kset defines a group of kobjects.  They can be individually
    * different "types" but overall these kobjects all want to be grouped
    * together and operated on in the same manner.  ksets are used to
    * define the attribute callbacks and other common events that happen to
    * a kobject.
     *
    * @list: the list of all kobjects for this kset
    * @list_lock: a lock for iterating over the kobjects
    * @kobj: the embedded kobject for this kset (recursion, isn't it fun...)
    * @uevent_ops: the set of uevent operations for this kset.  These are
    * called whenever a kobject has something happen to it so that the kset
    * can add new environment variables, or filter out the uevents if so
    * desired.
    */
   struct kset {
         struct list_head list;
         spinlock_t list_lock;
         struct kobject kobj;
         const struct kset_uevent_ops *uevent_ops;
   };      
  • list:指向該kset下所有的kobject的連結清單。
  • list_lock:避免操作連結清單時産生競态的自旋鎖。
  • kobj:該kset自己的kobject(kset是一個特殊的kobject,也會在sysfs中以目錄的形式展現)。
  • uevent_ops:該kset的uevent操作函數集,當kset中的某些kobject對象狀态發生變化需要通知使用者空間時,調用其中對應的函數來完成。當任何kobject需要上報uevent時,都要調用它所從屬的kset的uevent_ops,添加環境變量,或者過濾event(kset可以決定哪些event可以上報)。是以,如果一個kobject不屬于任何kset時,是不允許發送uevent的。

3.2.3 ktype的原型

/* include/linux/kobject.h, line 108 */
    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);
   };      
  • release:通過該回調函數,可以将包含該類型kobject的資料結構的記憶體空間釋放掉。
  • sysfs_ops:該種類型的kobject的sysfs檔案系統接口(讀屬性接口show及寫屬性接口store)。
  • default_attrs:該種類型的kobject的atrribute清單(所謂attribute,就是sysfs檔案系統中的一個檔案)。将會在kobject添加到核心時,一并注冊到sysfs中。
  • child_ns_type/namespace:和檔案系統(sysfs)的命名空間有關,這裡不再詳細說明。

3.2.4 整個kobject機制的了解

  • kobject的核心功能是:保持一個引用計數,當該計數減為0時,自動釋放(由本文所講的kobject子產品負責) kobject所占用的meomry空間。這就決定了kobject必須是動态配置設定的,因為隻有這樣才能動态釋放。
  • kobject大多數的使用場景,是内嵌在大型的資料結構中(如Kset、device_driver等),是以這些大型的資料結構,也必須是動态配置設定、動态釋放的。
  • 那麼釋放的時機是什麼呢?是内嵌的kobject釋放時。但是kobject的釋放是由kobject子產品自動完成的(在引用計數為0時),那麼怎麼一并釋放包含自己的大型資料結構呢?
  • 這時ktype就派上用場了。因為ktype中的release回調函數負責釋放kobject(甚至是包含kobject的資料結構)的記憶體空間
  • 那麼ktype及其内部函數,是由誰實作呢?是由上層資料結構所在的子產品!因為隻有它才清楚kobject嵌在哪個資料結構中,并通過kobject指針以及自身的資料結構類型,利用函數宏containerof找到需要釋放的上層資料結構的指針,然後釋放它。
  • 是以,每一個内嵌kobject的資料結構,例如kset、device、device_driver等等,都要實作一個ktype,并定義其中的回調函數。同理,sysfs相關的操作也一樣,必須經過ktype的中轉,因為sysfs看到的是kobject,而真正的檔案操作的主體,是内嵌kobject的上層資料結構!
  • kobject是面向對象的思想在Linux kernel中的極緻展現,但C語言的優勢卻不在這裡,是以Linux kernel需要用比較巧妙的手段去實作它。

3.3 接口功能分析

3.3.1 kobject使用流程

  • kobject大多數情況下(有一種例外,下面會講)會嵌在其它資料結構中使用,其使用流程如下:
  1. 定義一個struct kset類型的指針,并在初始化時為它配置設定空間,添加到核心中;
  2. 根據實際情況,定義内嵌有kobject的自己所需的資料結構原型;
  3. 定義一個适合自己的ktype,并實作其中回調函數release;
  4. 在需要使用到包含kobject的資料結構時,動态配置設定該資料結構,并配置設定kobject空間,添加到核心中;
  5. 每一次引用資料結構時,調用​

    ​kobject_get​

    ​​接口增加引用計數;引用結束時,調用​

    ​kobject_put​

    ​接口,減少引用計數;
  6. 當引用計數為0時,kobject子產品會自動調用ktype所提供的release接口,釋放上層資料結構以及kobject的記憶體空間。
  • 上面有提過有一種例外就是:開發者隻需要在sysfs中建立一個目錄,而不需要其它的kset、ktype的操作。這時可以直接調用​

    ​kobject_create_and_add​

    ​接口,配置設定一個kobject結構并把它添加到核心中。

3.3.2 kobject的配置設定和釋放

  • 前面講過,kobject必須動态配置設定,而不能靜态定義或者位于堆棧之上,它的配置設定方法有兩種:
  1. 通過kmalloc自行配置設定(一般是跟随上層資料結構配置設定),并在初始化後添加到kernel。這種方法涉及如下接口:
/* include/linux/kobject.h, line 85 */
void kobject_init(struct kobject *kobj, struct kobj_type *ktype);

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

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

    ​:初始化通過kmalloc等記憶體配置設定函數獲得的struct kobject指針。主要執行邏輯為:
  1. 确認kobj和ktype不為空;
  2. 如果該指針已經初始化過(判斷kobj->state_initialized),列印錯誤提示及堆棧資訊(但不是緻命錯誤,是以還可以繼續);
  3. 初始化kobj内部的參數,包括引用計數kref、list_head、各種标志等;
  4. 将輸入形參ktype賦予kobj->ktype
  • ​kobject_add​

    ​:如果koibject需要添加到sysfs中,則必須要調用kobject_add函數。它将初始化完成的kobject添加到kernel中,參數包括需要添加的kobject、該kobject的parent(用于形成層次結構,可以為空)、用于提供kobject name的格式化字元串。主要執行邏輯為:
  1. 确認kobj不為空,确認kobj已經初始化,否則錯誤退出;
  2. 調用内部接口​

    ​kobject_add_varg​

    ​,完成添加操作,具體調用邏輯如下:
kobject_add ——> kobject_add_varg ——> kobject_set_name_vargs //解析fmt,設定name,并指向parent;
                   ——> kobject_add_internal ——> kobject_get(kobj->parent)  //對parent的引用計數加1
                              ——> creat_dir  ——> sysfs_creat_dir  //完成sysfs的目錄建立
                                      ——> populate_dir   //完成sysfs目錄下的檔案建立
                                    ——> sysfs_get(kobj->sd)
                                    ——> kobj_child_ns_ops
                              ——> kobj->state_in_sysfs = 1; //表示kobject已經添加到sysfs中      
  • populate_dir中逐個處理核心對象所屬對象類型的預設屬性,對每個屬性,調用sysfs_create_file函數在核心對象的目錄下建立以屬性名為名字的檔案。kobject_add中隻會為預設屬性自動建立檔案。
  1. kobject_add定義
int kobject_add(struct kobject *kobj, struct kobject *parent,
                  const char *fmt, ...)
  {       
          va_list args;
          int retval;
  
        if (!kobj)
                  return -EINVAL;
  
        if (!kobj->state_initialized) {
                  printk(KERN_ERR "kobject '%s' (%p): tried to add an "
                         "uninitialized object, something is seriously wrong.\n",
                         kobject_name(kobj), kobj);
                  dump_stack();
                  return -EINVAL;
          }
          va_start(args, fmt);
          retval = kobject_add_varg(kobj, parent, fmt, args);
          va_end(args);
  
          return retval;
  }      
  • ​kobject_init_and_add​

    ​,是上面兩個接口的組合,不再說明。

内部接口=====

kobject_add_varg:解析格式化字元串,将結果賦予kobj->name,之後調用​

​kobject_add_internal​

​接口,完成真正的添加操作。

kobject_add_internal:将kobject添加到kernel。主要執行邏輯為:

  • 校驗kobj以及kobj->name的合法性,若不合法列印錯誤資訊并退出;
  • 調用kobject_get增加該kobject的parent的引用計數(如果存在parent的話);
  • 如果存在kset,則調用​

    ​kobj_kset_join​

    ​接口将本kobject加入到此kset的kobject連結清單中。同時,如果該kobject沒有parent,卻存在kset,則将它的parent設為kset(kset是一個特殊的kobject),并增加kset的引用計數;
  • 通過​

    ​create_dir​

    ​接口,調用sysfs的相關接口,在sysfs下建立該kobject對應的目錄;
  • 如果建立失敗,則執行後續的復原操作,否則将kobj->state_in_sysfs置為1;
這種方式配置設定的kobject,會在引用計數變為0時,由kobject_put調用其ktype的release接口,釋放記憶體空間,具體可參考後面有關kobject_put的講解。
  1. 使用kobject_create建立
  • kobject子產品可以使用kobject_create自行配置設定空間,并内置了一個ktype(dynamic_kobj_ktype),用于在計數為0時釋放空間。代碼如下:
/* include/linux/kobject.h, line 96 */
extern struct kobject * __must_check kobject_create(void);

extern struct kobject * __must_check kobject_create_and_add(const char *name, struct kobject *parent);      
  • kobject_create:該接口為kobj配置設定記憶體空間,并以​

    ​dynamic_kobj_ktype​

    ​為參數,調用kobject_init接口,完成後續的初始化操作。
struct kobject *kobject_create(void)
{
    struct kobject *kobj;
    kobj = kzalloc(sizeof(*kobj), GFP_KERNEL);
    if (!kobj)
        return NULL;
    kobject_init(kobj, &dynamic_kobj_ktype);    //預設屬性
    return kobj;
}      
  • kobject_create_and_add:是kobject_create和kobject_add的組合,不再說明。
/* lib/kobject.c, line 605 */
static struct kobj_type dynamic_kobj_ktype = {
  .release    = dynamic_kobj_release,
  .sysfs_ops  = &kobj_sysfs_ops,
};

static void dynamic_kobj_release(struct kobject *kobj)
{
  pr_debug("kobject: (%p): %s\n", kobj, __func__);
  kfree(kobj);
}      
  • dynamic_kobj_release:直接調用kfree釋放kobj的空間。

3.3.3 kobject引用計數的加減

  • 通過​

    ​kobject_get​

    ​​和​

    ​kobject_put​

    ​可以修改kobject的引用計數kref,并在kref為0時,調用對應ktype的release接口,釋放占用空間。
/* include/linux/kobject.h, line 103 */
struct kobject *kobject_get(struct kobject *kobj);

void kobject_put(struct kobject *kobj);      
  • kobject_get:調用​

    ​kref_get​

    ​,增加引用計數。
  • kobject_put:以内部接口kobject_release為參數,調用​

    ​kref_put​

    ​。kref子產品會在引用計數為零時,調用kobject_release。

=内部接口=====

kobject_release:通過kref結構,擷取kobject指針,并調用kobject_cleanup接口繼續。

  • 檢查該kobject是否有ktype,如果沒有,列印警告資訊;
  • 如果該kobject向使用者空間發送了ADD uevent但沒有發送REMOVE uevent,補發REMOVE uevent;
  • 如果該kobject有在sysfs檔案系統注冊,調用kobject_del接口,删除它在sysfs中的注冊;
  • 調用該kobject的ktype的release接口,釋放記憶體空間;
  • 釋放該kobject的name所占用的記憶體空間;

3.3.4 Kset的初始化、注冊

  • Kset是一個特殊的kobject,是以其初始化、注冊等操作也會調用kobject的相關接口,除此之外,會有它特有的部分。另外,和kobject一樣,kset的記憶體配置設定,可以由上層軟體通過kmalloc自行配置設定,也可以由kobject子產品負責配置設定,具體如下。
/* include/linux/kobject.h, line 166 */
extern void kset_init(struct kset *kset);

extern int __must_check kset_register(struct kset *kset);

extern void kset_unregister(struct kset *kset);

extern struct kset * __must_check kset_create_and_add(const char *name,
                                                      const struct kset_uevent_ops *u,
                                                      struct kobject *parent_kobj);      
  • kset_init:該接口用于初始化已配置設定的kset,主要包括調用​

    ​kobject_init_internal​

    ​初始化其kobject,然後初始化kset的連結清單。需要注意的時,如果使用此接口,上層軟體必須提供該kset中的kobject的ktype。
  • kset_register:先調用​

    ​kset_init​

    ​​,然後調用​

    ​kobject_add_internal​

    ​将其kobject添加到kernel。
  • kset_unregister:直接調用​

    ​kobject_put​

    ​釋放其kobject。當其kobject的引用計數為0時,即調用ktype的release接口釋放kset占用的空間。
  • kset_create_and_add:會調用内部接口​

    ​kset_create​

    ​​動态建立一個kset,并調用​

    ​kset_register​

    ​将其注冊到kernel。

繼續閱讀