使用 /sys 文件系统访问 Linux 内核

使用 /sys 文件系统访问 Linux 内核

sysfs 虚拟文件系统提供了一种比 proc 更为理想的访问内核数据的途径php

 

sysfs 与 /sys

sysfs 文件系统老是被挂载在 /sys 挂载点上。虽然在较早期的2.6内核系统上并无规定 sysfs 的标准挂载位置,能够把 sysfs 挂载在任何位置,但较近的2.6内核修正了这一规则,要求 sysfs 老是挂载在 /sys 目录上;针对之前的 sysfs 挂载位置不固定或没有标准被挂载,有些程序从 /proc/mounts 中解析出 sysfs 是否被挂载以及具体的挂载点,这个步骤如今已经不须要了。请参考附录给出的 sysfs-rules.txt 文件连接。html

sysfs 与 proc

sysfs 与 proc 相比有不少优势,最重要的莫过于设计上的清晰。一个 proc 虚拟文件可能有内部格式,如 /proc/scsi/scsi ,它是可读可写的,(其文件权限被错误地标记为了 0444 !,这是内核的一个BUG),而且读写格式不同,表明不一样的操做,应用程序中读到了这个文件的内容通常还须要进行字符串解析,而在写入时须要先用字符串格式化按指定的格式写入字符串进行操做;相比而言, sysfs 的设计原则是一个属性文件只作一件事情, sysfs 属性文件通常只有一个值,直接读取或写入。整个 /proc/scsi 目录在2.6内核中已被标记为过期(LEGACY),它的功能已经被相应的 /sys 属性文件所彻底取代。新设计的内核机制应该尽可能使用 sysfs 机制,而将 proc 保留给纯净的“进程文件系统”。node

初识 /sys

清单 1. 与 /sys 文件系统的一次交互(视内核版本号和外接设备的不一样,在您的系统上执行这些命令的结果可能与此有所不一样)
 
$ ls -F /sys
block/  bus/  class/  dev/  devices/  firmware/  fs/  kernel/  module/  power/
$ ls -F /sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/
broken_parity_status  enable         modalias  resource0     rom               uevent
class                 irq            msi_bus   resource0_wc  subsystem@        vendor
config                local_cpulist  power/    resource1     subsystem_device
device                local_cpus     resource  resource2     subsystem_vendor

这是在 Fedora 10 的 2.6.27.5-117.fc10.i686 的内核上,能够看到在 /sys 目录下有 block, bus, class, dev, devices, firmware, fs, kernel, module, power 这些子目录,本文将分别介绍这些目录存在的含义。linux

第二个 ls 命令展现了在一个 pci 设备目录下的文件, "ls" 命令的 "-F" 命令为所列出的每一个文件使用后缀来显示文件的类型,后缀 "/" 表示列出的是目录,后缀 "@" 表示列出的是符号连接文件。能够看到第二个目录下包含有普通文件 (regular file) 和符号连接文件 (symbolic link file) ,本文也将以这个具体的设备为例说明其中每个普通文件的用途。git

/sys 文件系统下的目录结构

/sys 下的目录结构是通过精心设计的:在 /sys/devices 下是全部设备的真实对象,包括如视频卡和以太网卡等真实的设备,也包括 ACPI 等不那么显而易见的真实设备、还有 tty, bonding 等纯粹虚拟的设备;在其它目录如 class, bus 等中则在分类的目录中含有大量对 devices 中真实对象引用的符号连接文件; 清单 1 中在 /sys 根目录下顶层目录的意义以下:编程

表 1. /sys 下的目录结构

接下来对 /sys/devices/ 下的目录结构做进一步探讨:bash

清单 2. 查看 /sys/devices/ 的目录结构
 
$ ls -F /sys/devices/
isa/  LNXSYSTM:00/  pci0000:00/  platform/  pnp0/  pnp1/  system/  virtual/

能够看到,在 /sys/devices/ 目录下是按照设备的基本总线类型分类的目录,再进入进去查看其中的 PCI 类型的设备:服务器

清单 3. 查看 /sys/devices/pci0000:00/ 的目录结构
 
$ ls -F /sys/devices/pci0000:00/
0000:00:00.0/  0000:00:02.5/  0000:00:03.1/  0000:00:0e.0/   power/
0000:00:01.0/  0000:00:02.7/  0000:00:03.2/  firmware_node@  uevent
0000:00:02.0/  0000:00:03.0/  0000:00:03.3/  pci_bus/

在 /sys/devices/pci0000:00/ 目录下是按照 PCI 总线接入的设备号分类存放的目录,再查看其中一个,数据结构

清单 4. 查看 /sys/devices/pci0000:00/ 的目录结构
 
$ ls -F /sys/devices/pci0000:00/0000:00:01.0/
0000:01:00.0/         device         local_cpus  power/            subsystem_vendor
broken_parity_status  enable         modalias    resource          uevent
class                 irq            msi_bus     subsystem@        vendor
config                local_cpulist  pci_bus/    subsystem_device

能够看到,其中有一个目录 0000:01:00.0/, 其它都是属性文件和属性组,而若是对 0000:01:00.0/ 子目录中进行再列表查看则会获得 清单 1 的目录结构。app

继续以上过程能够了解整个目录树的结构,这里把它整理成 图 1. sysfs 目录层次图

图 1. sysfs 目录层次图

sysfs目录层次图

其中涉及到 ksets, kobjects, attrs 等不少术语,这就不得不提到 Linux 统一设备模型。

Linux 统一设备模型

在 Linux 2.5 内核的开发过程当中,人们设计了一套新的设备模型,目的是为了对计算机上的全部设备进行统一地表示和操做,包括设备自己和设备之间的链接关系。这个模型是在分析了 PCI 和 USB 的总线驱动过程当中获得的,这两个总线类型能表明当前系统中的大多数设备类型,它们都有完善的热挺拔机制和电源管理的支持,也都有级连机制的支持,以桥接的 PCI/USB 总线控制器的方式能够支持更多的 PCI/USB 设备。为了给全部设备添加统一的电源管理的支持,而不是让每一个设备中去独立实现电源管理的支持,人们考虑的是如何尽量地重用代码;并且在有层次模型的 PCI/USB 总线中,必须以合理形式展现出这个层次关系,这也是电源管理等所要求的必须有层次结构。

如在一个典型的 PC 系统中,中央处理器(CPU)能直接控制的是 PCI 总线设备,而 USB 总线设备是以一个 PCI 设备(PCI-USB桥)的形式接入在 PCI 总线设备上,外部 USB 设备再接入在 USB 总线设备上;当计算机执行挂起(suspend)操做时, Linux 内核应该以 “外部USB设备->USB总线设备->PCI总线设备” 的顺序通知每个设备将电源挂起;执行恢复(resume)时则以相反的顺序通知;反之若是不按此顺序则将有设备得不到正确的电源状态变迁的通知,将没法正常工做。

sysfs 是在这个 Linux 统一设备模型的开发过程当中的一项副产品(见 参考资料 中 Greg K. Hartman 写做的 LinuxJournal 文章)。为了将这些有层次结构的设备以用户程序可见的方式表达出来,人们很天然想到了利用文件系统的目录树结构(这是以 UNIX 方式思考问题的基础,一切都是文件!)在这个模型中,有几种基本类型,它们的对应关系见 表 2. Linux 统一设备模型的基本结构 :

表 2. Linux 统一设备模型的基本结构

从内核在实现它们时所使用的数据结构来讲, Linux 统一设备模型又是以两种基本数据结构进行树型和链表型结构组织的:

  • kobject: 在 Linux 设备模型中最基本的对象,它的功能是提供引用计数和维持父子(parent)结构、平级(sibling)目录关系,上面的 device, device_driver 等各对象都是以 kobject 基础功能之上实现的;
     
    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;
             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;
    };

    其中 struct kref 内含一个 atomic_t 类型用于引用计数, parent 是单个指向父节点的指针, entry 用于父 kset 以链表头结构将 kobject 结构维护成双向链表;
  • kset: 它用来对同类型对象提供一个包装集合,在内核数据结构上它也是由内嵌一个 kboject 实现,于是它同时也是一个 kobject (面向对象 OOP 概念中的继承关系) ,具备 kobject 的所有功能;
    struct kset {
             struct list_head list;
             spinlock_t list_lock;
             struct kobject kobj;
             struct kset_uevent_ops *uevent_ops;
    };

    其中的 struct list_head list 用于将集合中的 kobject 按 struct list_head entry 维护成双向链表;

涉及到文件系统实现来讲, sysfs 是一种基于 ramfs 实现的内存文件系统,与其它一样以 ramfs 实现的内存文件系统(configfs,debugfs,tmpfs,...)相似, sysfs 也是直接以 VFS 中的 struct inode 和 struct dentry 等 VFS 层次的结构体直接实现文件系统中的各类对象;同时在每一个文件系统的私有数据 (如 dentry->d_fsdata 等位置) 上,使用了称为 struct sysfs_dirent 的结构用于表示 /sys 中的每个目录项。

 
struct sysfs_dirent {
         atomic_t                s_count;
         atomic_t                s_active;
         struct sysfs_dirent     *s_parent;
         struct sysfs_dirent     *s_sibling;
         const char              *s_name;
 
         union {
                 struct sysfs_elem_dir           s_dir;
                 struct sysfs_elem_symlink       s_symlink;
                 struct sysfs_elem_attr          s_attr;
                 struct sysfs_elem_bin_attr      s_bin_attr;
         };
 
         unsigned int            s_flags;
         ino_t                   s_ino;
         umode_t                 s_mode;
         struct iattr            *s_iattr;
};

在上面的 kobject 对象中能够看到有向 sysfs_dirent 的指针,所以在sysfs中是用同一种 struct sysfs_dirent 来统一设备模型中的 kset/kobject/attr/attr_group.

具体在数据结构成员上, sysfs_dirent 上有一个 union 共用体包含四种不一样的结构,分别是目录、符号连接文件、属性文件、二进制属性文件;其中目录类型能够对应 kobject,在相应的 s_dir 中也有对 kobject 的指针,所以在内核数据结构, kobject 与 sysfs_dirent 是互相引用的;

有了这些概念,再来回头看 图 1. sysfs 目录层次图 所表达的 /sys 目录结构就是很是清晰明了:

  • 在 /sys 根目录之下的都是 kset,它们组织了 /sys 的顶层目录视图;
  • 在部分 kset 下有二级或更深层次的 kset;
  • 每一个 kset 目录下再包含着一个或多个 kobject,这表示一个集合所包含的 kobject 结构体;
  • 在 kobject 下有属性(attrs)文件和属性组(attr_group),属性组就是组织属性的一个目录,它们一块儿向用户层提供了表示和操做这个 kobject 的属性特征的接口;
  • 在 kobject 下还有一些符号连接文件,指向其它的 kobject,这些符号连接文件用于组织上面所说的 device, driver, bus_type, class, module 之间的关系;
  • 不一样类型如设备类型的、设备驱动类型的 kobject 都有不一样的属性,不一样驱动程序支持的 sysfs 接口也有不一样的属性文件;而相同类型的设备上有不少相同的属性文件;

注意,此表内容是按照最新开发中的 2.6.28 内核的更新组织的,在附录资源如 LDD3 等位置中有提到 sysfs 中曾有一种管理对象称为 subsys (子系统对象),在最新的内核中通过重构认为它是不须要的,它的功能彻底能够由 kset 代替,也就是说 sysfs 中只须要一种管理结构是 kset,一种表明具体对象的结构是 kobject,在 kobject 下再用属性文件表示这个对象所具备的属性;

常见 sysfs 属性的功能

使用 sysfs 的关键就是掌握这些 sysfs 属性的用法,下面以一些常见的 sysfs 属性来展现它的用法;

使用设备(PCI)的 sysfs 属性文件

以一份桌面系统上的视频卡为例,列举它对应的 kobject 上的属性文件的对应用途;

通常来讲,在 Linux 桌面上都有视频卡以支持 Xorg 软件包做为 XWindow 服务器来运行,所以先找到 Xorg 的进程号,查看这个进程所使用的全部文件(注意查看这个进程属性须要 root 用户权限);

# ps xfa |grep Xorg
  2001 tty1     Ss+    2:24      \_ /usr/bin/Xorg :0 -nr -verbose -auth \
/var/run/gdm/auth-for-gdm-NPrkZK/database -nolisten tcp vt1
# lsof -nP -p 2001
Xorg    2001 root  mem    REG        8,3    617732     231033 \
/usr/lib/xorg/modules/drivers/sis_drv.so
[...]
Xorg    2001 root  mem    REG        0,0 134217728       5529 \
/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/resource0
Xorg    2001 root  mem    REG        0,0    131072       5531 \
/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/resource1
[...]
Xorg    2001 root    7u   REG        0,0       256       5504 \
/sys/devices/pci0000:00/0000:00:00.0/config
Xorg    2001 root    8u  unix 0xdbe66000       0t0       8756 socket
Xorg    2001 root    9u   REG        0,0       256       5528 \
/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/config
 

注意到此 Xorg 服务器是之内存映射 (mem) 的形式打开了 "/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/resource0" 和 "/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/resource1" ,同时以文件读写形式 (7u,9u) 打开了 "/sys/devices/pci0000:00/0000:00:00.0/config" 和 "/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/config"

事实上, PCI 设备对应的 kobject 目录下的 config 正是表明PCI设备的“配置空间”,对于普通 PCI (非PCI-E)设备而言,其配置空间大小通常是 256字节,这个空间可使用十六进制工具 dump 出来,以下。(有关 PCI 设备自己的三种地址空间,请参考附录 LDD3)

# hexdump -C /sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/config
00000000  39 10 30 63 03 00 30 02  00 00 00 03 00 00 00 80  |9.0c..0.........|
00000010  08 00 00 d8 00 00 00 e1  01 d0 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 19 10 30 1b  |..............0.|
00000030  00 00 00 00 40 00 00 00  00 00 00 00 00 00 00 00  |....@...........|
00000040  01 50 02 06 00 00 00 00  00 00 00 00 00 00 00 00  |.P..............|
00000050  02 00 30 00 0b 02 00 ff  00 00 00 00 00 00 00 00  |..0.............|
00000060  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000100
 

这个空间正好是 256字节大小,熟悉 PCI 的人们还能够知道,从 PCI 配置空间能够读到有关此 PCI 设备的不少有用信息,如厂商代码,设备代码,IRQ 号码等;前四个字节 0x39 0x10 0x30 0x63 就是按小端(little endian)存放的2个短整数,所以其 PCI 厂商号码和 PCI 设备号码分别是 0x1039 和 0x6330

# lspci -v -d 1039:6330
01:00.0 VGA compatible controller: Silicon Integrated Systems [SiS] 661/741/760 PCI/AGP \
or 662/761Gx PCIE VGA Display Adapter (prog-if 00 [VGA controller])
     Subsystem: Elitegroup Computer Systems Device 1b30
     Flags: 66MHz, medium devsel
     BIST result: 00
     Memory at d8000000 (32-bit, prefetchable) [size=128M]
     Memory at e1000000 (32-bit, non-prefetchable) [size=128K]
     I/O ports at d000 [size=128]
     Capabilities: [40] Power Management version 2
     Capabilities: [50] AGP version 3.0
 

在 PCI 设备上除了有 config 是配置空间对用户的接口之外,还有 resource{0,1,2,...} 是资源空间,对应着 PCI 设备的可映射内存空间;此外 PCI 设备还提供了不少接口,所有列表以下:

# ls -lU /sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/
总计 0
-rw-r--r-- 1 root root      4096 12-09 00:28 uevent
-r--r--r-- 1 root root      4096 12-09 00:27 resource
-r--r--r-- 1 root root      4096 12-09 00:27 vendor
-r--r--r-- 1 root root      4096 12-09 00:27 device
-r--r--r-- 1 root root      4096 12-09 00:28 subsystem_vendor
-r--r--r-- 1 root root      4096 12-09 00:28 subsystem_device
-r--r--r-- 1 root root      4096 12-09 00:27 class
-r--r--r-- 1 root root      4096 12-09 00:27 irq
-r--r--r-- 1 root root      4096 12-09 00:28 local_cpus
-r--r--r-- 1 root root      4096 12-09 00:28 local_cpulist
-r--r--r-- 1 root root      4096 12-09 00:28 modalias
-rw------- 1 root root      4096 12-09 00:28 enable
-rw-r--r-- 1 root root      4096 12-09 00:28 broken_parity_status
-rw-r--r-- 1 root root      4096 12-09 00:28 msi_bus
lrwxrwxrwx 1 root root         0 12-09 00:28 subsystem -> ../../../../bus/pci
drwxr-xr-x 2 root root         0 12-09 00:28 power
-rw-r--r-- 1 root root       256 12-08 23:03 config
-rw------- 1 root root 134217728 12-08 23:03 resource0
-rw------- 1 root root 134217728 12-09 00:28 resource0_wc
-rw------- 1 root root    131072 12-08 23:03 resource1
-rw------- 1 root root       128 12-09 00:28 resource2
-r-------- 1 root root         0 12-09 00:28 rom
 

能够看到不少其它属性文件,这些属性文件的权限位也都是正确的,有 w 权限位的才是能够写入。其中大小为 4096字节的属性通常是纯文本描述的属性,能够直接 cat 读出和用 echo 字符串的方法写入;其它非 4096字节大小的通常是二进制属性,相似于上面的 config 属性文件;关于纯文本属性和二进制属性,在下文 编程实践:添加sysfs支持 一节会进一步说明。

  • 从 vendor, device, subsystem_vendor, subsystem_device, class, resource 这些只读属性上分别能够读到此 PCI 设备的厂商号、设备号、子系统厂商号、子系统设备号、PCI类别、资源表等,这些都是相应 PCI 设备的属性,其实就是直接从 config 二进制文件读出来,按照配置空间的格式读出这些号码;
  • 使用 enable 这个可写属性能够禁用或启用这个 PCI 设备,设备的过程很直观,写入1表明启用,写入0则表明禁用;
  • subsystem 和 driver 符号连接文件分别指向对应的 sysfs 位置;(这里缺乏 driver 符号连接说明这个设备当前未使用内核级的驱动程序)
  • resource0, resource0_wc, resource1, resource2 等是从"PCI 配置空间"解析出来的资源定义段落分别生成的,它们是 PCI 总线驱动在 PCI 设备初始化阶段加上去的,都是二进制属性,但没有实现读写接口,只支持 mmap 内存映射接口,尝试进行读写会提示 IO 错误,其中 _wc 后缀表示 "合并式写入(write combined)" ,它们用于做应用程序的内存映射,就能够访问对应的 PCI 设备上相应的内存资源段落;

有了 PCI 核心对 sysfs 的完善支持,每一个设备甚至不用单独的驱动程序,如这里的 "0000:01:00.0" 不须要一个内核级的驱动程序,有了 PCI 核心对该设备的配置空间发现机制,能够自动发现它的各个不一样段落的资源属性,在 Xorg 应用程序中能够直接以 "/usr/lib/xorg/modules/drivers/sis_drv.so" 这个用户空间的驱动程序对其进行映射,就能够直接操做此视频卡了;

有了这一个 PCI 设备的示例能够知道,有了一个 PCI 设备的 /sys/devices/ 设备对象,去访问它的各项属性和设置属性都很是简单。

使用 uevent

在 sysfs 下的不少 kobject 下都有 uevent 属性,它主要用于内核与 udev (自动设备发现程序)之间的一个通讯接口;从 udev 自己与内核的通讯接口 netlink 协议套接字来讲,它并不须要知道设备的 uevent 属性文件,但多了 uevent 这样一个接口,可用于 udevmonitor 经过内核向 udevd (udev 后台程序)发送消息,也可用于检查设备自己所支持的 netlink 消息上的环境变量,这个特性通常用于开发人员调试 udev 规则文件, udevtrigger 这个调试工具自己就是以写各设备的 uevent 属性文件实现的。

这些 uevent 属性文件通常都是可写的,其中 /sys/devices/ 树下的不少 uevent 属性在较新内核下还支持可读:

# find /sys/ -type f -name uevent -ls
     11    0 -rw-r--r--   1 root     root         4096 12月 12 21:10 \
/sys/devices/platform/uevent
   1471    0 -rw-r--r--   1 root     root         4096 12月 12 21:10 \
/sys/devices/platform/pcspkr/uevent
   3075    0 -rw-r--r--   1 root     root         4096 12月 12 21:10 \
/sys/devices/platform/vesafb.0/uevent
   3915    0 -rw-r--r--   1 root     root         4096 12月 12 21:10 \
/sys/devices/platform/serial8250/uevent
   3941    0 -rw-r--r--   1 root     root         4096 12月 12 21:10 \
/sys/devices/platform/serial8250/tty/ttyS2/uevent
   3950    0 -rw-r--r--   1 root     root         4096 12月 12 21:10 \
/sys/devices/platform/serial8250/tty/ttyS3/uevent
   5204    0 -rw-r--r--   1 root     root         4096 12月 12 21:10 \
/sys/devices/platform/i8042/uevent
[...]
    912    0 -rw-r--r--   1 root     root         4096 12月 12 21:17 \
/sys/devices/pci0000:00/0000:00:02.5/uevent
[...]
 

上面截取的最后一个是 SCSI 硬盘控制器设备的 uevent 属性文件,这些 /devices/ 属性文件都支持写入,当前支持写入的参数有 "add","remove","change","move","online","offline"。如,写入 "add",这样能够向 udevd 发送一条 netlink 消息,让它再从新一遍相关的 udev 规则文件;这个功能对开发人员调试 udev 规则文件颇有用。

# echo add > /sys/devices/pci0000:00/0000:00:02.5/uevent
 

使用驱动(PCI)的 sysfs 属性文件, bind, unbind 和 new_id

在设备驱动 /sys/bus/*/driver/... 下能够看到不少驱动都有 bind, unbind, new_id 这三个属性,

# find /sys/bus/*/drivers/ -name bind -ls
 

每个设备驱动程序在程序内以某种方式注明了可用于哪些硬件,如全部的 PCI 驱动都使用 MODULE_DEVICE_TABLE 声明了所能驱动的 PCI 硬件的 PCI 设备号。但驱动程序不能预知将来,将来生产的新的硬件有可能兼容现有硬件的工做方式,就还可使用现有硬件驱动程序来工做。在 bind 和 unbind 发明之前,这种状况除了修改 PCI 设备驱动程序的 DEVICE_TABLE 段落,从新编译驱动程序,之外别无他法,在 2.6 内核上添加了 bind 和 unbind 以后能够在不从新编译的状况下对设备和驱动之间进行手工方式地绑定。

并且对于有些硬件设备能够有多份驱动可用,但任何具体时刻只能有一个驱动程序来驱动这个硬件,这时可使用 bind/unbind 来强制使用和不使用哪个驱动程序;(注意关于多种驱动程序的选择,更好的管理方法是使用 modprobe.conf 配置文件,须要重启才生效,而 bind/unbind 提供的是一种临时的无需重启当即生效的途径;)

使用它们能够强制绑定某个设备使用或强制不使用某个驱动程序,操做方法就是经过 bind 和 unbind 接口。

# find /sys/ -type f \( -name bind -or -name unbind -or -name new_id \) -ls
     69    0 -rw-r--r--   1 root     root         4096 12月 12 22:12 \
/sys/devices/virtual/vtconsole/vtcon0/bind
   3072    0 --w-------   1 root     root         4096 12月 12 22:15 \
/sys/bus/platform/drivers/vesafb/unbind
[...]
   6489    0 --w-------   1 root     root         4096 12月 12 22:09 \
/sys/bus/pci/drivers/8139too/unbind
   6490    0 --w-------   1 root     root         4096 12月 12 22:09 \
/sys/bus/pci/drivers/8139too/bind
   6491    0 --w-------   1 root     root         4096 12月 12 22:15 \
/sys/bus/pci/drivers/8139too/new_id
 

这个结果中特别提到了 8139too 这份驱动程序的这三个属性文件,

# find /sys/bus/pci/drivers/8139too/ -ls
   6435    0 drwxr-xr-x   2 root     root            0 12月 12 22:08 \
/sys/bus/pci/drivers/8139too/
   6436    0 lrwxrwxrwx   1 root     root            0 12月 12 22:08 \
/sys/bus/pci/drivers/8139too/0000:00:0e.0 -> ../../../../devices/pci0000:00/0000:00:0e.0
   6485    0 lrwxrwxrwx   1 root     root            0 12月 12 22:08 \
/sys/bus/pci/drivers/8139too/module -> ../../../../module/8139too
   6488    0 --w-------   1 root     root         4096 12月 12 22:08 \
/sys/bus/pci/drivers/8139too/uevent
   6489    0 --w-------   1 root     root         4096 12月 12 22:08 \
/sys/bus/pci/drivers/8139too/unbind
   6490    0 --w-------   1 root     root         4096 12月 12 22:08 \
/sys/bus/pci/drivers/8139too/bind
   6491    0 --w-------   1 root     root         4096 12月 12 22:08 \
/sys/bus/pci/drivers/8139too/new_id
# echo 0000:00:0e.0 > /sys/bus/pci/drivers/8139too/unbind
-bash: echo: write error: 没有那个设备
# ip addr
1: lo: < LOOPBACK ,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN
     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
     inet 127.0.0.1/8 scope host lo
2: eth0: < BROADCAST ,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state \
UNKNOWN qlen 1000
     link/ether 00:14:2a:d1:16:72 brd ff:ff:ff:ff:ff:ff
     inet 192.168.1.102/24 brd 192.168.1.255 scope global eth0
3: bond0: < BROADCAST ,MULTICAST,MASTER> mtu 1500 qdisc noop state DOWN
     link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
# echo -n 0000:00:0e.0 > /sys/bus/pci/drivers/8139too/unbind
# ip addr
1: lo: < LOOPBACK ,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN
     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
     inet 127.0.0.1/8 scope host lo
3: bond0: < BROADCAST ,MULTICAST,MASTER> mtu 1500 qdisc noop state DOWN
     link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
# echo -n 0000:00:0e.0 > /sys/bus/pci/drivers/8139too/bind
# ip addr
1: lo: < LOOPBACK ,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN
     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
     inet 127.0.0.1/8 scope host lo
3: bond0: < BROADCAST ,MULTICAST,MASTER> mtu 1500 qdisc noop state DOWN
     link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
4: eth0: < BROADCAST ,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
     link/ether 00:14:2a:d1:16:72 brd ff:ff:ff:ff:ff:ff
 

这一段操做过程演示了如何对 PCI 设备 "0000:00:0e.0" 强制取消绑定 "8139too" 驱动和强制绑定 "8139too" 驱动:

  • 对 unbind 属性写入总线号码(bus_id)便是强制取消绑定;
  • 对 bind 属性写入总线号码(bus_id)便是强制绑定;

注意,它要求的写入的是总线号码,对应于PCI设备的总线号码是按照 "domain(4位):bus(2位):slot(2位):function号(不限)" 的方式组织,是能够从其设备 kobject 节点上找到,而其它类型的总线有各自不一样的规则;

请特别注意: 在这一个例子中, "echo 0000:00:0e.0 > /sys/bus/pci/drivers/8139too/unbind" 这第一个写入命令以 "No such device" 为错误退出,然后续的 "echo -n" 命令则能够成功。这是由于内核在对总线号码进行匹配时过于严格了,一般的 "echo" 命令写入一个字符串会以一个换行符结束输出,内核所接收到的是带有这个换行符的 bus_id 字符串,将它与内核数据结构中的真正的 bus_id 字符串相比较,固然不能找到;所幸的是,这个问题在最新的 2.6.28 开发中的内核上已已经解决,它将这个比较函数改成一个特殊实现的字符串比较,自动忽略结尾处的换行符,在 2.6.28-rc6 内核上测试,不带"-n"参数的 echo 命令已经能够写入成功。

而 new_id 属性文件也能够以另外一种途径解决新的设备号问题:它是一个只写的驱动属性,可用于向其中写新的设备号。它支持写入 2至7个十六进制整形参数,分别表明 vendor, device, subvendor, subdevice, class, class_mask, driver_data 最少为 2个是由于一个 PCI设备主要以厂商号(vendor)和设备号(device)所惟一标定,其它 5个参数若是不输入则缺省值为 PCI_ANY_ID(0xffff)。

  5441    0 --w-------   1 root     root         4096 12月 14 18:15 \
/sys/bus/pci/drivers/8139too/new_id
 

从 8139too 驱动上能够看到它当前所静态支持的设备号码列表,其中包括当前系统中的设备 10ec:8139, 假设将来有一款 8140 设备也知足 8139 设备的硬件通信协议,因而可使用 8139too 驱动程序来驱动它,操做以下

# echo '10ec 8140' > /sys/bus/pci/drivers/8139too/new_id
 

这在不更新驱动程序的状况下调试设备颇有用处。

使用 scsi_host 的 scan 属性

在具备使用 SCSI 总线链接的主机上,与 PCI相似的是也采用四个号码做为一组来描述一个设备,其中位于最顶层的是 scsi_host。

咱们从设备类别 /class/为起点来探索:

# ls -lU /sys/class/scsi_host
总计 0
lrwxrwxrwx 1 root root 0 12-13 01:59 host0 -> \
../../devices/pci0000:00/0000:00:02.5/host0/scsi_host/host0
lrwxrwxrwx 1 root root 0 12-13 01:59 host1 -> \
../../devices/pci0000:00/0000:00:02.5/host1/scsi_host/host1
 

注意这是 2.6.27 内核的最新变化,在 /sys/class/ 下的都改成符号连接,真实的 kobject 都存在于 /sys/devices/ 中;咱们这里探索其中的 host0 这个 SCSI 控制器:

# readlink -f /sys/class/scsi_host/host0
/sys/devices/pci0000:00/0000:00:02.5/host0/scsi_host/host0
# ls -lU /sys/devices/pci0000:00/0000:00:02.5/host0/scsi_host/host0
总计 0
-rw-r--r-- 1 root root 4096 12-13 02:02 uevent
lrwxrwxrwx 1 root root    0 12-13 02:02 subsystem -> ../../../../../../class/scsi_host
lrwxrwxrwx 1 root root    0 12-13 02:02 device -> ../../../host0
-r--r--r-- 1 root root 4096 12-13 02:02 unique_id
-r--r--r-- 1 root root 4096 12-13 02:02 host_busy
-r--r--r-- 1 root root 4096 12-13 02:02 cmd_per_lun
-r--r--r-- 1 root root 4096 12-13 02:02 can_queue
-r--r--r-- 1 root root 4096 12-13 02:02 sg_tablesize
-r--r--r-- 1 root root 4096 12-13 02:02 unchecked_isa_dma
-r--r--r-- 1 root root 4096 12-13 02:02 proc_name
--w------- 1 root root 4096 12-13 02:02 scan
-rw-r--r-- 1 root root 4096 12-13 02:02 state
-rw-r--r-- 1 root root 4096 12-13 02:02 supported_mode
-rw-r--r-- 1 root root 4096 12-13 02:02 active_mode
-r--r--r-- 1 root root 4096 12-13 02:02 prot_capabilities
-r--r--r-- 1 root root 4096 12-13 02:02 prot_guard_type
drwxr-xr-x 2 root root    0 12-13 02:02 power
 

对这些属性文件解释以下:

  • 有四个 SCSI 特有的可写参数: scan,state,supported_mode,active_mode;能够向其中写入不一样的参数来控制此 SCSI 控制器的各类状态;
  • 其它一些可读属性用于读取这个 SCSI 控制器的一些当前值;

其中的 scan 属性文件在调试一些 SCSI 硬件驱动时颇有用,它是只写的,能够写入三个至四个以空格分开的整数,用于分别指定对应的 host, channel, id, lun 进行从新搜索。且这个 scan 属性支持以"-"做为通配符,如如下命令能够执行让整个 scsi_host 进行从新搜索,这个功能用于调试某些对热挺拔实现不完善的 SCSI 驱动程序颇有用:

# echo '- - -' >/sys/devices/pci0000:00/0000:00:02.5/host0/scsi_host/host0/scan
 

内核模块中的 sysfs 属性文件

以一个 8139too 模块为例解释在这个 kboject 下每个属性的用途;

# find /sys/module/8139too/ -ls
   6408    0 -r--r--r--   1 root     root         4096 12月 13 02:17 \
/sys/module/8139too/version
   6412    0 drwxr-xr-x   2 root     root            0 12月 13 02:17 \
/sys/module/8139too/sections
   6433    0 drwxr-xr-x   2 root     root            0 12月 13 02:17 \
/sys/module/8139too/notes
   6434    0 -r--r--r--   1 root     root           36 12月 13 02:17 \
/sys/module/8139too/notes/.note.gnu.build-id
   6486    0 drwxr-xr-x   2 root     root            0 12月 13 02:17 \
/sys/module/8139too/drivers
   6487    0 lrwxrwxrwx   1 root     root            0 12月 13 02:17 \
/sys/module/8139too/drivers/pci:8139too -> ../../../bus/pci/drivers/8139too
 

其中的属性文件都是只读的,用于提供信息。从 version, srcversion 上能够了解到这个模块所声明的版本号,源码版本号, refcnt 是模块引用计数, sections 属性组中有一些模块加载至内存的相应节信息, drivers/ 目录中是对所提供的驱动的连接;

由于模块是内核驱动编程的最佳选择,而一个模块有可能提供多个驱动程序,于是在未知一个设备在用哪个驱动的状况下能够先从 /sys/module/ 查找相应模块的状况,再从 drivers/ 发现出真正的驱动程序。或者也能够彻底反过来利用这些信息,先用 lspci/lshw 等工具找到 /sys/devices/ 下的设备节点,再从其设备的 driver 连接找到 /sys/bus/*/drivers/ 下的 device_driver, 再从 device_driver 下的 module 连接找到 /sys/module/*/,这样就能够获得已加载模块中空间是哪个模块在给一个设备提供驱动程序。

更多 sysfs 属性文件

以上所举的例子仅仅是一些常见的 sysfs 属性用法,实际的系统中还经常有不少其它的从未见过的 sysfs 属性,所以只有举例是不够的,即便维护了一份 sysfs 属性用法参考大全也不够,将来的内核版本还会出现新的 sysfs 属性,所以还必须了解 Linux 内核代码以找到实现这些属性的代码位置,以学会在没有相应属性文档的状况从内核源代码来分析其 sysfs 属性功能。

Sysfs 源码分析和编程实践

从源代码中理解 sysfs 属性的用途

更多的 sysfs 属性的功能只能靠阅读源代码来理解。仍是以上文提到的 scsi_host 的 scan 属性来理解,这个功能没有任何文档上有描述,所以只能去读源代码。

在内核中, sysfs 属性通常是由 __ATTR 系列的宏来声明的,如对设备的使用 DEVICE_ATTR ,对总线使用 BUS_ATTR ,对驱动使用 DRIVER_ATTR ,对类别(class)使用 CLASS_ATTR, 这四个高级的宏来自于 <include/linux/device.h>, 都是以更低层的来自 <include/linux/sysfs.h> 中的 __ATTR/__ATRR_RO 宏实现; 所以咱们在内核源码树中相应位置 drivers/scsi/ 找到这几个宏的使用状况,能够获得在 drivers/scsi/scsi_sysfs.c 中:

static ssize_t
store_scan(struct device *dev, struct device_attribute *attr,
            const char *buf, size_t count)
{
         struct Scsi_Host *shost = class_to_shost(dev);
         int res;
 
         res = scsi_scan(shost, buf);
         if (res == 0)
                 res = count;
         return res;
};
static DEVICE_ATTR(scan, S_IWUSR, NULL, store_scan);
 

DEVICE_ATTR 宏声明有四个参数,分别是名称、权限位、读函数、写函数。这里对应的,名称是 scan, 权限是只有属主可写(S_IWUSR)、没有读函数、只有写函数。所以读写功能与权限位是对应的,由于 DEVICE_ATTR 把权限位声明与真正的读写是否实现放在了一块儿,减小了出现不一致的可能。(上文提到 /proc/scsi/scsi 接口的权限位声明与其功能不对应,这与注册 proc 接口的函数设计中的不一致是有关系的,权限位声明与功能实现不在代码中同一个位置,所以易出错。虽然修复 /proc/scsi/scsi 的权限位错误很容易,但内核团队中多年来一直没有人发现或未有人去修正这个 BUG,应该是与 /proc/scsi/ 接口的过期有关,过期的功能会在将来某个内核版本中去除。)

上面的 scan 属性写入功能是在 store_scan 函数中实现的,这个接口的四个参数中, buf/count 表明用户写入过来的字符串,它把 buf 进一步传给了 scsi_scan 函数;若是进一步分析 scsi_scan 函数实现能够知道,它指望从 buf 中接受三个或四个整型值(也接受"-"做为通配符),分别表明 host, channel, id 三个值,(第四个整数在早期内核中曾表明 lun 号码,但在较新内核中第四个数字被忽略,仅做为向后兼容保留接受四个整数),而后对具体的 (host, channel, id) 进行从新扫描以发现这个 SCSI 控制器上的设备变更。

添加 sysfs 支持

若是你正在开发的设备驱动程序中须要与用户层的接口,通常可选的方法有:

  1. 注册虚拟的字符设备文件,以这个虚拟设备上的 read/write/ioctl 等接口与用户交互;但 read/write 通常只能作一件事情, ioctl 能够根据 cmd 参数作多个功能,但其缺点是很明显的: ioctl 接口没法直接在 Shell 脚本中使用,为了使用 ioctl 的功能,还必须编写配套的 C语言的虚拟设备操做程序, ioctl 的二进制数据接口也是形成大小端问题 (big endian与little endian)、32位/64位不可移植问题的根源;
  2. 注册 proc 接口,接受用户的 read/write/ioctl 操做;一样的,一个 proc 项一般使用其 read/write/ioctl 接口,它所存在的问题与上面的虚拟字符设备的的问题类似;
  3. 注册 sysfs 属性;

最重要的是,添加虚拟字符设备支持和注册 proc 接口支持这二者所须要增长的代码量都并很多,最好的方法仍是使用 sysfs 属性支持,一切在用户层是可见的透明,且增长的代码量是最少的,可维护性也最好;方法就是使用 <include/linux/device.h> 头文件提供的这四个宏,分别应用于总线/类别/驱动/设备四种内核数据结构对象上:

#define BUS_ATTR(_name, _mode, _show, _store)   \
struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)
 
#define CLASS_ATTR(_name, _mode, _show, _store)                 \
struct class_attribute class_attr_##_name = __ATTR(_name, _mode, _show, _store)
 
#define DRIVER_ATTR(_name, _mode, _show, _store)        \
struct driver_attribute driver_attr_##_name =           \
         __ATTR(_name, _mode, _show, _store)
 
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
 

总线(BUS)和类别(CLASS)属性通常用于新设计的总线和新设计的类别,这二者通常是不用的;由于你的设备通常是以PCI等成熟的常规方式链接到主机,而不会去新发明一种类型;使用驱动属性和设备属性的区别就在于:看你的 sysfs 属性设计是针对整个驱动有效的仍是针对这份驱动所可能支持的每一个设备分别有效。

从头文件中还能够找到 show/store 函数的原型,注意到它和虚拟字符设备或 proc 项的 read/write 的做用很相似,但有一点不一样是 show/store 函数上的 buf/count 参数是在 sysfs 层已做了用户区/内核区的内存复制,虚拟字符设备上常见的 __user 属性在这里并不须要,于是也不须要多一次 copy_from_user/copy_to_user, 在 show/store 函数参数上的 buf/count 参数已是内核区的地址,能够直接操做。

上面四种都是 Linux 统一设备模型所添加的高级接口,若是使用 sysfs 所提供的底层接口的话,则还有下面两个,定义来自 <include/linux/sysfs.h> :(上面的总线/类别/驱动/设备四个接口都是以这里的__ATTR实现的)

#define __ATTR(_name,_mode,_show,_store) { \
         .attr = {.name = __stringify(_name), .mode = _mode },   \
         .show   = _show,                                        \
     .store  = _store,                                       \
}
 
#define __ATTR_RO(_name) { \
         .attr   = { .name = __stringify(_name), .mode = 0444 }, \
         .show   = _name##_show,                                 \
}
 

上面这些宏都是在注册总线/类别/驱动/设备时做为缺省属性而使用的,在实际应用中还有一种状况是根据条件动态添加属性,如 PCI 设备上的 resource{0,1,2,...} 属性文件,由于一个 PCI 设备上的可映射资源究竟有多少没法预知,也只能以条件判断的方式动态添加上。

int __must_check sysfs_create_file(struct kobject *kobj,
                                    const struct attribute *attr);
int __must_check sysfs_create_bin_file(struct kobject *kobj,
                                        struct bin_attribute *attr);
 

这两个函数能够对一个 kobject 动态添加上文本属性或二进制属性,这也是惟一能够添加二进制属性的方法。

二进制属性与普通文本属性的区别在于:

  • 二进制属性 struct bin_attribute 中内嵌一个 struct attribute 结构体对象,所以具备普通属性的全部功能特征;
  • 二进制属性上多一个 size 用来描述此二进制文件的大小,而普通属性文件的大小老是 4096, 准确地说,应该是一个内存页的大小,由于从当前 sysfs 内核实现来讲,它分配一个内存页面来做为 (buf/count) 的缓冲区;
  • 二进制属性比普通属性多内存映射(mmap)接口的支持;

编程示例,对 LDD3 一书中的 lddbus 驱动程序的 sysfs 改进

首先,这个程序自己是针对当时做者写书的年代的内核(2.6.11)而编写的,在当前的 Fedora10 系统 (2.6.27.5-117.fc10.i686) 上甚至没法编译编译经过;所以首先须要将它移植过来至少达到可运行状态;

附件的压缩包中含有修改过的 lddbus, sculld 的源代码和修改过程的四个patch:

  • 第一个 0001-ldd3-examples-build-on-fedora-10-2.6.27.5-117.fc10.i.patch 是将 lddbus 和 sculld 移植到 Fedora10 内核上可运行,这其中主要是一此内核 API 的变化;
  • 第二个 0002-port-dmem-proc-entry-to-use-sysfs-entry.patch 演示了怎样将原有的 proc 接口改进成为 sysfs 属性接口的,从这个 patch 中能够看到删除的代码多而新增长的代码少,这说明对于相同的功能,使用 sysfs 编程接口的代码量更少,并且 sysfs 代码看起来也比 proc 更为整洁:打印每一个设备的调试信息能够作成每一个设备上分别有本身的接口,而不是统一的一个 proc 接口;设备属性文件最终出现的位置如 "/sys/devices/ldd0/sculld0/dmem";
    static ssize_t sculld_show_dmem(struct device *ddev,
             struct device_attribute *attr, char *buf)
    {
         /* 其中打印每一个设备调试信息的代码复制自原proc接口 */
    }
    static DEVICE_ATTR(dmem, S_IRUGO, sculld_show_dmem, NULL);
    static int __init sculld_register_dev(struct sculld_dev *dev, int index)
    {
         /* 建立此device属性文件 */
         ret |= device_create_file(&dev->ldev.dev, &dev_attr_dmem);
    }
     
  • 第三个 0003-add-.gitignore.patch 是增长了 .gitignore 文件,屏蔽一些编译生成的临时文件;
  • 第四个 0004-port-qset-get-set-ioctl-to-use-sysfs-entry.patch 演示了怎样把基于 ioctl 的操做接口改进成为基于 sysfs 接口,因为原来的 ioctl 接口设置和获取 qset 信息是表示整个驱动模块级的变量,它用来控制整个驱动程序而非驱动所支持的单个的设备,所以这个 qset 属性使用 DRIVER_ATTR 来添加更为合适;
    ssize_t sculld_show_qset(struct device_driver *driver, char *buf)
    {
         return snprintf(buf, PAGE_SIZE, "%d\n", sculld_qset);
    }
    ssize_t sculld_store_qset(struct device_driver *driver, const char *buf,
             size_t count)
    {
         sculld_qset = simple_strtol(buf, NULL, 0);
         return count;
    }
         /* 声明一个权限为0644的可同时读写的driver属性 */
    static DRIVER_ATTR(qset, S_IRUGO | S_IWUSR, sculld_show_qset, sculld_store_qset);
         /* 建立此driver属性文件 */
         result = driver_create_file(&sculld_driver.driver, &driver_attr_qset);
     

    驱动属性最终出现如 "/sys/bus/ldd/drivers/sculld/qset" ,这里声明的是同时可读写的,权限位 0644 与其保持一致。

    6446 0 -rw-r--r-- 1 root root 4096 12月 14 07:44 /sys/bus/ldd/drivers/sculld/qset

小结

sysfs 给应用程序提供了统一访问设备的接口,但能够看到, sysfs 仅仅是提供了一个能够统一访问设备的框架,但到底是否支持 sysfs 还须要各设备驱动程序的编程支持;在 2.6 内核诞生 5年以来的发展中,不少子系统、设备驱动程序逐渐转向了 sysfs 做为与用户空间友好的接口,但仍然也存在大量的代码还在使用旧的 proc 或虚拟字符设备的 ioctl 方式;若是仅从最终用户的角度来讲, sysfs 与 proc 都是在提供相同或相似的功能,对于旧的 proc 代码,没有绝对的必要去作 proc 至 sysfs 的升级;所以在可预见的未来, sysfs 会与 proc, debugfs, configfs 等共存很长一段时间。

下载资源

相关主题

相关文章
相关标签/搜索