《sysfs 文件系统 》
作者:Patrick Mochel
来源:网络
提要
sysfs是2.6内核的一个特性,它允许内核代码经由一个in-memory的文件系统把信息出报(export)到用户进程中,文件系统的目录等列(heirarchy)的组织是严格的,并构成了内核数据结构的内部组织的基础。在这种文件系统中产生的文件大多数是ASCII文件,通常每个文件有一个值。这些特性保证了被出报的信息的准确性并易于被访问,从而使sysfs成为2.6内核的最直观、最有用的特性之一。
介绍
sysfs是内核对象、属性及它们的相互关系的一种表现机制。它提供了两个组件:把这些条目通过sysfs来出报的内核编程接口,和一个用户接口,用来查看和操作这些映射了它们所代表的内核对象的条目。下面这个表揭示了内部(内核)建制与他们的外部(用户空间)sysfs映射。
内部----- 外部
内核对象---目录
对象属性---常规文件
对象关系---软连接
sysfs 是内核底层的一个核心成分,它的意义在于提供一个较为简单的接口来实施一项简单的任务,代码中很少有过分复杂的内容或含混的描述。然而,像许多底层的核心成分一样,它会有点抽象和疏离以致于难以缕清思路。为了减轻困难,在进入实质性的细节之前本文先对sysfs来一个渐进的诠释。
首先写一段短小精悍的历史描述它的起源,其次是包括了对sysfs的挂载与访问的关键信息,而后传述了sysfs中的目录组织和子系统布局,它为用户理解通过sysfs出报的信息的组织与内容提供了足够的信息,尽管由于时间与篇幅的局限,不是每个对象及其属性都会被写到。
这篇文章的首要目的是为内部sysfs接口 - 用来把内核建制向用户空间出报的数据结构与函数 - 提供一个技术视角,它描述了上面提到的三个概念(内核对象、对象属性和对象关系)中的函数,并为每个概念设一个章节。它还为另外两个用来简化一些普通操作的常规文件接口 - 属性组与二进制属性 - 分别设一个章节。
sysfs是内核空间与用户空间之间的一个信息渠道,用户空间的应用程序有很多机会来利用这种信息,目前的利用法是I/O调度器的参数能力与udev程序。最末章节叙述了使用sysfs的当前应用的一个范例,并且为促使这个领域的进一步开发作一个鞭策。
因为它是个简单的而且基本上是抽象的接口,所以需要很多时间来描述它与使用它的子系统们之间的交互。对于kobject与device model来说更是这样,这两个都是2.6内核的新特性并与sysfs密切地交织在一起,由于篇幅所限本文不可能对这些话题都充分地展开,还是把它们留给其它的文章作主题吧。
1. sysfs 的历史
sysfs是一个in-memory文件系统,最开始是基于ramfs的, ramfs是在2.4.0版内核稳定过程中的时候写成的,它作得很妙,通过它可以看到利用当时还是新事物的VFS layer写一个简单的文件系统是多么地容易,由于它简易并使用VFS,它成为一个好的基础,使其它的in-memory文件系统从它那里衍生出来。
sysfs 最初被称为ddfs(设备驱动文件系统),它的产生是为了调试正在开发过程中的新的驱动模型,调试代码最初使用procfs来出报设备树,但在莱纳斯*托瓦兹的严格的催促下,它转而使用了一个基于ramfs的新的文件系统。
当时,这种新的设备模型被溶入内核(2.5.1前后),为了使其更明了给它换了个名字叫driverfs,次年在2.5版的开发中,这种设备模型与设备文件系统的底层功能证明对其它的子系统很有用。kobject被开发出来,提供了一个中央对象管理机制,而设备文件系统则被转换成sysfs,来表示它的子系统的不可预知性。
2. sysfs 的挂载
sysfs像其它基于内存的文件系统一样可以从用户空间挂载:
mount -t sysfs sysfs /sys
sysfs还能够在启动时用/etc/fstab文件来自动挂载。大多数支持2.6 内核的发行版都在 /etc/fstab有sysfs行:
sysfs /sys sysfs noauto 0 0
注意sysfs被挂载的目录:/sys, 这是sysfs 佳载点的事实标准位置,各主流发行版都采用这个作法。
3.sysfs 的浏览
既然sysfs是由目录、文件、软连接组成的集合,那么就可以用简单的 shell工具来浏览和操作,本人推荐用 tree(1)这个工具,它是内核对象底层的核心的开发过程中最得力的助手。
sysfs挂载点的顶层是一定数量的目录,这些目录代表了注册了sysfs的主要的子系统。在本文撰写之时, 这些目录是:
/sys/
| - - block
| - - bus
| - - class
| - - device
| - - firmware
| - - module
| - - power
这些目录是在子系统注册kobject核心的系统启动时刻产生的, 当它们被初始化以后, 它们开始搜寻在各自的目录内注册了的对象。对象注册sysfs的方式以及目录如何产生的将在后文解释. 其间, 有兴趣的话最好在这个sysfs等列中来一番审视, 下面叙述各子系统的含义以及它们的内容。
3.1 块
"块"目录包含了在系统中发现的每个块设备的子目录,每个块设备的目录中是各种属性,描述了方方面面,从设备大小到映射的dev_t数值,有一个指向块设备所映射的物理设备的软连接(在物理设备树上,将在后文说到),还有一个目录揭示了I/O调度器的接口,这个接口提供了一些数据,它们是关于设备请求队列和一些可调整的特性,用户和管理员可以用它们来优化性能,包括用它们来动态改变I/O调度器。每个分区块设备表示为块设备的子目录,这些目录中包含了分区的只读属性。
3.2 总线
"总线"目录包含了在内核中注册而得到支持(统一编译或是通过模块来加载的)的每个物理总线类型的子目录, 部分输出如下:
bus/
| - - ide
| - - pci
| - - scsi
| - - usb
每个总线类型以两个子目录列出: devices 和 drivers , devices目录包含了在整个系统中发现的每一个该总线类型的设备的列表,这些列出的设备实际上是在全局设备树中指向设备目录的软连接。如:
| - - 0000:00:00.0 -> ../../../devices/pci0000:00/0000:00:00.0
| - - 0000:00:01.0 -> ../../../devices/pci0000:00/0000:00:01.0
| - - 0000:01:00.0 -> ../../../devices/pci0000:00/0000:00:01.0/0000:01:00.0
| - - 0000:02:00.0 -> ../../../devices/pci0000:00/0000:00:1e.0/0000:02:00.0
| - - 0000:02:00.1 -> ../../../devices/pci0000:00/0000:00:1e.0/0000:02:00.1
| - - 0000:02:01.0 -> ../../../devices/pci0000:00/0000:00:1e.0/0000:02:01.0
| - - 0000:02:02.0 -> ../../../devices/pci0000:00/0000:00:1e.0/0000:02:02.0
drivers目录包含了注册该总线类型的每个驱动的目录,每个驱动目录中是允许查看和操作设备参数的属性,和指向该设备所绑定的物理设备(在全局设备树上)的软连接。
3.3 类
"类“目录包含了在内核中注册了的每个设备类的表示,一个设备类描述了设备的一个功能类型,如下:
class/
| - - graphics
| - - input
| - - net
| - - printer
| - - scsi_device
| - - sound
| - - tty
每个设备类包含了每个分配并注册了那个设备类的类对象的子目录,大多数设备类对象的目录包含了指向与那个类对象关联的设备和驱动目录(分别在全局的设备等列与总线等列)的软连接,注意,在设备与物理设备之间不一定是1:1的映射,一个物理设备也许包含多个类对象执行不同的逻辑功能。例如,一个物理鼠标会映射一个内核鼠标对象,也会映射一个泛”输入事件“设备,也许还会映射一个”输入调试“设备。每个类与类对象会包含各种属性,它们阵列出参数来描述并控制那个类对象,内容与格式完全是类依赖的,并依赖于内核中所存的支持。
以上所举的例子仅仅是一些常见的 sysfs 属性用法,实际的系统中还常常有很多其它的从未见过的 sysfs 属性,因此只有举例是不够的,即使维护了一份 sysfs 属性用法参考大全也不够,未来的内核版本还会出现新的 sysfs 属性,因此还必须了解 Linux 内核代码以找到实现这些属性的代码位置,以学会在没有相应属性文档的情况从内核源代码来分析其 sysfs 属性功能。
Sysfs 源码分析和编程实践
从源代码中理解 sysfs 属性的用途
更多的 sysfs 属性的功能只能靠阅读源代码来理解。还是以上文提到的 scsi_host 的 scan 属性来理解,这个功能没有任何文档上有描述,因此只能去读源代码。
在内核中, sysfs 属性一般是由 __ATTR 系列的宏来声明的,如对设备的使用 DEVICE_ATTR ,对总线使用 BUS_ATTR ,对驱动使用 DRIVER_ATTR ,对类别(class)使用 CLASS_ATTR, 这四个高级的宏来自于
|
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 支持
如果你正在开发的设备驱动程序中需要与用户层的接口,一般可选的方法有:
注册虚拟的字符设备文件,以这个虚拟设备上的 read/write/ioctl 等接口与用户交互;但 read/write 一般只能做一件事情, ioctl 可以根据 cmd 参数做多个功能,但其缺点是很明显的: ioctl 接口无法直接在 Shell 脚本中使用,为了使用 ioctl 的功能,还必须编写配套的 C语言的虚拟设备操作程序, ioctl 的二进制数据接口也是造成大小端问题 (big endian与little endian)、32位/64位不可移植问题的根源;
注册 proc 接口,接受用户的 read/write/ioctl 操作;同样的,一个 proc 项通常使用其 read/write/ioctl 接口,它所存在的问题与上面的虚拟字符设备的的问题相似;
注册 sysfs 属性;
最重要的是,添加虚拟字符设备支持和注册 proc 接口支持这两者所需要增加的代码量都并不少,最好的方法还是使用 sysfs 属性支持,一切在用户层是可见的透明,且增加的代码量是最少的,可维护性也最好;方法就是使用
#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 所提供的底层接口的话,则还有下面两个,定义来自
#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, "%dn", 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 等共存很长一段时间。