Linux设备管理(四)_从sysfs回到ktype

时间:2022-12-03 10:54:01

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有着本质的区别

参考文档