在内核中, sysfs 属性一般是由 __ATTR 系列的宏来声明的,如对设备使用 DEVICE_ATTR,对总线使用 BUS_ATTR,对驱动使用 DRIVER_ATTR,对类别(class)使用 CLASS_ATTR,这四个高级的宏来自于 <include/linux/device.h>,都是以更低层的来自 <include/linux/sysfs.h> 中的 __ATTR/__ATTR_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 把权限位声明与真正的读写是否实现放在了一起,减少了出现不一致的可能。
上面的 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 属性支持,一切都在用户层是可见的透明,且增加的代码量是最少的,可维护性也最好;方法就是使用 <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 / wirte 的作用很类似,但有一点不同是 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)接口的支持。
小结
sysfs 给应用程序提供了统一访问设备的接口,但可以看到,sysfs 仅仅是提供了一个可以统一访问设备的框架,但究竟是否支持 sysfs 还需要各设备驱动程序的编程支持;在 2.6 内核诞生5年以来的发展中,很多子系统、设备驱动程序逐渐转向了 sysfs 作为用户空间友好的接口,但仍然存在大量的代码还在使用旧的 proc 或虚拟字符设备的 ioctl 方式;如果仅从最终用户的角度来说,sysfs 与 proc 都是在提供相同或类似的功能,对于旧的 proc 代码,没有绝对的必要去做 proc 至 sysyfs 的升级;因此可预见的将来,sysyfs 会与 proc,debugfs,configfs 等共存很长一段时间。