提到 sysfs 文件系统 ,必须先需要了解的是Linux设备模型,什么是Linux设备模型呢?
一、Linux 设备模型
1、设备模型概述
从2.6版本开始,Linux开发团队便为内核建立起一个统一的设备模型。在以前的内核中没有独立的数据结构用来让内核获得系统整体配合的信息。尽管缺乏这些信息,在多数情况下内核还是能正常工作的。然而,随着拓扑结构越来越复杂,以及要支持诸如电源管理等新特性的需求,向新版本的内核明确提出了这样的要求:需要有一个对系统结构的一般性抽象描述,即设备模型。
目的
I 设备、驱动、总线等彼此之间关系错综复杂。如果想让内核运行流畅,那就必须为每个模块编码实现这些功能。如此一来,内核将变得非常臃肿、冗余。而设备模型的理念即是将这些代码抽象成各模块共用的框架,这样不但代码简洁了,也可让设备驱动开发者摆脱这本让人头痛但又必不可少的一劫,将有限的精力放于设备差异性的实现。
II 设备模型用类的思想将具有相似功能的设备放到一起管理,并将相似部分萃取出来,使用一份代码实现。从而使结构更加清晰,简洁。
III 动态分配主从设备号,有效解决设备号的不足。设备模型实现了只有设备在位时才为其分配主从设备号,这与之前版本为每个设备分配一个主从设备号不同,使得有限的资源得到合理利用。
IV 设备模型提供sysfs文件系统,以文件的方式让本是抽象复杂而又无法捉摸的结构清晰可视起来。同时也给用户空间程序配置处于内核空间的设备驱动提供了一个友善的通道。
V 程序具有随意性,同一个功能,不同的人实现的方法和风格各不相同,设备驱动亦是如此。大量的设备亦若实现方法流程均不相同,对以后的管理、重构将是难以想象的工作量。设备模型恰是提供了一个模板,一个被证明过的最优的思路和流程,这减少了开发者设计过程中不必要的错误,也给以后的维护扫除了障碍。
2、设备模型结构
如表,Linux设备模型包含以下四个基本结构:
类型 | 所包含的内容 | 内核数据结构 | 对应/sys项 |
设备(Devices) | 设备是此模型中最基本的类型,以设备本身的连接按层次组织 | struct device | /sys/devices/*/*/.../ |
驱动 (Drivers) | 在一个系统中安装多个相同设备,只需要一份驱动程序的支持 | struct device_driver | /sys/bus/pci/drivers/*/ |
总线 (Bus) | 在整个总线级别对此总线上连接的所有设备进行管理 | struct bus_type | /sys/bus/*/ |
类别(Classes) | 这是按照功能进行分类组织的设备层次树;如 USB 接口和 PS/2 接口的鼠标都是输入设备,都会出现在/sys/class/input/下 | struct class | /sys/class/*/ |
device、driver、bus、class是组成设备模型的基本数据结构。kobject是构成这些基本结构的核心,kset又是相同类型结构kobject的集合。kobject和kset共同组成了sysfs的底层数据体系。本节采用从最小数据结构到最终组成一个大的模型的思路来介绍。当然,阅读时也可先从Device、Driver、Bus、Class的介绍开始,先总体了解设备模型的构成,然后再回到kobject和kset,弄清它们是如何将Device、Driver、Bus、Class穿插链接在一起的,以及如何将这些映像成文件并最终形成一个sysfs文件系统。
二、sys 文件系统
sysfs是一个基于内存的文件系统,它的作用是将内核信息以文件的方式提供给用户程序使用。
sysfs可以看成与proc,devfs和devpty同类别的文件系统,该文件系统是虚拟的文件系统,可以更方便对系统设备进行管理。它可以产生一个包含所有系统硬件层次视图,与提供进程和状态信息的proc文件系统十分类似。
sysfs把连接在系统上的设备和总线组织成为一个分级的文件,它们可以由用户空间存取,向用户空间导出内核的数据结构以及它们的属性。sysfs的一个目的就是展示设备驱动模型中各组件的层次关系,其顶级目录包括block,bus,drivers,class,power和firmware等.
sysfs提供一种机制,使得可以显式的描述内核对象、对象属性及对象间关系。sysfs有两组接口,一组针对内核,用于将设备映射到文件系统中,另一组针对用户程序,用于读取或操作这些设备。表2描述了内核中的sysfs要素及其在用户空间的表现:
sysfs在内核中的组成要素 | 在用户空间的显示 |
内核对象(kobject) | 目录 |
对象属性(attribute) | 文件 |
对象关系(relationship) | 链接(Symbolic Link) |
sysfs目录结构:
/sys 下的子目录 | 所包含的内容 |
/sys/devices | 这是内核对系统中所有设备的分层次表达模型,也是/sys文件系统管理设备的最重要的目录结构; |
/sys/dev | 这个目录下维护一个按字符设备和块设备的主次号码(major:minor)链接到真实的设备(/sys/devices下)的符号链接文件; |
/sys/bus | 这是内核设备按总线类型分层放置的目录结构, devices 中的所有设备都是连接于某种总线之下,在这里的每一种具体总线之下可以找到每一个具体设备的符号链接,它也是构成 Linux 统一设备模型的一部分; |
/sys/class | 这是按照设备功能分类的设备模型,如系统所有输入设备都会出现在/sys/class/input 之下,而不论它们是以何种总线连接到系统。它也是构成 Linux 统一设备模型的一部分; |
/sys/kernel | 这里是内核所有可调整参数的位置,目前只有 uevent_helper, kexec_loaded, mm, 和新式的slab 分配器等几项较新的设计在使用它,其它内核可调整参数仍然位于sysctl(/proc/sys/kernel) 接口中; |
/sys/module | 这里有系统中所有模块的信息,不论这些模块是以内联(inlined)方式编译到内核映像文件(vmlinuz)中还是编译为外部模块(ko文件),都可能会出现在/sys/module 中:
|
/sys/power | 这里是系统中电源选项,这个目录下有几个属性文件可以用于控制整个机器的电源状态,如可以向其中写入控制命令让机器关机、重启等。 |
表3:sysfs目录结构
三、深入理解 sysfs 文件系统
sysfs是一个特殊文件系统,并没有一个实际存放文件的介质。
1、kobject结构
sysfs的信息来源是kobject层次结构,读一个sysfs文件,就是动态的从kobject结构提取信息,生成文件。重启后里面的信息当然就没了
sysfs文件系统与kobject结构紧密关联,每个在内核中注册的kobject对象都对应于sysfs文件系统中的一个目录。
Kobject 是Linux 2.6引入的新的设备管理机制,在内核中由struct kobject表示。通过这个数据结构使所有设备在底层都具有统一的接口,kobject提供基本的对象管理,是构成Linux2.6设备模型的核心结构,Kobject是组成设备模型的基本结构。类似于C++中的基类,它嵌入于更大的对象的对象中,用来描述设备模型的组件。如bus,devices, drivers 等。都是通过kobject连接起来了,形成了一个树状结构。这个树状结构就与/sys向对应。
2、sysfs 如何读写kobject 结构
sysfs就是利用VFS的接口去读写kobject的层次结构,建立起来的文件系统。 kobject的层次结构的注册与注销XX_register()形成的。文件系统是个很模糊广泛的概念, linux把所有的资源都看成是文件,让用户通过一个统一的文件系统操作界面,也就是同一组系统调用,对属于不同文件系统的文件进行操作。这样,就可以对用户程序隐藏各种不同文件系统的实现细节,为用户程序提供了一个统一的,抽象的,虚拟的文件系统界面,这就是所谓"VFS(Virtual Filesystem Switch)"。这个抽象出来的接口就是一组函数操作。
我们要实现一种文件系统就是要实现VFS所定义的一系列接口,file_operations, dentry_operations, inode_operations等,供上层调用。
file_operations是描述对每个具体文件的操作方法(如:读,写);
dentry_operations结构体指明了VFS所有目录的操作方法;
inode_operations提供所有结点的操作方法。
举个例子,我们写C程序,open(“hello.c”, O_RDONLY),它通过系统调用的流程是这样的
open() -> 系统调用-> sys_open() -> filp_open()-> dentry_open() -> file_operations->open()
不同的文件系统,调用不同的file_operations->open(),在sysfs下就是sysfs_open_file()。我们使用不同的文件系统,就是将它们各自的文件信息都抽象到dentry和inode中去。这样对于高层来说,我们就可以不关心底层的实现,我们使用的都是一系列标准的函数调用。这就是VFS的精髓,实际上就是面向对象。
注意sysfs是典型的特殊文件。它存储的信息都是由系统动态的生成的,它动态的包含了整个机器的硬件资源情况。从sysfs读写就相当于向 kobject层次结构提取数据。
下面是详细分析:
a -- sysfs_dirent是组成sysfs单元的基本数据结构,它是sysfs文件夹或文件在内存中的代表。sysfs_dirent只表示文件类型(文件夹/普通文件/二进制文件/链接文件)及层级关系,其它信息都保存在对应的inode中。我们创建或删除一个sysfs文件或文件夹事实上只是对以sysfs_dirent为节点的树的节点的添加或删除。sysfs_dirent数据结构如下:
- struct sysfs_dirent {
- atomic_t s_count;
- atomic_t s_active;
- struct sysfs_dirent *s_parent; /* 指向父节点 */
- struct sysfs_dirent *s_sibling; /* 指向兄弟节点,兄弟节点是按照inode索引s_ino的大小顺序链接在一起的。*/
- const char *s_name; /* 节点名称 */
- union {
- struct sysfs_elem_dir s_dir; /* 文件夹,s_dir->kobj指向sysfs对象 */
- 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; /* inode索引,创建节点时被动态申请,通过此值和sysfs_dirent地址可以到inode散列表中获取inode结构 */
- umode_t s_mode;
- struct iattr *s_iattr;
- };
b -- inode(index node)中保存了设备的主从设备号、一组文件操作函数和一组inode操作函数。
文件操作比较常见:open、read、write等。inode操作在sysfs文件系统中只针对文件夹实现了两个函数一个是目录下查找inode函数(.lookup=sysfs_lookup),该函数在找不到inode时会创建一个,并用sysfs_init_inode为其赋值;另一个是设置inode属性函数(.setattr=sysfs_setattr),该函数用于修改用户的权限等。inode结构如下:
- struct inode {
- struct hlist_node i_hash; /* 散列表链节 */
- struct list_head i_list;
- struct list_head i_sb_list;
- struct list_head i_dentry; /* dentry链节 */
- unsigned long i_ino; /* inode索引 */
- atomic_t i_count;
- unsigned int i_nlink;
- uid_t i_uid;
- gid_t i_gid;
- dev_t i_rdev; /* 主从设备号 */
- const struct inode_operations *i_op; /* 一组inode操作函数,可用其中lookup查找目录下的inode,对应sysfs为sysfs_lookup函数 */
- const struct file_operations *i_fop; /* 一组文件操作函数,对于sysfs为sysfs的open/read/write等函数 */
- struct super_block *i_sb;
- struct list_head i_devices;
- union {
- struct pipe_inode_info *i_pipe;
- struct block_device *i_bdev;
- struct cdev *i_cdev;
- };
- };
c -- dentry(directory entry) 的中文名称是目录项,是Linux文件系统中某个索引节点(inode)的链接。
这个索引节点可以是文件,也可以是目录。引入dentry的目的是加快文件的访问。dentry数据结构如下:
- struct dentry {
- atomic_t d_count; /* 目录项对象使用的计数器 */
- unsigned int d_flags; /* 目录项标志 */
- spinlock_t d_lock; /* 目录项自旋锁 */
- int d_mounted; /* 对于安装点而言,表示被安装文件系统根项 */
- struct inode *d_inode; /* 文件索引节点(inode) */
- /*
- * The next three fields are touched by __d_lookup. Place them here
- * so they all fit in a cache line.
- */
- struct hlist_node d_hash; /* lookup hash list */
- struct dentry *d_parent; /* parent directory */
- struct qstr d_name; /* 文件名 */
- /*
- * d_child and d_rcu can share memory
- */
- union {
- struct list_head d_child; /* child of parent list */
- struct rcu_head d_rcu;
- } d_u;
- void *d_fsdata; /* 与文件系统相关的数据,在sysfs中指向sysfs_dirent */
- unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* 存放短文件名 */
- };
sysfs_dirent、inode、dentry三者关系:
如上图sysfs超级块sysfs_sb、dentry根目录root、sysfs_direct根目录sysfs_root都是在sysfs初始化时创建。
sysfs_root下的子节点是添加设备对象或对象属性时调用sysfs_create_dir/ sysfs_create_file创建的,同时会申请对应的inode的索引号s_ino。注意此时并未创建inode。
inode是在用到的时候调用sysfs_get_inode函数创建并依据sysfs_sb地址和申请到的s_ino索引计算散列表位置放入其中。dentry的子节点也是需要用的时候才会创建。比如open文件时,会调用path_walk根据路径一层层的查找指定dentry,如果找不到,则创建一个,并调用父dentry的inodelookup函数(sysfs文件系统的为sysfs_lookup)查找对应的子inode填充指定的dentry。
这里有必要介绍一下sysfs_lookup的实现,以保证我们更加清晰地了解这个过程,函数主体如下:
- static struct dentry * sysfs_lookup(struct inode *dir, struct dentry *dentry, struct nameidata *nd)
- {
- struct dentry *ret = NULL;
- struct sysfs_dirent *parent_sd = dentry->d_parent->d_fsdata; //获取父sysfs_direct
- struct sysfs_dirent *sd;
- struct inode *inode;
- mutex_lock(&sysfs_mutex);
- /* 在父sysfs_direct查找名为dentry->d_name.name的节点 */
- sd = sysfs_find_dirent(parent_sd, dentry->d_name.name);
- /* no such entry */
- if (!sd) {
- ret = ERR_PTR(-ENOENT);
- goto out_unlock;
- }
- /* 这儿就是通过sysfs_direct获取对应的inode,sysfs_get_inode实现原理上面已经介绍过了 */
- /* attach dentry and inode */
- inode = sysfs_get_inode(sd);
- if (!inode) {
- ret = ERR_PTR(-ENOMEM);
- goto out_unlock;
- }
- /* 填充目录项,至此一个目录项创建完毕 */
- /* instantiate and hash dentry */
- dentry->d_op = &sysfs_dentry_ops; /* 填充目录项的操作方法,该方法只提供一释放inode函数sysfs_d_iput */
- dentry->d_fsdata = sysfs_get(sd); //填充sysfs_direct
- d_instantiate(dentry, inode); //填充inode
- d_rehash(dentry); //将dentry加入hash表
- out_unlock:
- mutex_unlock(&sysfs_mutex);
- return ret;
- }
四、实例分析
a -- sysfs文件open流程
open的主要过程是通过指定的路径找到对应的dentry,并从中获取inode,然后获取一个空的file结构,将inode中相关内容赋值给file,这其中包括将inode的fop赋给file的fop。因此接下来调用的filp->fop->open其实就是inode里的fop->open。新的file结构对应一个文件句柄fd,这会作为整个open函数的返回值。之后的read/write操作就靠这个fd找到对应的file结构了。
图3-2是从网上找到的,清晰地描述了file和dentry以及inode之间的关系
图3-2:file、dentry、inode关系
进程每打开一个文件,就会有一个file结构与之对应。同一个进程可以多次打开同一个文件而得到多个不同的file结构,file结构描述了被打开文件的属性,读写的偏移指针等等当前信息。
两个不同的file结构可以对应同一个dentry结构。进程多次打开同一个文件时,对应的只有一个dentry结构。dentry结构存储目录项和对应文件(inode)的信息。
在存储介质中,每个文件对应唯一的inode结点,但是,每个文件又可以有多个文件名。即可以通过不同的文件名访问同一个文件。这里多个文件名对应一个文件的关系在数据结构中表示就是dentry和inode的关系。
inode中不存储文件的名字,它只存储节点号;而dentry则保存有名字和与其对应的节点号,所以就可以通过不同的dentry访问同一个inode。
b -- sysfs文件read/write流程
sysfs与普通文件系统的最大差异是,sysfs不会申请任何内存空间来保存文件的内容。事实上再不对文件操作时,文件是不存在的。只有用户读或写文件时,sysfs才会申请一页内存(只有一页),用于保存将要读取的文件信息。如果作读操作,sysfs就会调用文件的父对象(文件夹kobject)的属性处理函数kobject->ktype->sysfs_ops->show,然后通过show函数来调用包含该对象的外层设备(或驱动、总线等)的属性的show函数来获取硬件设备的对应属性值,然后将该值拷贝到用户空间的buff,这样就完成了读操作。写操作也类似,都要进行内核空间ßà用户空间内存的拷贝,以保护内核代码的安全运行。
图为用户空间程序读sysfs文件的处理流程,其他操作类似: