由于procfs的默认操作函数只使用一页的缓存,在处理较大的proc文件时就有点麻烦,并且在输出一系列结构体中的数据时也比较不灵活,需要自己在read_proc函数中实现迭代,容易出现Bug。所以内核黑客们对一些/proc代码做了研究,抽象出共性,最终形成了seq_file(Sequence file:序列文件)接口。 这个接口提供了一套简单的函数来解决以上proc接口编程时存在的问题,使得编程更加容易,降低了Bug出现的机会。
在需要创建一个由一系列数据顺序组合而成的虚拟文件或一个较大的虚拟文件时,推荐使用seq_file接口。但是我个人认为,并不是只有procfs才可以使用这个seq_file接口,因为其实seq_file是实现的是一个操作函数集,这个函数集并不是与proc绑定的,同样可以用在其他的地方。对于一个函数接口层的学习,首先要看一个相关的数据结构struct seq_file:
include/linux/seq_file.h
- struct seq_file {
- char *buf; //seq_file接口使用的缓存页指针
- size_t size; //seq_file接口使用的缓存页大小
- size_t from; //从seq_file中向用户态缓冲区拷贝时相对于buf的偏移地址
- size_t count; //buf中可以拷贝到用户态的字符数目
- loff_t index; //start、next的处理的下标pos数值
- loff_t read_pos; //当前已拷贝到用户态的数据量大小
- u64 version;
- struct mutex lock; //针对此seq_file操作的互斥锁,所有seq_*的访问都会上锁
- const struct seq_operations *op; //操作实际底层数据的函数
- void *private;
- };
在这个结构体中,几乎所有的成员都是由seq_file内部实现来处理的,程序员不用去关心,除非你要去研究seq_file的内部原理。对于这个结构体,程序员唯一要做的就是实现其中的
const struct seq_operations *op。为使用 seq_file接口对于不同数据结构体进行访问,你必须创建一组简单的对象迭代操作函数。- 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内部机制使用这些接口函数访问底层的实际数据结构体,并不断沿数据序列向前,同时逐个输出序列里的数据到seq_file自建的缓存(大小为一页)中。也就是说seq_file内部机制帮你实现了对序列数据的读取和放入缓存的机制,你只需要实现底层的迭代函数接口就好了,因为这些是和你要访问的底层数据相关的,而seq_file属于上层抽象。这可能看起来有点复杂,大家看了下面的图就好理解了:
这里我们大致介绍下struct seq_operations中的各个函数的作用:
void * (*start) (struct seq_file *m, loff_t *pos);
- start 方法会首先被调用,它的作用是在设置访问的起始点。
- m:指向的是本seq_file的结构体,在正常情况下无需处理。
- pos:是一个整型位置值,指示开始读取的位置。对于这个位置的意义完全取决于底层实现,不一定是字节为单位的位置,可能是一个元素的序列号。
- 返回值如果非NULL,则是一个指向迭代器实现的私有数据结构体指针。如果访问出错则返回NULL。
设置好了访问起始点,seq_file内部机制可能会使用show方法获取start返回值指向的结构体中的数据到内部缓存,并适时送往用户空间。
- int (*show) (struct seq_file *m, void *v);
所以show方法就是负责将v指向元素中的数据输出到seq_file的内部缓存,但是其中必须借助seq_file提供的一些类似printf的接口函数:
- int seq_printf(struct seq_file *sfile, const char *fmt, ...);
- //专为 seq_file 实现的类似 printf 的函数;用于将数据常用的格式串和附加值参数.
- //你必须将给 show 函数的 set_file 结构指针传递给它。如果seq_printf 返回-1,意味着缓存区已满,部分输出被丢弃。但是大部分时候都忽略了其返回值。
- int seq_putc(struct seq_file *sfile, char c);
- int seq_puts(struct seq_file *sfile, const char *s);
- //类似 putc 和 puts 函数的功能,sfile参数和返回值与 seq_printf相同。
- int seq_escape(struct seq_file *m, const char *s, const char *esc);
- //这个函数类似 seq_puts ,但是它会将 s 中所有在 esc 中出现的字符以八进制格式输出到缓存。
- //esc 的常用值是"\t\n\\", 它使内嵌的空格不会搞乱输出或迷惑 shell 脚本.
int seq_write(struct seq_file *seq, const void *data, size_t len)
//直接将data指向的数据写入seq_file缓存,数据长度为len。用于非字符串数据。
- int seq_path(struct seq_file *sfile, struct vfsmount *m, struct dentry *dentry, char *esc);
- //这个函数能够用来输出给定目录项关联的文件名,驱动极少使用。
在show函数返回之后,seq_file机制可能需要移动到下一个数据元素,那就必须使用next方法。
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
- v:是之前调用start或next返回的元素指针,可能是上一个show已经完成输出所指向的元素。
- pos:需要移动到的元素索引值。
在next实现中应当递增pos指向的值,但是具体递增的数量和迭代器的实现有关,不一定是1。而next的返回值如果非NULL,则是下一个需要输出到缓存的元素指针,否则表明已经输出结束,将会调用stop方法做清理。
- void (*stop) (struct seq_file *m, void *v);
在stop实现中,参数m指向本seq_file的结构体,在正常情况下无需处理。而v是指向上一个next或start返回的元素指针。在需要做退出处理的时候才需要实现具体的功能。但是许多情况下可以直接返回。
在next和start的实现中可能需要对一个序列的函数进行遍历,而在内核中,对于一个序列数据结构体的实现一般是使用双向链表或者哈希链表,所有seq_file同时提供了一些对于内核双向链表和哈希链表的封装接口函数,方便程序员实现对于通过链表链接的结构体序列的操作。这些函数名一般是seq_list_*或者seq_hlist_*,这些函数的实现都在fs/seq_file.c中,有兴趣的朋友可以看看。我在后面的实验中依然使用内核通用的双向链表API。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
编程步骤:
在上面我们介绍了使用seq_file需要实现的一些函数和相关结构体,现在我们把她们组合起来,介绍以下通过proc来使用seq_file的一般步骤,而seq_file在其他方面的应用方法也是一样的。
(1)在注册proc文件入口的时候,注册包含seq_file操作函数的file_operations结构体到proc_fops。我的测试程序中的实例代码如下:
- proc_test_entry = create_proc_entry("proc_seq", 0644, NULL);
- if (proc_test_entry == NULL) {
- ret = -ENOMEM;
- cleanup_test_data();
- pr_err("proc_test: Couldn't create proc entry\n");
- } else {
- proc_test_entry->proc_fops = &proc_ops;
- pr_info("proc_test: Module loaded.\n");
- }
(2)实现struct file_operations proc_ops,示例如下:
- static struct file_operations proc_ops = {
- .owner = THIS_MODULE,
- .open = proc_seq_open,
- .read = seq_read,
- .llseek = seq_lseek,
- .release = seq_release,
- };
大家可以看到,以上的read、llseek、release都是有seq_file提供的接口函数,直接注册即可,唯有open是需要自己实现的。
(3)这个open函数的实现也是非常简单固定格式,实例如下:
- static int proc_seq_open(struct inode *inode, struct file *file)
- {
- return seq_open(file, &proc_seq_ops);
- };
可以看到,一般就是使用seq_file中的一个API:seq_open,目的是向seq_file结构体中注册一个struct seq_operations 。
(4)实现 seq_operations ,也就是前面我们介绍的seq_file的底层数据操作函数集,示例如下:
- static struct seq_operations proc_seq_ops = {
- .start = proc_seq_start,
- .next = proc_seq_next,
- .stop = proc_seq_stop,
- .show = proc_seq_show
- };
这些回调函数都是需要根据你所要获取的序列数据结构来实现的。
其实在实际编程过程应该是相反的:(4)-->(3)-->(2)-->(1),但是为了更好的理解,我就从最后的注册开始,再介绍需要实现的结构体。
好了,基本的seq_file的使用都已经介绍过了,但是要真正的用好seq_file最好还是要去看一下她的实现源码(fs/seq_file.c),只有了解了内部原理,用起来才会得心应手。
而对于前面所使用的测试代码以及前面的procfs的测试代码,在下一篇中将会给出,并做测试记录。
阅读(1064) | 评论(0) | 转发(0) |