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 recordp = m->op->next(m, p, &m->index);continue;}if (!seq_has_overflowed(m)) // got itgoto Fill;// need a bigger bufferm->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
发现他们的输出刚好可以拼接在一起
out10
1
2
3
...
154
15out25
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 - 摩斯电码 - 博客园
linux seq_file机制学习相关推荐
- 【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】Linux信号机制分析
Linux信号机制分析 Sailor_forever sailing_9806@163.com 转载请注明 http://blog.csdn.net/sailor_8318/archive/2008 ...
- linux通信机制总结
目录 1. Linux通信机制分类简介 2. Inter-Process Communication (IPC) mechanisms: 进程间通信机制0x1: 信号量(Signals)0x2: 管道 ...
- linux线程并不真正并行,Linux系统编程学习札记(十二)线程1
Linux系统编程学习笔记(十二)线程1 线程1: 线程和进程类似,但是线程之间能够共享更多的信息.一个进程中的所有线程可以共享进程文件描述符和内存. 有了多线程控制,我们可以把我们的程序设计成为在一 ...
- Windows消息机制学习笔记(一)—— 消息队列
Windows消息机制学习笔记(一)-- 消息队列 基本概念 实验一:使用代码画出最简单窗口 第一步:编译并运行以下代码 第二步:查看运行结果 第三步:使用其它窗口对其进行覆盖,观察效果 总结 消息队 ...
- linux 内核 ide,Linux设备驱动程序学习(7)-内核的数据类型
Linux设备驱动程序学习(7)-内核的数据类型 由于前面的学习中有用到 第十一章 内核数据结构类型 的知识,所以我先看了.要点如下: 将linux 移植到新的体系结构时,开发者遇到的若干问题都与不正 ...
- 嵌入式系统开发学习步骤(Linux高级编程学习顺序)
2019独角兽企业重金招聘Python工程师标准>>> 嵌入式系统开发学习步骤(Linux高级编程学习顺序) 1.Linux 基础 安装Linux操作系统 Linux文件系统 Lin ...
- Linux设备驱动程序学习(2)-调试技术
Linux设备驱动程序学习(2)-调试技术 http://blog.chinaunix.net/u3/102754/showart_2018516.html 今天进入<Linux设备驱动程序(第 ...
- linux文件控制驱动程序,Linux设备驱动程序学习(6)-高级字符驱动程序操作[(3)设备文件的访问控制]...
Linux设备驱动程序学习(6) -高级字符驱动程序操作[(3)设备文件的访问控制] 提供访问控制对于一个设备节点来的可靠性来说有时是至关重要的.这部分的内容只是在open和release方法上做些修 ...
- Linux Namespace机制简介
最近Docker技术越来越受到关注,作为Docker中很重要的一项技术,Namespace也就经常在Docker的简介里面看到. 在这里总结一下它的内部机制.也解决一下自己原来的一些疑惑. Names ...
最新文章
- oracle grand select,Oracle SQL 高级篇
- Uber无人车撞人视频公布,究竟哪儿出问题了?
- 深度详解ResNet到底在解决一个什么问题?
- Hadoop完全分布式环境搭建(三节点)
- 在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁
- Srping MVC入门推荐
- 查看ie保存的表单_小学信息技术gt;搜索保存网页教师资格证面试模板
- 日期无忧,Python计算日期清单
- php 7中文手册pdf版,手册的格式 - PHP 7 中文文档
- 学习党Win10装机必备软件
- 使用Arcade制作的简单吃豆人游戏
- 如何解开payload.bin获取包括Android内核在内的系统镜像文件?payload.bin解包教程
- 1.认识华为数据通信
- 关于重装系统前的准备、备份和重装完后系统优化、使用习惯等说明
- 十点读书:如果你不想工作了,就去这四个地方走走
- MATLAB 暖通,MATLAB在暖通空调课程教学中的应用
- Windows注册表的基本知识及应用
- 递归实现树状分级部门树《部门单表》
- 【SCSS】1300- 这些 SCSS 使用技巧真好用~
- 因特网(Internet)与万维网(www)区别