内容简介
本文主要讲述序列文件(seq_file)接口的内核实现,如何使用它将Linux内核里面常用的数据结构通过文件(主要关注proc文件)导出到用户空间,最后定义了一些宏以便于编程,减少重复代码。在分析序列文件接口实现的过程中,还连带涉及到一些应用陷阱和避免手段。序列文件接口
UNIX的世界里,文件是最普通的概念,所以用文件来作为内核和用户空间传递数据的接口也是再普通不过的事情,并且这样的接口对于shell也是相当友好的,方便管理员通过shell直接管理系统。由于伪文件系统proc文件系统在处理大数据结构(大于一页的数据)方面有比较大的局限性,使得在那种情况下进行编程特别别扭,很容易导致bug,所以序列文件接口被发明出来,它提供了更加友好的接口,以方便程序员。之所以选择序列文件接口这个名字,应该是因为它主要用来导出一条条的记录数据。为了能给大家一个具体形象的认识,我们首先来看一段用序列文件接口通过proc文件导出内核双向循环链接表的实例代码:
|
注:以上代码需要内核版本不小于2.6.23,小版本内核可以从2.6.23内核代码中抽取函数seq_list_start和seq_list_next。
上面的这段程序创建了一个结构体my_data的双向循环链表(head),并且通过my_data proc文件将其导出到用户空间,用户空间的程序除了可以通过对my_data的读操作来获取my_data链表中各个结构体中的value域的值之外,还能通过写(write)系统调用添加一个具有随机value值的my_data结构体到双向循环链表中(从链表的头部添加)。
gentux kernel # cat /proc/my_data value: 474615376 value: 474615632 value: 474615568 gentux kernel # echo 0 > /proc/my_data gentux kernel # cat /proc/my_data value: 758528120 value: 474615376 value: 474615632 value: 474615568 |
虽然,上面的那段代码行数不少,但是和输出相关的部分除了函数_seq_show外都是惯例代码,这种简便性是很直观的。相信对于双向循环链表数据结构的导出,读者们都能够照猫画虎写出自己的代码而毋须我再多言。
序列文件接口相关的两个重要数据结构为seq_file结构体和seq_operations表(表是延续ULK3的叫法,实际上就是只包含函数指针的结构体)。首先介绍seq_file结构体:
- buf: 序列文件对应的数据缓冲区,要导出的数据都是首先打印到这个缓冲区,然后才被拷贝到用户指定的用户空间缓冲区。
- size: 缓冲区的大小,默认为1个内存页面大小,随着需求会动态以2的级数倍扩张,比如:4k,8k,16k...
- from: 没有拷贝到用户空间的数据在buf中的起始偏移量。
- count: buf中没有被拷贝到用户空间的数据的字节数。
- index: 数据项的索引,和稍后提到的seq_operations表中的start, next操作中的pos项一致。
- version: 文件的版本。
- lock: 序列化对这个文件的并行操作
- op:指向稍后提到的seq_operations表。
- private: 指向文件的私有数据,是特例化一个序列文件的方法。
- start: 开始读数据项,通常需要在这个函数里面加锁,以防止并行访问数据。
- stop: 停止读数据项,和start相对,通常需要解锁。
- next:找到下一个要处理的数据项。
- show:打印数据项到临时缓冲区。
一些有用的全局函数:
- 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的时候创建一小块文件私有数据。
- seq_list_start:返回链表中的特定项。
- seq_list_start_head:在需要输出表格头的时候使用。
- seq_list_next: 返回链表中的下一项。
序列文件接口的内核实现
序列文件接口的实现比较简单易懂,出于完整性考虑,我姑且把核心函数seq_read拿来和读者一起解读:- 如果是第一次对序列文件调用读操作,那么内核数据缓冲区指针(buf)为空,这个时候申请一整页内存作为缓冲区。
- 如果内核缓冲区中尚且有数据,那么先用它们填补用户缓冲区。
- 如果用户缓冲区仍有空间,则按序调用start和show,填补内核缓冲区。如果当前的内核缓冲区不足以容纳一条记录,那么将缓冲区大小加倍后再次尝试,直到可以容纳至少一条记录。然后持续调用next和show,直到内核缓冲区无空间容纳新的整条记录后调用stop终止。
因为序列文件的缓冲区有自动扩张的功能,所以它更便于导出大于一页的数据结构,这也正是single_open/release函数的用武之地。
内核常用数据结构的导出
应用序列文件接口导出常用的面向记录的数据结构通常是非常简单的,下面我将给出内核常用的数据结构通过序列文件接口导出的思路。数组
数组通过其索引就能够随机地访问其中的任一元素,所以我们只要将start和next参数中的pos当成索引,返回对应元素的地址即可。双向循环链表
- start:跳过前pos个元素,返回即可。
- next:增加pos,返回下一个元素。
哈希表
哈希表有两种导出方法:- 以每个哈希桶为元素,这个时候哈希表退化为数组。
- 以每个哈希节点为元素,这个时候它退化为按桶连接起来的链表。
红黑树
因为Linux内核提供了按照类似链表的方式遍历红黑树的方法,所以导出它的方法和双向链接表类似。序列文件接口的帮助宏
实际上,应用序列文件接口进行编程,存在大量的重复代码,为了减少可恶的重复代码,也为了减少复制粘贴可能引入的bug,我写了这一套宏以简化大多数情况下的序列文件接口调用。具体宏代码如下:
|
应用起来比较简单,下面是用其重写过的上面的双向连接表导出代码:
|
是不是简单了不少了?其它数据结构的实例代码可以参考文后的附件。
参考资料:
文章出自:http://www.4ucode.com/Study/Topic/1554981