天天看點

Linux裝置管理(四)_從sysfs回到ktype【轉】

sysfs是一個基于ramfs的檔案系統,在2.6核心開始引入,用來導出核心對象(kernel object)的資料、屬性到使用者空間。與同樣用于檢視核心資料的proc不同,sysfs隻關心具有層次結構的裝置資訊,比如系統中的總線,驅動以及已經加載的子產品等,而諸如PID等資訊還是使用proc來管理。本質上,sysfs檔案的層次結構就是基于核心中kset與kobject邏輯結構來組織的。從驅動開發的角度,/sysfs為我們提供了除了裝置檔案/dev和/proc之外的另外一種通過使用者空間通路核心資料的方式。想要使用sysfs,編譯核心的時候需要定義CONFIG_SYSFS,可以通過mount -t sysfs sysfs /sys指令來挂載sysfs到"/sys"目錄。本文以ubuntu15.04(3.19)為例分析。

sysfs目錄結構

sysfs的布局展現核心的資料結構,頂層的目錄有

$ls /sys/
block/  bus/  class/  dev/  devices/  firmware/  fs/  hypervisor/  kernel/  module/  power/      

每一個目錄都對應核心中的一個kset,每一個kset還會包含一些kobject或其他kset。下面針對常用目錄做一個簡單的介紹

/sys/block/

塊裝置的存放目錄,這是一個過時的接口,按照sysfs的設計理念,所有的裝置都存放在"sys/devices/"同時在"sys/bus/"或(和)"sys/class/"存放相應的符号連結,是以現在這個目錄隻是為了提高相容性的設計,裡面的檔案已經被全部替換成了符号連結,隻有在編譯核心的時候勾選CONFIG_SYSFS_DEPRECATED才會有這個目錄,

sys $ll block/
total 0
lrwxrwxrwx  1 root root 0 12月 20 11:29 dm-0 -> ../devices/virtual/block/dm-0/
lrwxrwxrwx  1 root root 0 12月 20 11:29 dm-1 -> ../devices/virtual/block/dm-1/
...
      

/sys/bus/

bus包含了系統中所有的總線,比如我的系統目前提供的總線有:

sys $ls bus/
acpi/   container/  i2c/    media/     mipi-dsi/  pci/  pnp/    sdio/   usb/    platform/     scsi/     spi/   ...      

每一種總線通常還有兩個子目錄:device和driver,這兩個字目錄分别對應核心中的兩個kset,同時bus本身也對應一個kset,也有自己的kobject和以及(可能)有相應的ktype。我們可以檢視相應的kset屬性。

sys $ls bus/platform/
devices/  drivers/  drivers_autoprobe  drivers_probe  uevent
sys $cat bus/platform/drivers_autoprobe 
1      

我們可以扒一下3.19的源碼,找到這個屬性

//include/linux/platform_device.h
 22 struct platform_device {
            ...
 26         struct device   dev;
            ...
 38 };      
//include/linux/device.h
 731 struct device {
             ...
 744         struct bus_type *bus;           /* type of bus device is on */
             ...
 800 };

 104 struct bus_type {
             ...
 129         struct subsys_private *p;
             ...
 131 };
      
//drivers/base/base.h
 28 struct subsys_private {
 29         struct kset subsys;
 30         struct kset *devices_kset;
            ...
 38         unsigned int drivers_autoprobe:1;       #Bingo!!!
            ...
 43 };        

同時,根據kset的組織形式,平台總線的裝置kset連結了挂接在平台總線上的所有裝置,是以"platform/devices"下應該可以檢視到,要注意的事,為了使一個裝置在sysfs中隻有一個執行個體,很多目錄都是使用符号連結的形式,下面顯示的結果也驗證了這種設計。

sys $ll bus/platform/devices/
lrwxrwxrwx 1 root root 0 12月 19 08:17 ACPI0003:00 -> ../../../devices/pci0000:00/0000:00:14.3/PNP0C09:00/ACPI0003:00/  ...

sys $ll bus/platform/drivers/thinkpad_acpi/
lrwxrwxrwx  1 root root    0 12月 20 20:19 thinkpad_acpi -> ../../../../devices/platform/thinkpad_acpi/
--w-------  1 root root 4096 12月 20 20:18 uevent
--w-------  1 root root 4096 12月 20 20:19 unbind
-r--r--r--  1 root root 4096 12月 20 20:19 version
...

sys $cat bus/platform/drivers/thinkpad_acpi/version 
ThinkPad ACPI Extras v0.25      

/sys/class/

按照裝置功能對系統裝置進行分類的結果放在這個目錄,如系統所有輸入裝置都會出現在 "/sys/class/input"之下。和sys/bus一樣,sys/class最終的檔案都是符号連結,這種裝置可以保證整個系統中每一個裝置都隻有一個執行個體。

sys $l class/
ata_device/   i2c-adapter/    net/     rtc/           spi_master/    gpio/      input/   ...

sys $l class/input/
event0@  event10@  event12@   mouse0@   ...      

/sys/dev/

按照裝置号對字元裝置和塊裝置進行分類的結果放在這個目錄,同樣,檔案依然是使用符号連結的形式連結到"sys/devices/"中的相應檔案

sys $ls dev/
block/  char/

sys $ls dev/char/
10:1@    10:236@  108:0@   1:3@    ...      

/sys/devices/

如前所述,所有的裝置檔案執行個體都在"sys/devices/"目錄下,

sys $ls devices/
amd_nb/  breakpoint/  cpu/  ibs_fetch/  ibs_op/  LNXSYSTM:00/  pci0000:00/  platform/  ...

sys $ls devices/platform/serial8250/
driver@  driver_override  modalias  power/  subsystem@  tty/  uevent

sys $cat devices/platform/serial8250/driver_override 
(null)      

"sys/class/","sys/bus/","sys/devices"是裝置開發中最重要的幾個目錄。他們之間的關系可以用下圖表示。

Linux裝置管理(四)_從sysfs回到ktype【轉】

/sys/fs

這裡按照設計是用于描述系統中所有檔案系統,包括檔案系統本身和按檔案系統分類存放的已挂載點,但目前隻有 fuse,gfs2 等少數檔案系統支援 sysfs 接口,一些傳統的虛拟檔案系統(VFS)層次控制參數仍然在 sysctl (/proc/sys/fs) 接口中中;

/sys/kernel

這裡是核心所有可調整參數的位置,目前隻有 uevent_helper, kexec_loaded, mm, 和新式的 slab 配置設定器等幾項較新的設計在使用它,其它核心可調整參數仍然位于 sysctl (/proc/sys/kernel) 接口中 ;

/sys/module

這裡有系統中所有子產品的資訊,不論這些子產品是以内聯(inlined)方式編譯到核心映像檔案(vmlinuz)中還是編譯為外部子產品(ko檔案),都可能會出現在 /sys/module 中:編譯為外部子產品(ko檔案)在加載後會出現對應的/sys/module/<module_name>/, 并且在這個目錄下會出現一些屬性檔案和屬性目錄來表示此外部子產品的一些資訊,如版本号、加載狀态、所提供的驅動程式等;編譯為内聯方式的子產品則隻在當它有非0屬性的子產品參數時會出現對應的 /sys/module/<module_name>, 這些子產品的可用參數會出現在 /sys/modules//parameters/<param_name> 中,如 /sys/module/printk/parameters/time 這個可讀寫參數控制着内聯子產品 printk 在列印核心消息時是否加上時間字首;所有内聯子產品的參數也可以由 "<module_name>.<param_name>="的形式寫在核心啟動參數上,如啟動核心時加上參數 "printk.time=1" 與 向"/sys/module/printk/parameters/time" 寫入1的效果相同;沒有非0屬性參數的内聯子產品不會出現于此。

/sys/power

這裡是系統中電源選項,這個目錄下有幾個屬性檔案可以用于控制整個機器的電源狀态,如可以向其中寫入控制指令讓機器關機、重新開機等。

/sys/slab

(對應 2.6.23 核心,在 2.6.24 以後移至/sys/kernel/slab) 從2.6.23 開始可以選擇 SLAB 記憶體配置設定器的實作,并且新的 SLUB(Unqueued Slab Allocator)被設定為預設值;如果編譯了此選項,在 /sys 下就會出現 /sys/slab ,裡面有每一個 kmem_cache 結構體的可調整參數。對應于舊的 SLAB 記憶體配置設定器下的/proc/slabinfo 動态調整接口, 新式的 /sys/kernel/slab/<slab_name> 接口中的各項資訊和可調整項顯得更為清晰。

sysfs與kobject、kset

對于每一個注冊到核心的kobject,都會在sysfs中建立一個目錄!!!一個目錄!!!一個目錄!!!,目錄名就是kobject.name,這個目錄會從屬于kobject.parent對應的目錄,我們就可以實作在sysfs中用樹狀結構來呈現核心中的kobject。最初的sysfs下頂層目錄下的目錄使用subsystem的結構,在某些書中還會見到這個概念,不過現在已經被kset替代了。在 kobject 下還有一些符号連結檔案,指向其它的 kobject,這些符号連結檔案用于組織上面所說的 device, driver, bus_type, class, module 之間的關系。我們再來看看kobject結構:

//include/linux/kobject.h
 63 struct kobject {      
 64         const char              *name;
 65         struct list_head        entry;
 66         struct kobject          *parent;
 67         struct kset             *kset;
 68         struct kobj_type        *ktype;
 69         struct kernfs_node      *sd;
 70         struct kref             kref;
            ...
 79 };      
//include/linux/kernfs.h
106 struct kernfs_node {
            ...
125         union {
126                 struct kernfs_elem_dir          dir;
127                 struct kernfs_elem_symlink      symlink;
128                 struct kernfs_elem_attr         attr;
129         };
            ...
137 };
      

這其中的symlink就組成了下面的符号連結,許許多多這樣的符号連結就構成了整個sysfs的符号連結體系

sys $ll devices/platform/serial8250/
lrwxrwxrwx  1 root root    0 12月 20 16:17 driver -> ../../../bus/platform/drivers/serial8250/
-rw-r--r--  1 root root 4096 12月 20 16:17 driver_override
-rw-r--r--  1 root root 4096 12月 20 16:17 uevent
...      

sysfs與ktype

在sysfs中,kobject的屬性(kobject.ktype.attribute)可以以普通檔案的形式導出,sysfs還提供了使用檔案I/O直接修改核心屬性的機制,這些屬性一般都是ASCII格式的文本檔案(ktype.attribute.name)或二進制檔案(通常隻用在sys/firmware中),為了提高效率,可以将具有同一類型的屬性放置在一個檔案中,這樣就可以使用數組進行批量修改,不要在一個檔案中使用混合類型,也不要使用多行資料,這些做法會大大降低代碼的可讀性,下面就是一個屬性的定義,可以看到,屬性中并沒有包含讀寫屬性的函數,但是從面向對象的思想看,核心提供了兩個用于讀寫attribute結構的函數。

//include/linux/sysfs.h
 29 struct attribute {
 30     const char      *name;
 31     umode_t         mode;
 32 #ifdef CONFIG_DEBUG_LOCK_ALLOC       
 33     bool            ignore_lockdep:1; 
 34     struct lock_class_key   *key;     
 35     struct lock_class_key   skey;  
 36 #endif
 37 };

int sysfs_create_file(struct kobject * kobj, const struct attribute * attr);
void sysfs_remove_file(struct kobject * kobj, const struct attribute * attr);      

由于一個ktype往往包含很多屬性(default_attr是一個二級指針),當使用者通過sysfs讀寫一個kobject的屬性的時候,會自動回調ktype中的sysfs_ops->show()和sysfops->remove(),是以一個典型的做法是,當我們建立了一個繼承自kobject的子類child後,同時還會建立兩個調用了sysfs_create_file()和sys_remove_file()的讀寫函數,并将它們注冊到struct sysfs_ops中。比如核心使用的struct device就将相應的方法和屬性都封裝在了一起。

//include/linux/device.h
 512 /* interface for exporting device attributes */
 513 struct device_attribute {      
 514         struct attribute        attr;
 515         ssize_t (*show)(struct device *dev, struct device_attribute *attr,
 516                         char *buf);
 517         ssize_t (*store)(struct device *dev, struct device_attribute *attr,
 518                          const char *buf, size_t count);
 519 };

 560 extern int device_create_file(struct device *device,const struct device_attribute *entry);
 562 extern void device_remove_file(struct device *dev,const struct device_attribute *attr);
      

此外,核心甚至還提供了輔助定義這個屬性的宏

//include/linux/device.h
 539 #define DEVICE_ATTR(_name, _mode, _show, _store) \ 
 540         struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

//include/linux/sysfs.h
 75 #define __ATTR(_name, _mode, _show, _store) {                           \         
 76         .attr = {.name = __stringify(_name),                            \
 77                  .mode = VERIFY_OCTAL_PERMISSIONS(_mode) },             \
 78         .show   = _show,                                                \
 79         .store  = _store,                                               \
 80 }
      

有了這個宏,我們就可以直接通過這個接口建立我們自己的對象

static DEVICE_ATTR(foo, S_IWUSR | S_IRUGO, show_foo, store_foo);      

我們可以追一下源碼,可以發現,我們使用的​​自動建立裝置檔案​​的device_create()就會調用device_create_file()并最終調用sysfs_create_file()

"drivers/base/core.c"

device_create()

   └── device_create_vargs()

            └── device_create_groups_vargs()

                        └── device_add()

                                    └── device_create_file()

                                                ├── "include/linux/sysfs.h"

                                                └── sysfs_create_file()

eg_0:

#define to_dev(obj) container_of(obj, struct device, kobj)
#define to_dev_attr(_attr) container_of(_attr, struct device_attribute, attr)

static ssize_t dev_attr_show(struct kobject *kobj, struct attribute *attr,
                             char *buf)
{
        struct device_attribute *dev_attr = to_dev_attr(attr);
        struct device *dev = to_dev(kobj);
        ssize_t ret = -EIO;

        if (dev_attr->show)
                ret = dev_attr->show(dev, dev_attr, buf);
        if (ret >= (ssize_t)PAGE_SIZE) {
                print_symbol("dev_attr_show: %s returned bad count\n",
                                (unsigned long)dev_attr->show);
        }
        return ret;
}      

讀寫attribute

當一個子系統定義了一個新的屬性,它必須執行一組針對的sysfs操作以便對實作對屬性的讀寫,這些讀寫操作通過回調ktype.sysfs_ops.show()和store()。

//include/linux/sysfs.h
184 struct sysfs_ops {  
185         ssize_t (*show)(struct kobject *, struct attribute *, char *);
186         ssize_t (*store)(struct kobject *, struct attribute *, const char *, size_t);
187 };
      

當進行讀寫的時候,sysfs會配置設定一個PAGE_SIZE大小的buf并把它作為參數傳入這兩個函數,同時,對于每一次對屬性的讀寫操作,sysfs都會調用這兩個函數,是以,調用read系統調用的時候,show()方法應該填滿整個buf,注意一個屬性應該是一個或一組相似的值,是以這種機制并不會浪費很多系統資源。這種機制允許使用者讀取一部分内容并且可以任意的移動檔案位置指針,如果使用者空間将檔案指針置為0或以0為偏移量調用了pread(),show()會被重新調用并且再填滿一個buf。類似地,調用write()系統調用的時候,sysfs希望第一次傳入的buf是被填滿的,sysfs會在傳入的資料最後自動加NUL,這可以讓諸如sysfs_strqe()一類的函數用起來更安全。當對sysfs執行寫操作時,使用者空間應該首先讀取整個檔案的内容,按自己的需求改變其中的一部分并回寫,屬性讀寫操作應該使用同一個buf。

tips:

  1. 通過read()/write()傳遞資料不同,這裡的show()/store()裡的buf已經是核心空間的了,不需要進行copy_to_user() etc
  2. 寫操作會導緻show方法重新執行而忽視目前檔案位置指針的位置
  3. buf是PAGE_SIZE大小
  4. show()方法傳回列印到buf的實際byte數,這個就是scnprintf()的傳回值
  5. 在進行格式化列印到使用者空間的時候,show必須用scnprintf()除非你能保證棧不會溢出
  6. stor應該傳回buf中使用的資料的byte數目
  7. show或store應該設定合适的傳回值確定安全

eg_1

static ssize_t show_name(struct device *dev, struct device_attribute *attr,
                         char *buf)
{
    return scnprintf(buf, PAGE_SIZE, "%s\n", dev->name);
}

static ssize_t store_name(struct device *dev, struct device_attribute *attr,
                          const char *buf, size_t count)
{
        snprintf(dev->name, sizeof(dev->name), "%.*s",
                 (int)min(count, sizeof(dev->name) - 1), buf);
    return count;
}

static DEVICE_ATTR(name, S_IRUGO, show_name, store_name);      

核心已實作接口

核心中已經使用sysfs實作了很多的讀寫函數,下面是幾個典型的

裝置

/* devices */
/* structure */
//include/linux/device.h)
 512 /* interface for exporting device attributes */
 513 struct device_attribute {
 514         struct attribute        attr;
 515         ssize_t (*show)(struct device *dev, struct device_attribute *attr,
 516                         char *buf);
 517         ssize_t (*store)(struct device *dev, struct device_attribute *attr,
 518                          const char *buf, size_t count);
 519 };

/* Declaring */
 539 #define DEVICE_ATTR(_name, _mode, _show, _store) \  
 540         struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

/* Creation/Removal */
 560 extern int device_create_file(struct device *device,const struct device_attribute *entry);
 562 extern void device_remove_file(struct device *dev,const struct device_attribute *attr);      

總線驅動

/* bus drivers */
/* Structure */
//include/linux/device.h
  44 struct bus_attribute {  
  45         struct attribute        attr;
  46         ssize_t (*show)(struct bus_type *bus, char *buf);
  47         ssize_t (*store)(struct bus_type *bus, const char *buf, size_t count);
  48 };

/* Declaring */
  50 #define BUS_ATTR(_name, _mode, _show, _store)   \    
  51         struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)

/* Creation/Removal */
  57 extern int __must_check bus_create_file(struct bus_type *,struct bus_attribute *);
  59 extern void bus_remove_file(struct bus_type *, struct bus_attribute *);      

裝置驅動

/* device drivers */
/* Structure */
//include/linux/device.h

 265 struct driver_attribute {    
 266         struct attribute attr;
 267         ssize_t (*show)(struct device_driver *driver, char *buf);
 268         ssize_t (*store)(struct device_driver *driver, const char *buf,
 269                          size_t count);
 270 };

/* Declaring */
 272 #define DRIVER_ATTR(_name, _mode, _show, _store) \  
 273         struct driver_attribute driver_attr_##_name = __ATTR(_name, _mode, _show, _store)

/* Creation/Removal */
 281 extern int __must_check driver_create_file(struct device_driver *driver,   
 282                                         const struct driver_attribute *attr);
 283 extern void driver_remove_file(struct device_driver *driver,
 284                                const struct driver_attribute *attr);
      

彩蛋

Linux中幾乎所有的"裝置"都是"device"的子類,無論是平台裝置還是i2c裝置還是網絡裝置,但唯獨字元裝置不是,從"​Linux字元裝置驅動架構"一文中我們可以看出cdev并不是繼承自device,從"​​Linux裝置管理(二)_從cdev_add說起​"一文中我們可以看出注冊一個cdev對象到核心其實隻是将它放到cdev_map中,直到"​​Linux裝置管理(四)_從sysfs回到ktype​"一文中對device_create的分析才知道此時才建立device結構并将kobj挂接到相應的連結清單,,是以,基于曆史原因,當下cdev更合适的一種了解是一種接口(使用mknod時可以當作裝置),而不是而一個具體的裝置,和platform_device,i2c_device有着本質的差別

繼續閱讀