linux seq_file机制学习

linux内核驱动模块经常要将一些信息通过/proc文件树暴露给用户,以方便用户直接能从文件系统中读取到驱动程序或者内核的一些状态信息,当这些信息比较短的时候编程比较容易,一旦过长并且用户有lseek相关的操作,那么在内核中编程就就会变得比较困难,需要维护很多状态。为了解决这个问题,linux内核提供了一种seq_file机制来简化编程的复杂性。

本文实验环境:Linux VM-0-13-ubuntu 5.4.0-90-generic #101-Ubuntu SMP Fri Oct 15 20:00:55 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

假设有这样一个场景

有一个内核模块,实现了一个自定义的路由表功能,并且想通过/proc/my_route_table这个接口暴露给用户。

当用户执行 cat /proc/my_route_table的时候,它可以按行打印出当前所有路由表的信息。

比如

1.1.1.1 2.2.2.2 gateway0

1.1.1.1 2.2.2.3 gateway1

...

当这个路由表比较小的时候,逻辑很好做,这时候一半用户态调用read时候的buffer长度一般都可以一次装下所有的数据。

但是随着数据规模的增加,肯定会出现buffer一次装不下的情况,需要装两次才能完成。

我们知道,如果想暴露一个文件作为从内核传递给用户数据的通道,无论是驱动设备,还是/proc下的文件,一般都需要实现一个file_operations 结构

static const struct file_operations xxx_fops = {
    .owner = THIS_MODULE,
    .read = xxx_read,
    .write = xxx_write,
    .llseek = xxx_llseek,
    .open = xxx_open,
    ...
};

在输出的场景中,我们需要关注xxx_read这个函数

static ssize_t xxx_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos);

通常情况下,我们要根据*ppos这个偏移量,计算出需要输出的内存数据,并拷贝到buf变量所指向的用户态内存中

但是在这种通过大量格式化字符串函数生成的字符串的场景下,计算偏移量所指向的位置的字符串的值很麻烦

比如考虑这样的场景

比如我有两个路由表

1.1.1.1 2.2.2.2 gateway0

1.1.1.1 2.2.2.3 gateway1

用户一次读10 字节,那么读到的数据是“1.1.1.1 2.“

再读10字节,读到的是“2.2.2 gate“

如果更变态一点,我输出的一行中有一个动态变化的统计数字,比如

1.1.1.1 2.2.2.2 gateway0 378384

1.1.1.1 2.2.2.3 gateway1 3346

由于有位数的变化,根本没有规律能够计算出每一行的长度,如果再加上用户的lseek动作,情况就会变得雪上加霜

对于这样的场景可以总结以下几个特征:

1. 输出的信息并非直接是内核的数据结构本身,而是转换成的一种可读的字符串

2. 只读,没有写的需求

3. 数据规模比较大,很有可能超过一次read,一般是某种表结构的输出

这看起来是一个非常通用的需求,于是内核提供了一个通用的实现,叫seq_file

使用seq_file实现一个这样的需求就变得非常简单,比如我们简化一下上面路由表的输出,变成通过cat /proc/sequence输出一个无限递增的数字,每输出一个就换一行。

看起来像这样,代码出处

下文的代码稍微有一些改动适配linux 5.4的内核代码

/*
 * Simple demonstration of the seq_file interface.
 *
 * $Id: seq.c,v 1.1 2003/02/10 21:02:02 corbet Exp $
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/fs.h>
#include <linux/seq_file.h>
#include <linux/slab.h>
...

/*
 * The sequence iterator functions.  We simply use the count of the
 * next line as our internal position.
 */
static void *ct_seq_start(struct seq_file *s, loff_t *pos) {
    loff_t *spos = kmalloc(sizeof(loff_t), GFP_KERNEL);
    if (!spos)
        return NULL;
    *spos = *pos;
    return spos;
}

static void *ct_seq_next(struct seq_file *s, void *v, loff_t *pos) {
    loff_t *spos = (loff_t *)v;
    *pos = ++(*spos);
    return spos;
}

static void ct_seq_stop(struct seq_file *s, void *v) {
    kfree(v);
}

/*
 * The show function.
 */
static int ct_seq_show(struct seq_file *s, void *v) {
    loff_t *spos = (loff_t *)v;
    seq_printf(s, "%Ld\n", *spos);
    return 0;
}

/*
 * Tie them all together into a set of seq_operations.
 */
static struct seq_operations ct_seq_ops = {
    .start = ct_seq_start,
    .next = ct_seq_next,
    .stop = ct_seq_stop,
    .show = ct_seq_show,
};

/*
 * Time to set up the file operations for our /proc file.  In this case,
 * all we need is an open function which sets up the sequence ops.
 */

static int ct_open(struct inode *inode, struct file *file) {
    return seq_open(file, &ct_seq_ops);
};

/*
 * The file operations structure contains our open function along with
 * set of the canned seq_ ops.
 */
static struct file_operations ct_file_ops = {
    .owner = THIS_MODULE,
    .open = ct_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .release = seq_release,
};

/*
 * Module setup and teardown.
 */

static int ct_init(void) {
    struct proc_dir_entry *entry;
    entry = proc_create("sequence", 0, NULL, &ct_file_ops);
    if (entry == NULL)
        return -ENOMEM;
    return 0;
}

static void ct_exit(void) {
    remove_proc_entry("sequence", NULL);
}

module_init(ct_init);
module_exit(ct_exit);

可以看到为了创建/proc/sequence这个节点,我们调用

entry = proc_create("sequence", 0, NULL, &ct_file_ops);

 其中依然有一个file_operations结构的对象ct_file_ops

但我们看到这个对象的初始化被大大简化了

static struct file_operations ct_file_ops = {
    .owner = THIS_MODULE,
    .open = ct_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .release = seq_release,
};

可以看到,read,llseek,release,都没有自己实现,而是调用了seq_file机制提供的帮助函数

既然有系统固化的逻辑,则必然有一些约定,这个约定可以在open的实现中看到,也就是ct_open函数

static int ct_open(struct inode *inode, struct file *file) {
    return seq_open(file, &ct_seq_ops);
};

可以看到,又是调用了一个seq_file机制提供的帮助函数seq_open,那么所谓的“契约”应该就在这里面注册的结构体ct_seq_ops中了

static struct seq_operations ct_seq_ops = {
    .start = ct_seq_start,
    .next = ct_seq_next,
    .stop = ct_seq_stop,
    .show = ct_seq_show,
};

可以看到ct_seq_ops是一个seq_operations结构体,分别需要告诉seq_file机制四种行为,分别是start next stop show

先看start和next

static void *ct_seq_start(struct seq_file *s, loff_t *pos) {
    loff_t *spos = kmalloc(sizeof(loff_t), GFP_KERNEL);
    if (!spos)
        return NULL;
    *spos = *pos;
    return spos;
}

static void *ct_seq_next(struct seq_file *s, void *v, loff_t *pos) {
    loff_t *spos = (loff_t *)v;
    *pos = ++(*spos);
    return spos;
}

重点关注这里的返回值类型,是一个void *,而next函数的第二个参数也是一个void *

每一次用户有read动作,被称为一个session

session开始时,seq_file机制会调用start函数,start会初始化迭代器

迭代器分为两部分

1. *pos的值

2. return 的void *

*pos值会作为next的第三个参数,void *会作为next的第二个参数

而next修改过的 *pos 和void*又回成为下一个next的输入

直到next return一个NULL

这里可以参考内核源码

每经过一次迭代都会有一次输出,也就是调用show

static int ct_seq_show(struct seq_file *s, void *v) {
    loff_t *spos = (loff_t *)v;
    seq_printf(s, "%Ld\n", *spos);
    return 0;
}

这里非常贴心的提供了一个seq_printf函数,是我们可以直接把格式化的字符串输出到seq_file *s的某个buffer中去,至于分几次返回给用户,就不需要我们操心了。

stop 函数很简单,给我们一个机会:在本次session结束之前,清理一些临时数据

static void ct_seq_stop(struct seq_file *s, void *v) {
    kfree(v);
}

 我们来看一下迭代这部分的核心逻辑

p = m->op->start(m, &m->index);
while (1) {
    ...
    err = m->op->show(m, p);
    ...
    if (unlikely(!m->count)) { // empty record
        p = m->op->next(m, p, &m->index);
        continue;
    }
    if (!seq_has_overflowed(m)) // got it
        goto Fill;
    // need a bigger buffer
    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);
}

 可以看到实现的非常贴心,包括考虑了一次输出如果超过了buffer给定的范围之后的重新分配2倍内存重试的机制。

下面我们来验证一下,当我们加载了内核模块之后,通过下面两个命令来读这个文件

dd if=/proc/sequence of=out1 count=1
dd if=/proc/sequence skip=1 of=out2 count=1

 发现他们的输出刚好可以拼接在一起

out1

0
1
2
3
...
154
15

out2

5
156
...
281
282
28

 另外当我们提到skip参数值的时候,会发现dd命令的执行时间会成比例提高

ubuntu@VM-0-13-ubuntu:~/linux_learn_diary/seq_file_demo$ dd if=/proc/sequence skip=122 of=out2 count=1
dd: /proc/sequence: cannot skip to specified offset
1+0 records in
1+0 records out
512 bytes copied, 0.00176521 s, 290 kB/s
ubuntu@VM-0-13-ubuntu:~/linux_learn_diary/seq_file_demo$ dd if=/proc/sequence skip=12222 of=out2 count=1
dd: /proc/sequence: cannot skip to specified offset
1+0 records in
1+0 records out
512 bytes copied, 0.0734327 s, 7.0 kB/s
ubuntu@VM-0-13-ubuntu:~/linux_learn_diary/seq_file_demo$ dd if=/proc/sequence skip=1222222 of=out2 count=1
dd: /proc/sequence: cannot skip to specified offset
1+0 records in
1+0 records out
512 bytes copied, 5.64728 s, 0.1 kB/s

 当我们跳过122w个bs之后,耗时达到了5.6s,可想而知seq_file的实现机制就是迭代。

以上例子的可执行的代码

另外,当对每一次打开有private_data需求的时候该怎么办呢

一般的场景是这样的

在创建proc下的文件节点时,根据文件节点的含义,会给文件节点挂一份相应的数据结构

在迭代输出时,要使用对应的数据结构

给节点挂数据的操作是这样的

    entry = proc_create_data("test_d0", 0, NULL, &ct_file_ops, &d0);
    if (entry == NULL) {
        return -ENOMEM;
    }
    entry = proc_create_data("test_d1", 0, NULL, &ct_file_ops, &d1);
    if (entry == NULL) {
        return -ENOMEM;
    }

 就是使用了proc_create_data函数,绑定了一份私有数据

我们要在open函数中,将inode这份私有数据,放到file相关的私有数据中

正常在驱动程序中一般是将数据放到struct file *file 中的private_data下面,但是由于private_data被seq_file已经占用了(已经存放了一个struct seq_file*),于是seq_file又在自己的私有数据结构中留了一个下一层的私有数据指针,给我们使用

那么我们代码绑定自己的私有数据结构看起来像这样

static int ct_open(struct inode *inode, struct file *file) {
    int ret = 0;
    ret = seq_open(file, &ct_seq_ops);
    if (0 == ret) {
        struct seq_file *seq = file->private_data;
        // 5.4的内核已经屏蔽了 struct proc_dir_entry
        // 原来拿proc_dir_entry中private数据的方法是
        // 1. 通过PDE宏拿到 proc_dir_entry结构体指针
        // 2. 通过指针拿到private数据
        // 现在的方法是直接通过PDE_DATA宏拿到数据
        seq->private = PDE_DATA(inode);
    }
    return ret;
};

 linux 5.4中直接使用PDE_DATA来获取inode绑定的数据

这里要注意,不是说seq->private 指向了数据,就要使用seq_release_private作为file_opreations的release函数的,因为seq_release_private在关闭文件的时候要释放seq->private,但是如果指向的内存并非一个在open的时候动态申请的内存(比如在别的时刻动态申请的内存,或者静态内存),就会由于多次释放之类的错误崩溃。比如这个例子中指向的是PDE_DATA(inode),后者指向的是一个static变量。

当然,我们依然可以用seq_file机制实现一个返回数据很小的文件,并且如果不考虑迭代的话,seq_file提供了一种更简单的机制

static int ct_open(struct inode *inode, struct file *file) {
    return single_open(file, ct_seq_show, PDE_DATA(inode));
};

static struct file_operations ct_file_ops = {
    .owner = THIS_MODULE,
    .open = ct_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .release = single_release,
};

我们不需要提供 start stop next,只需要提供show即可

使用single_open替代seq_open

使用single_release替代seq_release

使用single_open的例子

参考文献:

The seq_file Interface — The Linux Kernel documentation

The /proc/sequence module source [LWN.net]

序列文件(seq_file)接口 - 摩斯电码 - 博客园

seq_file学习(1)—— single_open - 摩斯电码 - 博客园

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值