再识proc(linux seq_file 机制)

  linux内核驱动模块经常要将一些信息通过/proc文件树暴露给用户,以方便用户直接能从文件系统中读取到驱动程序或者内核的一些状态信息,当这些信息比较短的时候编程比较容易,一旦过长并且用户有lseek相关的操作,那么在内核中编程就就会变得比较困难,需要维护很多状态。为了解决这个问题,linux内核提供了一种seq_file机制来简化编程的复杂性。
  我们知道,如果想将一个文件作为从内核传递给用户数据的通道,无论是驱动设备,还是/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操作,情况就会变得雪上加霜。

  假设还有这样一个场景:
有一个内核模块,实现了一个自定义的路由表功能,并且想通过/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一次装不下的情况,需要装两次才能完成。

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

    1. 输出的信息并非直接是内核的数据结构本身,而是转换成的一种可读的字符串
    1. 只读,没有写的需求
    1. 数据规模比较大,通过一次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 
#include 
#include 
#include 
#include 
#include 
...
 
/*
 * 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
这里非常贴心的提供了一个seq_printf函数,是我们可以直接把格式化的字符串输出到seq_file *s的某个buffer中去,至于分几次返回给用户,就不需要我们操心了。
stop 函数很简单,给我们一个机会:在本次session结束之前,清理一些临时数据

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

我们来看一下迭代这部分的核心逻辑(位于seq_read函数)

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的时候动态申请的内存(比如在别的时刻动态申请的内存,或者静态内存),就会由于多次释放之类的错误崩溃。

当然,我们依然可以用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

————————————————
版权声明:本文为CSDN博主「LiWang112358」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/LiWang112358/article/details/123588281

你可能感兴趣的:(linux,linux,api,linux)