Linux 驱动之文件系统

seq_file

概述

seq_file 只是在普通文件 read 中加入了内核缓冲的功能,从而实现顺序多次遍历读取大数据量的简单接口,seq_file 一般只提供只读接口。

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};

核心功能

seq_file 接口可通过 <linux/seq_file.h> 获得。 seq_file 包含三个方面:

  • An iterator interface which lets a virtual file implementation step through the objects it is presenting
  • Some utility functions for formatting objects for output without needing to worry about things like output buffers
  • A set of canned file_operations which implement most operations on the virtual file

实现文件在 fs/seq_file.c 和 include/linux/seq_file.h 中。

总结

seq_file 是为 proc 文件系统设计的,但不只是 proc,在需要创建一个由一系列数据顺序组合而成的虚拟文件或一个较大的虚拟文件时,推荐使用 seq_file 接口。这正是符合 bin_attribute 的场景。用户空间通过 open、read 等函数操作文件时通过系统的 VFS 调用到内核层,其中 file 结构体的 private 指针指向 seq_file 结构体,然后调用该结构体里的 op 指针指向的相应函数。

kernfs

概述

在 Linux 内核中,kernfs 提供内核子系统内部虚拟文件系统所需的功能,源于拆分 sysfs 使用的部分内部逻辑,它通过将有关硬件设备和相关设备驱动程序的信息从内核的设备模型导出到用户空间,提供一组虚拟文件,从而实现独立且可重用的功能。 其他内核子系统可以更容易,更一致地实现自己的文件系统。

数据结构

先整体看下数据结构,如下图:原图

该图展示了各个数据结构之间的关系以及实现方式,kernfs 是 fs 抽象出来公共的属性操作,sysfs 基本就是调用该结构的方法来实现,同时 kobject 的目录和属性文件在用户空间的文件组织也是通过该数据结构实现的。

实现

所有文件系统的实现都是通过 file_operations 结构体实现具体的操作的,kernfs 也是,它同样实现了 file_operations 结构体相关操作,如下:

1
2
3
4
5
6
7
8
9
10
const struct file_operations kernfs_file_fops = {
.read = kernfs_fop_read,
.write = kernfs_fop_write,
.llseek = generic_file_llseek,
.mmap = kernfs_fop_mmap,
.open = kernfs_fop_open,
.release = kernfs_fop_release,
.poll = kernfs_fop_poll,
.fsync = noop_fsync,
};

这个可以关注下 open 成员的实现函数 kernfs_fop_open,

  • ((struct seq_file *)file->private_data)->private = of: 可以看到 kernfs_open_file 结构体是挂在 seq_file 结构体的 private 指针下的,现在可以梳理下这三者之间的关系,file 结构体的 private_data 指向 seq_file,seq_file 的 private 指向 kernfs_open_file,这样他们就都联系起来的,当然这里是分析特定文件系统下的实现,在其他情况下可以指向其他数据结构

  • seq_open(file, &kernfs_seq_ops): seq_file 的实现就使用了 kernfs 结构,kernfs 实现了 seq_file 的 file_operations 结构成员,如下:

    1
    2
    3
    4
    5
    6
    static const struct seq_operations kernfs_seq_ops = {
    .start = kernfs_seq_start,
    .next = kernfs_seq_next,
    .stop = kernfs_seq_stop,
    .show = kernfs_seq_show,
    };

    然后通过调用 seq_open 将该数据结构连入 seq_file 结构体的 op 成员中。

sysfs

概述

sysfs 是非持久性虚拟文件系统,它提供系统的全局视图,并通过它们的 kobiect 显示内核对象的层次结构(拓扑)。每个 kobiect 显示为目录和目录中的文件,目录代表相关 kobject 导出的内核变量。这些文件称为属性,可以读取或写入。

对系统上的每个块设备,block 都包含一个目录,目录下包含设备上分区的子目录。bus 包含系统上注册的总线。dev 以原始方式(无层次结构)包含已注册的设备节点,每个节点都是/sys/devices 目录中真实设备的符号链接。devices 给出系统内设备的拓扑结构视图。firmware 显示系统相关的低层子系统树,如 ACPI、EFI、OF (DT)。fs 列出系统上实际使用的文件系统。kernel 保存内核配置选项和状态信息。module 是加载的模块列表。

kernel_kobj:对应于/sys/kernel
power_kobj:对应于/sys/power
firmware_kobi :对应于/sys/firmware,导出在 drivers/baselirmware.c 源文件中
hvpervisor_kobj:对应于/sys/hypervisor,导出在 drivers/base hypervisor.c 中
fs_kobj:对应于/sys/fs,导出在 fs/namespace.c 文件中

然而,class/、dev/、devices/是在启动期间由内核源代码内 drivers_base/core.c 中的 devices_init 函数创建的,block/在 block/genhd.c 中创建,bus/ 在 drivers/base/bus.c 中被创建为 kset。

kobject 目录被添加到 sysfs(使用 kobject_add)时,其添加位置取决于 kobject 的父项。如果其父指针已设置,则它将被添加为父目录内的子目录。如果父指针为 NULL,则将其添加为 kset->kobj 内的子目录。如果其父和 kset 字段都未设置,它将映射到 sysfs 内的根目录(/sys)。

挂载 sysfs

1
mount -t sysfs sysfs /sys

属性文件使用

属性文件定义

裸属性不包含读取或写入属性值的方法。 鼓励子系统定义自己的属性结构和包装函数,以便为特定对象类型添加和删除属性。 例如驱动模型定义 struct device_attribute 如下:

1
2
3
4
5
6
7
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};

它还定义了用于定义设备属性的 helper 程序:

1
2
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

例如做如下声明:

1
static DEVICE_ATTR(foo, S_IWUSR | S_IRUGO, show_foo, store_foo);

相当于定义:

1
2
3
4
5
6
7
8
static struct device_attribute dev_attr_foo = {
.attr = {
.name = "foo",
.mode = S_IWUSR | S_IRUGO,
},
.show = show_foo,
.store = store_foo,
};

子系统定义回调

当子系统定义新的属性类型时,它必须实现一组 sysfs 操作,以将读取和写入调用转发到属性所有者的 show 和 store 方法。读取或写入文件时,sysfs 会调用该类型的适当方法。 然后该方法将通用 struct kobject 和 struct attribute 指针转换为适当的指针类型,并调用关联的方法。

阐明:

1
2
#define to_dev(obj) container_of(obj, struct device, kobj)
#define to_dev_attr(_attr) container_of(_attr, struct device_attribute, attr)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

这里通过 to_dev_attr 宏定义(container_of 方法)通过 kobject 结构体找到子系统或自定义的数据结构指针,这里以 device_attribute 为例,该数据结构要实现自己的 show 和 store 方法。得到 device_attribute 数据地址后调用该数据的 show/store 方法。

可以看到 kobject 实际上实现了一套机制,该机制帮组用户方便的创建文件和删除文件并提供访问文件的方法,而它本身并未实现具体的操作。

一个简单的例子,来看看具体的使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);

参考文献

窝窝科技相-设备驱动模型
https://blog.csdn.net/chenying126/article/details/78079942
https://www.kernel.org/doc/html/latest/filesystems/seq_file.html
https://lwn.net/Articles/22359/
《linux 设备驱动开发》
《linux 内核文档》