Seq File 在 Linux 中很常见,因此这里稍微整理一下,也分享一下我的理解。
由于 procfs 的默认操作函数只使用一页的缓存,在处理较大的proc文件时就有点麻烦,并且在输出一系列结构体中的数据时也比较不灵活,因此内核开发者们抽象出了seq_file(Sequence file:序列文件)接口。 提供了一套简单的函数来解决以上 procfs 编程时存在的问题,使得编程更加容易。
同时也需要注意,虽然 seq_file 最一开始是为了方便 procfs 使用,但大家意识到,这玩意很 nice 啊,于是已经渗透到很多别的虚拟文件系统种,比如 debugfs,sysfs,都有很多的应用。
1. API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| struct seq_file { char *buf; // 序列文件对应的数据缓冲区,要导出的数据是首先打印到这个缓冲区,然后才被拷贝到指定的用户缓冲区。 size_t size; // 缓冲区大小,默认为1个页面大小,随着需求会动态以2的级数倍扩张,4k,8k,16k... size_t from; // 没有拷贝到用户空间的数据在 buf 中的起始偏移量 size_t count; // buf中没有拷贝到用户空间的数据的字节数,调用 seq_printf() 等函数向buf写数据的同时相应增加m->count size_t pad_until; // useless loff_t index; // 正在或即将读取的数据项索引,和 seq_operations 中的start、next 操作中的 pos 项一致,一条记录为一个索引 loff_t read_pos; // 当前读取数据(file)的偏移量,字节为单位 u64 version; // 文件的版本 struct mutex lock; // 序列化对这个文件的并行操作 const struct seq_operations *op; // 指向seq_operations int poll_event; const struct file *file; // seq_file 相关的 proc 或其他文件 void *private; //指向文件的私有数据 };
seq_open:通常会在打开文件的时候调用,以第二个参数为 seq_operations 表创建 seq_file 结构体。 seq_read, seq_lseek,seq_release:直接对应着文件操作表中的 read, llseek 和 release。 seq_escape:将一个字符串中的需要转义的字符(字节长)以8进制的方式打印到 seq_file。 seq_putc, seq_puts, seq_printf:分别和C语言中的 putc,puts 和 printf 相对应。 seq_path:输出文件名 single_open, single_release: 打开和释放只有一条记录的文件。 seq_open_private, __seq_open_private, seq_release_private:和 seq_open 类似,不过打开 seq_file 的时候创建一小块文件私有数据,这个 private 数据会保存在 file->private 字段中,之后读取或者写入的时候,会有其特定的作用
|
2. procfs 和 seq_file 结合
以 /proc/kallsyms
这个文件为例,演示一下 procfs 和 seq_file 是如何结合在一起的。
先了解一下 /proc/kallsyms
的作用,使用 sudo cat /proc/kallsyms
命令,就可以查看这个虚拟文件的内容,发现是很多符号,以及符号对应的地址,其实就是目前正在运行的 kernel 的符号信息。这些信息显然不会保存在磁盘当中,是随着系统运行变化的,自然需要通过虚拟文件暴露给用户。其读取正是用到了 seq_file 以及 seq_read 一系列接口
2.1 kallsyms 初始化
/proc/kallsyms
这个文件,使用的也是 procfs 提供的 proc_create 接口进行创建的
1 2 3 4 5 6 7 8 9 10 11 12 13
| static const struct proc_ops kallsyms_proc_ops = { .proc_open = kallsyms_open, .proc_read = seq_read, .proc_lseek = seq_lseek, .proc_release = seq_release_private, };
static int __init kallsyms_init(void) { proc_create("kallsyms", 0444, NULL, &kallsyms_proc_ops); return 0; }
|
直接看这里的 kallsyms_proc_ops
结构,proc_read
proc_lseek
proc_release
的回调函数都很普通,关键自然在于 proc_open
对应的 kallsyms_open
,通过这个名字也不难得知,通过 open 系统调用打开 /proc/kallsyms
文件的时候,内部则会调用该回调函数进行处理。
那么,关注一下这里的 kallsyms_open
,内部是调用了 __seq_open_private
创建了一个 iter,联系上面 API 说明可知,在打开这个文件的时候,设置了一个 private 字段,用来辅助后面的 read write 操作,而这里对应的就应该是 kallsym_iter 结构了
1 2 3 4 5 6 7 8 9 10 11
| static int kallsyms_open(struct inode *inode, struct file *file) { struct kallsym_iter *iter; iter = __seq_open_private(file, &kallsyms_op, sizeof(*iter)); if (!iter) return -ENOMEM; reset_iter(iter, 0);
iter->show_value = kallsyms_show_value(file->f_cred); return 0; }
|
可以看出,__seq_open_private
没什么神奇的,就是分配了 private 所需要的空间,并且将 file->private 指向该 private 字段,然后返回给外层的调用者。
有了这个保存在 file->private 中的 iter,我们就赋予了 seq_read 一个状态。
这点我觉得非常重要,因为在遍历 kernel symbols 的过程中,必须要清楚当前读取到了哪一步,或者说当前的状态是什么样的。对于有一些静态信息来说,则无关紧要,不需要保存状态,也就不用通过 __seq_open_private
来打开文件了。
与之对应,在 release 文件的时候,需要销毁这个额外分配出来的 private 字段,因此,proc_release 对应的是 seq_release_private
,而非普通的 seq_release
。
并且 __seq_open_private
传入了一个很重要的参数:kallsyms_op
,这就是遍历并读取 seq_file 的几个核心 API,看上去非常简单,事实上也正是如此,通过 start, next, stop, show
的配合,决定该 seq_file 的读取如何开始,何时结束,如何展示,这就是典型的内核提供的机制,用户提供策略(当然不是普通的用户),填充好回调函数,就能按照机制走流程,在特定的位置上调用这些回调函数即可。
1 2 3 4 5 6
| static const struct seq_operations kallsyms_op = { .start = s_start, .next = s_next, .stop = s_stop, .show = s_show };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| void *__seq_open_private(struct file *f, const struct seq_operations *ops, int psize) { int rc; void *private; struct seq_file *seq;
private = kzalloc(psize, GFP_KERNEL_ACCOUNT); if (private == NULL) goto out;
rc = seq_open(f, ops); if (rc < 0) goto out_free;
seq = f->private_data; seq->private = private; return private;
out_free: kfree(private); out: return NULL; } EXPORT_SYMBOL(__seq_open_private);
|
2.2. seq_read API
seq_operations
中的 start, next, stop, show
最终都是要在读取文件的时候起作用的,seq_file 的读取文件的函数为 seq_read
,在作用了一些简单的初始化操作之后,比如分配 buf,设置好 size,紧接着就调用了 seq_read_iter
,这才是读取文件的核心 API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) { struct iovec iov = { .iov_base = buf, .iov_len = size}; struct kiocb kiocb; struct iov_iter iter; ssize_t ret;
init_sync_kiocb(&kiocb, file); iov_iter_init(&iter, READ, &iov, 1, size);
kiocb.ki_pos = *ppos; ret = seq_read_iter(&kiocb, &iter); *ppos = kiocb.ki_pos; return ret; } EXPORT_SYMBOL(seq_read);
|
seq_read_iter
中,省去一些细枝末节的部分,就是下面这样的结构,先调用 op->start(...)
,然后在一个 while 循环中,通过 op->show()
进行展示,通过 op->next()
进行迭代,直到 EOF 或者发生了错误,这才跳出循环,调用 op->stop()
,同时,循环中还需要注意 m->buf 是否超出了原先分配的大小,如果超出了,需要重新分配缓冲,并且调用 op->start()
重启
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter) { m->from = 0; p = m->op->start(m, &m->index); while (1) { err = PTR_ERR(p); if (!p || IS_ERR(p)) break; err = m->op->show(m, p); if (err < 0) break; if (unlikely(err)) m->count = 0; if (unlikely(!m->count)) { p = m->op->next(m, p, &m->index); continue; } if (!seq_has_overflowed(m)) goto Fill; m->op->stop(m, p); kvfree(m->buf); m->count = 0; m->buf = seq_buf_alloc(m->size <<= 1); if (!m->buf) goto Enomem; p = m->op->start(m, &m->index); } m->op->stop(m, p); m->count = 0; goto Done; copied = m->count ? -EFAULT : err; return copied; } EXPORT_SYMBOL(seq_read_iter);
|
而所有输出的内容,其实都会保存在 m->buf 当中,最后一次性从内核空间返回给读取该文件的用户
2.3 kallsyms 如何迭代
现在我们已经基本了解了 seq_file 是如何运转的了,接下俩接着解决 kallsyms 的问题,我们还剩下一个问题没有讨论:seq_operations
中回调函数的实现细节
可以看出,s_next s_start
都是调用了 update_iter
进行调用,如果返回为 0,表明遇到了 EOF 或者发生了错误,直接返回 NULL 就好了,这样在 seq_read_iter
当中,就能及时跳出循环。
而这里的 s_stop
什么都没做,其实也没有什么好做的,并没有什么资源啊的等待释放。
s_show
给人一种亲切感,因为这个函数中的 seq_printf
输出的格式,不就是执行 sudo cat /proc/kallsyms
得到的单行的文件内容吗!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| static void *s_next(struct seq_file *m, void *p, loff_t *pos) { (*pos)++;
if (!update_iter(m->private, *pos)) return NULL; return p; }
static void *s_start(struct seq_file *m, loff_t *pos) { if (!update_iter(m->private, *pos)) return NULL; return m->private; }
static void s_stop(struct seq_file *m, void *p) { }
static int s_show(struct seq_file *m, void *p) { void *value; struct kallsym_iter *iter = m->private;
if (!iter->name[0]) return 0;
value = iter->show_value ? (void *)iter->value : NULL;
if (iter->module_name[0]) { char type;
type = iter->exported ? toupper(iter->type) : tolower(iter->type); seq_printf(m, "%px %c %s\t[%s]\n", value, type, iter->name, iter->module_name); } else seq_printf(m, "%px %c %s\n", value, iter->type, iter->name); return 0; }
|
那么,update_iter
做了什么呢?就留给读者自己去研究吧!
3. 自己写一个 proc seeq_file
了解 seq_file 的使用方式之后,自己来写一个类似 kallsyms 的模块也是轻而易举的事情了。
如下所示,是输出当前的所有进程,同时更新 offset 这个变量(其实就是 line number),除了 seq_file 和 procfs 的处理以外,还用到了 init_task
这个全局变量,是系统初始化时候创建的进程,同时,这些进程都以链表的方式存放,可以调用 next_task
获取下一个进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
| #include <linux/kernel.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> #include <linux/percpu.h> #include <linux/init.h> #include <linux/sched.h>
MODULE_LICENSE("GPL"); MODULE_AUTHOR("Girish Joshi"); MODULE_DESCRIPTION("HelloWorld Linux Kernel Module.");
static struct proc_dir_entry *entry; static loff_t offset = 1;
static void *l_start(struct seq_file *m, loff_t * pos) { loff_t index = *pos; loff_t i = 0; struct task_struct * task ;
if (index == 0) { seq_printf(m, "Current all the processes in system:\n" "%-24s%-5s%-5s\n", "name", "pid","index"); printk(KERN_EMERG "++++++++++=========>%5d\n", 0); return &init_task; }else { for(i = 0, task=&init_task; i < index; i++){ task = next_task(task); } BUG_ON(i != *pos); if(task == &init_task){ return NULL; }
printk(KERN_EMERG "++++++++++>%5d\n", task->pid); return task; } }
static void *l_next(struct seq_file *m, void *p, loff_t * pos) { struct task_struct * task = (struct task_struct *)p;
task = next_task(task); if ((*pos != 0) && (task == &init_task)) { return NULL; }
printk(KERN_EMERG "=====>%5d\n", task->pid); offset = ++(*pos);
return task; }
static void l_stop(struct seq_file *m, void *p) { printk(KERN_EMERG "------>\n"); }
static int l_show(struct seq_file *m, void *p) { struct task_struct * task = (struct task_struct *)p;
seq_printf(m, "%-24s%-5d\t%-5lld\n", task->comm, task->pid, offset); return 0; }
static struct seq_operations kallprocesses_seq_fops = { .start = l_start, .next = l_next, .stop = l_stop, .show = l_show };
static int kallprocesses_open(struct inode *inode, struct file *file) { seq_open(file, &kallprocesses_seq_fops); return 0; }
static const struct proc_ops kallprocesses_proc_ops = { .proc_open = kallprocesses_open, .proc_read = seq_read, .proc_lseek = seq_lseek, .proc_release = seq_release, };
static int __init hello_init(void) { entry = proc_create("kallprocesses", 0444, NULL, &kallprocesses_proc_ops); printk(KERN_INFO "Hello world!\n"); return 0; }
static void __exit hello_cleanup(void) { printk(KERN_INFO "Cleaning up module.\n"); }
module_init(hello_init); module_exit(hello_cleanup);
|