天天看點

Linux裝置模型剖析系列之二(uevent、sysfs)

三、uevent

1. uevent的功能

  • uevent是kobject的一部分,用于在kobject狀态發生改變時(增加、移除等),通知使用者空間程式。使用者空間程式收到這樣的事件後,會做相應的處理。該機制通常是用來支援熱拔插裝置的,例如U盤插入後,USB相關的驅動軟體會動态建立用于表示該U盤的device結構(相應的也包括其中的kobject),并告知使用者空間程式,為該U盤動态的建立/dev/目錄下的裝置節點,更進一步,可以通知其它的應用程式,将該U盤裝置mount到系統中,進而動态的支援該裝置。

2. uevent在kernel中的位置

下面圖檔描述了uevent子產品在核心中的位置:

Linux裝置模型剖析系列之二(uevent、sysfs)

由此可知,uevent的機制是比較簡單的,裝置模型中任何裝置有事件需要上報時,會觸發uevent提供的接口。uevent子產品準備好上報事件的格式後,可以通過兩個途徑把事件上報到使用者空間:一種是通過kmod子產品,直接調用使用者空間的可執行檔案;另一種是通過netlink通信機制,将事件從核心空間傳遞給使用者空間。有關kmod和netlink,會在其它文章中描述,是以本文就不再詳細說明了。

3. uevent的内部邏輯解析

3.1 源代碼的位置

uevent的代碼比較簡單,主要涉及kobject.h和kobject_uevent.c兩個檔案,如下:

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

3.2 資料結構描述

kobject.h定義了uevent相關的常量和資料結構,如下:

  • kobject_action
/* include/linux/kobject.h, line 50 */
enum kobject_action {
    KOBJ_ADD,
    KOBJ_REMOVE,    
    KOBJ_CHANGE, 
    KOBJ_MOVE,
    KOBJ_ONLINE, 
    KOBJ_OFFLINE,
    KOBJ_MAX 
};      
  • kobject_action定義了event的類型,包括:

ADD/REMOVE: kobject(或上層資料結構)的添加/移除事件。

ONLINE/OFFLINE:kobject(或上層資料結構)的上線/下線事件,即是否使能。

CHANGE: kobject(或上層資料結構)的狀态或者内容發生改變。如果裝置驅動需要上報的事件不再上面事件的範圍内,或者是自定義的事件,可以使用該event,并攜帶相應的參數。

MOVE: kobject(或上層資料結構)更改名稱或者更改parent(意味着在sysfs中更改了目錄結構)。

MAX: 事件種類的最大值

  • kobj_uevent_env
/* include/linux/kobject.h, line 31 */
#define UEVENT_NUM_ENVP         32 /* number of env pointers */
#define UEVENT_BUFFER_SIZE      2048 /* buffer for the variables */
 
/* include/linux/kobject.h, line 116 */
struct kobj_uevent_env {
    char *envp[UEVENT_NUM_ENVP];
    int envp_idx;
    char buf[UEVENT_BUFFER_SIZE];
    int buflen;
};      

envp,指針數組,用于儲存每個環境變量的位址,最多可支援的環境變量數量為UEVENT_NUM_ENVP。

envp_idx,用于通路環境變量指針數組的index。

buf,儲存環境變量的buffer,最大為UEVENT_BUFFER_SIZE。

buflen,通路buf的變量。

  • 前面有提到過,在利用kmod向使用者空間上報event事件時,會直接執行使用者空間的可執行檔案。而在Linux系統,可執行檔案的執行,依賴于環境變量,是以kobj_uevent_env用于組織此次事件上報時的環境變量。
  • kset_uevent_ops
/* include/linux/kobject.h, line 123 */
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);
};      
  • kset_uevent_ops是為kset量身訂做的一個資料結構,裡面包含filter和uevent兩個回調函數,用處如下:
  • filter:當任何kobject需要上報uevent時,它所屬的kset可以通過該接口過濾,阻止(過濾掉)不希望上報的event,進而達到從整體上管理的目的。
  • uevent:當任何kobject需要上報uevent時,它所屬的kset可以通過該接口統一為這些event添加環境變量。因為很多時候上報uevent時的環境變量都是相同的,是以可以由kset統一處理,就不需要讓每個kobject獨自添加了。
  • name:該接口可以傳回kset的名稱。如果一個kset沒有合法的名稱,則其下的所有kobject将不允許上報uevent。

3.3 内部接口

  • 通過​

    ​kobject.h​

    ​,uevent子產品提供了如下的API(這些API的實作是在​

    ​lib/kobject\_uevent.c​

    ​檔案中):
/* include/linux/kobject.h, line 206 */
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp[]);
int add_uevent_var(struct kobj_uevent_env *env, const char *format, ...);
int kobject_action_type(const char *buf, size_t count, enum kobject_action *type);      
3.3.1 kobject_uevent_env
  • ​kobject_uevent_env​

    ​函數以envp為環境變量,上報一個指定action的uevent。環境變量的作用是為執行使用者空間程式指定運作環境。具體動作如下:
  1. 查找kobj本身或者其parent是否從屬于某個kset,如果不是,則報錯傳回(如果一個kobject沒有加入kset,是不允許上報uevent的);
  2. 檢視kobj->uevent_suppress是否設定,如果設定,則忽略所有的uevent上報并傳回(可以通過kobject的uevent_suppress标志,管控kobject的uevent的上報);
  3. 如果所屬的kset有​

    ​uevent_ops->filter​

    ​函數,則調用該函數,過濾此次上報(kset可以通過filter接口過濾不希望上報的event,進而達到整體的管理效果);
  4. 判斷所屬的kset是否有合法的名稱(稱作subsystem,和前期的核心版本有差別),否則不允許上報uevent;
  5. 配置設定一個用于此次上報的、存儲環境變量的buffer(結果儲存在env指針中),并獲得該kobject在sysfs中的路徑資訊(使用者空間軟體需要依據該路徑資訊在sysfs中通路它);
  6. 調用add_uevent_var接口(下面會介紹),将action、路徑資訊、subsystem等資訊,添加到env指針中;
  7. 如果傳入的envp不空,則解析傳入的環境變量,同樣調用add_uevent_var接口,添加到env指針中;
  8. 如果所屬的kset存在uevent_ops->uevent接口,調用該接口,添加kset統一的環境變量到env指針;
  9. 根據ACTION的類型,設定kobj->state_add_uevent_sent和kobj->state_remove_uevent_sent變量,以記錄正确的狀态;
  10. 調用add_uevent_var接口,添加格式為"SEQNUM=%llu”的序列号;
  11. 如果定義了"CONFIG_NET”,則使用netlink發送該uevent;
  12. 以uevent_helper、subsystem以及添加了标準環境變量(HOME=/,PATH=/sbin:/bin:/usr/sbin:/usr/bin)的env指針為參數,調用kmod子產品提供的​

    ​call_usermodehelper​

    ​函數,上報uevent。
  1. ​uevent_helper​

    ​的内容是由核心配置項CONFIG_UEVENT_HELPER_PATH(位于./drivers/base/Kconfig)決定的(可參考lib/kobject_uevent.c, line 32),該配置項指定了一個使用者空間程式(或者腳本),用于解析上報的uevent,例如"/sbin/hotplug”。
  2. ​call_usermodehelper​

    ​的作用,就是fork一個程序,以uevent為參數,執行uevent_helper。
3.3.2 kobject_uevent
  • ​kobject_uevent​

    ​,和kobject_uevent_env功能一樣,隻是沒有指定任何的環境變量。
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
  return kobject_uevent_env(kobj, action, NULL);
}      
3.3.3 add_uevent_var
  • ​add_uevent_var​

    ​以格式化字元的形式(類似printf、printk等),将環境變量copy到env指針中。
3.3.4 kobject_action_type
  • ​kobject_action_type​

    ​将enum kobject_action類型的action轉換為字元串。
  • 注意:怎麼指定處理uevent的使用者空間程式(簡稱uevent helper)?
  • 上面介紹kobject_uevent_env的内部動作時,有提到,uevent子產品通過Kmod上報uevent時,會通過call_usermodehelper函數,調用使用者空間的可執行檔案(或者腳本,簡稱*uevent helper* )處理該event。而該uevent helper的路徑儲存在**uevent_helper數組中。
  • 可以在編譯核心時,通過CONFIG_UEVENT_HELPER_PATH配置項,靜态指定uevent helper。但這種方式會為每個event fork一個程序,随着核心支援的裝置數量的增多,這種方式在系統啟動時将會是緻命的(可以導緻記憶體溢出等)。是以隻有在早期的核心版本中會使用這種方式,現在核心不再推薦使用該方式。是以核心編譯時,需要把該配置項留白。
  • 在系統啟動後,大部分的裝置已經ready,可以根據需要,重新指定一個uevent helper,以便檢測系統運作過程中的熱拔插事件。這可以通過把helper的路徑寫入到​

    ​/sys/kernel/uevent_helper​

    ​檔案中實作。實際上,核心通過sysfs檔案系統的形式,将uevent_helper數組開放到使用者空間,供使用者空間程式修改通路,具體可參考​

    ​./kernel/ksysfs.c​

    ​中相應的代碼,這裡不再較長的描述。

四、sysfs

1. 前言

sysfs是一個基于RAM的檔案系統,它和kobject一起,可以将kernel的資料結構導出到使用者空間,以檔案目錄結構的形式,提供對這些資料結構(以及資料結構的屬性)的通路支援。sysfs是一種表示核心對象、對象屬性,以及對象關系的一種機制,一般核心對象、屬性、以及對象關系組織成樹狀形式。其中核心對象被映射為使用者态的目錄;對象屬性被映射為使用者态的檔案,檔案在目錄下;對象關系被映射成使用者空間的符号連結。

sysfs具備檔案系統的所有屬性,而本文主要側重其裝置模型的特性,是以不會涉及過多的檔案系統實作細節,而隻介紹sysfs在Linux裝置模型中的作用和使用方法。具體包括:

  • sysfs和kobject的關系
  • attribute的概念
  • sysfs的檔案系統操作接口

2. sysfs和kobject的關系

在前面有提到過,每一個kobject,都會對應sysfs中的一個目錄。是以在将kobject添加到kernel時,​

​create_dir​

​函數會調用sysfs檔案系統的建立目錄接口,建立和kobject對應的目錄,相關的代碼如下:

/* lib/kobject.c, line 47 */
static int create_dir(struct kobject *kobj)
{
    int error = 0;
    if (kobject_name(kobj)) {
        error = sysfs_create_dir(kobj);
        if (!error) {
            error = populate_dir(kobj);
            if (error)
                sysfs_remove_dir(kobj);
        }
    }
    return error;
}      
  • 這個函數接受唯一的一個參數,也就是要為其建立目錄的kobject的位址。它完成如下操作:
  1. 檢查傳遞進來kobject對象的name的有效性,若無效,則傳回0。
  2. 若有效,則調用sysfs_create_dir(kobj)來在sysfs中建立目錄。
/* fs/sysfs/dir.c, line 736 */
/*
 *  sysfs_create_dir - create a directory for an object.
 */
int sysfs_create_dir(struct kobject * kobj)
{
    enum kobj_ns_type type;
    struct sysfs_dirent *parent_sd, *sd;
    const void *ns = NULL;
    int error = 0;
    BUG_ON(!kobj);
    if (kobj->parent)
        parent_sd = kobj->parent->sd;
    else
        parent_sd = &sysfs_root;
    error = create_dir(kobj, parent_sd, kobject_name(kobj), &sd);
    if (!error)
        kobj->sd = sd;
    return error;
}      
  • 這個函數首先會找到要為其建立目錄的kobject的sysfs_dirent對象的父sysfs_dirent,通常是由父kobject的sd字段所指向。若父kobject為NULL,則設為sysfs根目錄的sysfs_dirent。
  • 然後調用create_dir(kobj, parent_sd, kobject_name(kobj) , &sd)來建立目錄。
  • 最後,若成功,設定kobject的sd字段指向為其新建立的sysfs_dirent,并傳回0。若失敗則傳回錯誤碼。

3. attribute

3.1 attribute的功能概述

在sysfs中,為什麼會有attribute的概念呢?其實它是對應kobject而言的,指的是kobject的屬性。我們知道,sysfs中的目錄描述了kobject,而kobject是特定資料類型變量(如struct device)的展現。是以kobject的屬性,就是這些變量的屬性。它可以是任何東西,名稱、一個内部變量、一個字元串等等。而attribute,在sysfs檔案系統中是以檔案的形式提供的,即:kobject的所有屬性,都在它對應的sysfs目錄下以檔案的形式呈現。這些檔案一般是可讀寫的,而kernel中定義了這些屬性的子產品,會根據使用者空間的讀寫操作,記錄和傳回這些attribute的值。

總結一下:所謂的attibute,就是核心空間和使用者空間進行資訊互動的一種方法。例如某個驅動定義了一個變量,卻希望使用者空間程式可以修改該變量,以控制driver的運作行為,那麼就可以将該變量以sysfs attribute的形式開放出來。

Linux核心中,attribute分為普通的attribute和二進制attribute,如下:

/* include/linux/sysfs.h, line 26 */
struct attribute
{
    const char *name;
    umode_t         mode;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    bool ignore_lockdep:1;
    struct lock_class_key   *key;
    struct lock_class_key   skey;
#endif
};
  
/* include/linux/sysfs.h, line 100 */
struct bin_attribute {
    struct attribute    attr;
    size_t  size;
    void    *private;
    ssize_t (*read)(struct file *, struct kobject *, struct bin_attribute *, char *, loff_t, size_t);
    ssize_t (*write)(struct file *,struct kobject *, struct bin_attribute *, char *, loff_t, size_t);
    int (*mmap)(struct file *, struct kobject *, struct bin_attribute *attr, struct vm_area_struct *vma);
};      

struct attribute為普通的attribute,使用該attribute生成的sysfs檔案,隻能用字元串的形式讀寫(後面會說為什麼)。而struct bin_attribute在struct attribute的基礎上,增加了read、write等函數,是以它所生成的sysfs檔案可以用任何方式讀寫。

說完基本概念,我們要問兩個問題:

kernel怎麼把attribute變成sysfs中的檔案呢?

使用者空間對sysfs的檔案進行的讀寫操作,怎麼傳遞給kernel呢?

下面來看看這個過程。

3.2 attibute檔案的建立

在linux核心中,attibute檔案的建立是由fs/sysfs/file.c中​

​sysfs_create_file​

​接口完成的,該接口的實作沒有什麼特殊之處,大多是檔案系統相關的操作,和裝置模型沒有太多的關系,這裡先略過不提。

3.3 attibute檔案的read和write

看到3.1章節struct attribute的原型時,也許我們會犯嘀咕,該結構很簡單啊,name表示檔案名稱,mode表示檔案模式,其它的字段都是核心用于debug kernel Lock的,那檔案操作的接口在哪裡呢?

不着急,我們去fs/sysfs目錄下看看sysfs相關的代碼邏輯。

所有的檔案系統,都會定義一個​

​struct file_operations​

​變量,用于描述本檔案系統的操作接口,sysfs也不例外:

/* fs/sysfs/file.c, line 472 */
const struct file_operations sysfs_file_operations = {
    .read       = sysfs_read_file,
    .write      = sysfs_write_file,
    .llseek     = generic_file_llseek,
    .open       = sysfs_open_file,
    .poll       = sysfs_poll,
};      

attribute檔案的read操作,會由VFS轉到sysfs_file_operations的read(也就是sysfs_read_file)接口上,讓我們大概看一下該接口的處理邏輯。

/* fs/sysfs/file.c, line 127 */
static ssize_t sysfs_read_file(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    struct sysfs_buffer *buffer = file->private_data;
    ssize_t retval = 0;

    mutex_lock(&buffer->mutex);
    if (buffer->needs_read_fill || *ppos == 0) {
        retval = fill_read_buffer(file->f_path.dentry, buffer);
        if (retval)
            goto out;
    }
    ...
}

/* fs/sysfs/file.c, line 67 */
static int fill_read_buffer(struct dentry * dentry, struct sysfs_buffer * buffer)
{           
    struct sysfs_dirent *attr_sd = dentry->d_fsdata;
    struct kobject *kobj = attr_sd->s_parent->s_dir.kobj;
    const struct sysfs_ops * ops = buffer->ops;
    ...        
    count = ops->show(kobj, attr_sd->s_attr.attr, buffer->page);
    ...
}      

read處理看着很簡單,sysfs_read_file從file指針中取一個私有指針,轉換為一個struct sysfs_buffer類型的指針,以此為參數(buffer),轉身就調用fill_read_buffer接口。而fill_read_buffer接口,直接從buffer指針中取出一個struct sysfs_ops指針,調用該指針的show函數,即完成了檔案的read操作。

那麼後續呢?當然是由ops->show接口接着處理咯。而具體怎麼處理,就是其它子產品(例如某個driver)的事了,sysfs不再關心(其實,Linux大多的核心代碼,都是隻提供架構和機制,具體的實作,也就是苦力,留給那些碼農吧!這就是設計的魅力)。

不過還沒完,這個struct sysfs_ops指針哪來的?好吧,我們再看看open(sysfs_open_file)接口吧。

/* fs/sysfs/file.c, line 326 */
 static int sysfs_open_file(struct inode *inode, struct file *file)
 {
    struct sysfs_dirent *attr_sd = file->f_path.dentry->d_fsdata;
    struct kobject *kobj = attr_sd->s_parent->s_dir.kobj;
    struct sysfs_buffer *buffer;
    const struct sysfs_ops *ops;
    int error = -EACCES;
  
    /* need attr_sd for attr and ops, its parent for kobj */
    if (!sysfs_get_active(attr_sd))
        return -ENODEV;

    /* every kobject with an attribute needs a ktype assigned */
    if (kobj->ktype && kobj->ktype->sysfs_ops)
        ops = kobj->ktype->sysfs_ops;
    else {
        WARN(1, KERN_ERR "missing sysfs attribute operations for kobject: %s\n", kobject_name(kobj));
        goto err_out;
    }

     ...

    buffer = kzalloc(sizeof(struct sysfs_buffer), GFP_KERNEL);
    if (!buffer)
        goto err_out;

    mutex_init(&buffer->mutex);
    buffer->needs_read_fill = 1;
    buffer->ops = ops;
    file->private_data = buffer;
    ...
}      

原來和ktype有關系。這個指針是從該attribute所從屬的kobject中拿的。再去看一下ktype的定義,裡面有一個struct sysfs_ops的指針。

注意:通過注釋“every kobject with an attribute needs a ktype assigned”以及其後代碼邏輯可知,如果從屬的kobject(就是attribute檔案所在的目錄)沒有ktype,或者沒有ktype->sysfs_ops指針,是不允許它注冊任何attribute的!

經過确認後,sysfs_open_file從ktype中取出struct sysfs_ops指針,并在随後的代碼邏輯中,配置設定一個struct sysfs_buffer類型的指針(buffer),并把struct sysfs_ops指針儲存在其中,随後把buffer指針交給file的private_data,随後read/write等接口便可以取出使用。這是核心中的常用方法!

順便看一下struct sysfs_ops吧,我想你已經能夠猜到了。

/* include/linux/sysfs.h, line 124 */
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 *);
};      

attribute檔案的write過程和read類似,這裡就不再多說。另外,上面隻分析了普通attribute的邏輯,而二進制類型的呢?也類似,去看看fs/sysfs/bin.c吧,這裡也不說了。

講到這裡,應該已經結束了,事實卻不是如此。上面read/write的資料流,隻到kobject(也就是目錄)級别哦,而真正需要操作的是attribute(檔案)啊!這中間一定還有一層轉換!确實,不過又交給其它子產品了。 下面我們通過一個例子,來說明如何轉換的。

4. sysfs在裝置模型中的應用總結

讓我們通過裝置模型class.c中有關sysfs的實作,來總結一下sysfs的應用方式。

首先,在class.c中,定義了class所需的ktype以及sysfs_ops類型的變量,如下:

/* drivers/base/class.c, line 86 */
static const struct sysfs_ops class_sysfs_ops = {
    .show      = class_attr_show,
    .store     = class_attr_store,
    .namespace = class_attr_namespace,
};  

static struct kobj_type class_ktype = {
    .sysfs_ops  = &class_sysfs_ops,
    .release    = class_release,
    .child_ns_type  = class_child_ns_type,
};      

由前面章節的描述可知,所有class_type的kobject下面的attribute檔案的讀寫操作,都會交給class_attr_show和class_attr_store兩個接口處理。以class_attr_show為例:

/* drivers/base/class.c, line 24 */
#define to_class_attr(_attr) container_of(_attr, struct class_attribute, attr)
 
static ssize_t class_attr_show(struct kobject *kobj, struct attribute *attr, char *buf)
{   
    struct class_attribute *class_attr = to_class_attr(attr);
    struct subsys_private *cp = to_subsys_private(kobj);
    ssize_t ret = -EIO;

    if (class_attr->show)
        ret = class_attr->show(cp->class, class_attr, buf);
    return ret;
}      
/* include/linux/device.h, line 399 */
struct class_attribute {
    struct attribute attr;
    ssize_t (*show)(struct class *class, struct class_attribute *attr,char *buf);
    ssize_t (*store)(struct class *class, struct class_attribute *attr, const char *buf, size_t count);
    const void *(*namespace)(struct class *class, const struct class_attribute *attr); 
};      

繼續閱讀