欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

嵌入式(驱动-基础):25---调试技术之(/proc文件系统信息接口:read_proc、get_info、create_proc_read_entry、remove_proc_entry)

程序员文章站 2022-05-09 21:09:32
...
  • 首先声明:
    • ①我们并不鼓励在/proc文件系统下添加我们自己的文件来记录内核信息,因为/proc文件凶已经提供了很多的信息,此文是用来介绍知识的
    • ②我们建议新的代码通过sysfs来向外界导出信息,我们将在后面讲到

一、/proc文件系统介绍

  • /proc文件系统是一种特殊的、由软件创建的文件系统,内核使用它向外界导出信息
  • /proc下面的每个文件都绑定于一个内核函数,用户读取其中的文件时,该函数动态地生成文件的“内容”。例如,/proc/modules文件记录了系统中所有装载的内核,这个文件也相应的绑定了一个函数,我们通过lsmod查看文件的内容时,这些内容就是与/proc/modules文件绑定的函数返回的
  • 在Linux系统中对/proc的使用很频繁,例如ps、top、uptime等工具都是通过读取/proc目录下的文件来获取信息的。当然,有些设备驱动程序也通过/proc导出信息,而我们自己的驱动程序当然也这么做。因此/proc文件系统是动态地,所以驱动程序模块可以在任何时候添加或删除其中的入口项
  • 头文件:所有使用/proc的模块都必须包含<linux/proc_fs.h>

二、在/proc中实现文件(read_proc函数)

  • 原理:
    • ①为创建一个只读的/proc文件,驱动程序必须实现一个函数,用来在读取文件时生成数据。当某个进程读取这个文件时(使用read系统调用),读取请求会通过这个函数发送到驱动模块
    • ②在某个进程读取我们的/proc文件时,内核会分配一个内存页(PAGE_SIZE字节的内存块),驱动程序可以将数据通过这个内存页返回到用户空间

嵌入式(驱动-基础):25---调试技术之(/proc文件系统信息接口:read_proc、get_info、create_proc_read_entry、remove_proc_entry)

#include <linux/proc_fs.h>
int (*read_proc)(char *page,char **start,off_t offset,int count,
    int *eof,void *data);

参数:

  • page:用来记录数据的缓冲区,需要调用者传入
  • start:用来指示要返回给用户的数据写到内存页的哪个位置(用法比较复杂,可以帮助实现大(大于一个内存页)的/proc文件)
  • offset:用来指定返回给用户的数据从内存页的多少偏移处开始返回(受start参数影响)
  • count:返回数据的长度
  • eof:当没有数据可返回时,驱动程序必须设置这个参数,这个参数指向一个整型数
  • data:提供给驱动程序的专用数据指针,可用于内部记录

返回值:

  • 成功返回存放到内存页缓冲区的字节数,这一点与read对其他类型文件的处理相同

start参数介绍

  • start参数有些特别,其用来指示要返回给用户的数据保存在内存页的什么位置
  • 在我们的read_proc方法被调用时,*star的初始值为NULL
  • *start取值:
    • 如果*start为空,内核将假定数据保存在内存页偏移量0的地方,也就是说,内存将对read_proc作如下简单假定:该函数将虚拟文件的整个内容放到了内存页,并同时忽略offset参数
    • 如果将*star设置为非空,内核将认为由*start指定的数据是offset指定的偏移量处的数据,可直接返回给用户
  • 通常,返回少量数据的简单read_proc方法可忽略start参数,复杂的read_proc方法会将*start设置为页面,并将所请求偏移量处的数据方法内存页中

start参数解决的另一个问题

  • 关于/proc文件还有一个主要的问题,这也是start意图要解决的
  • 有时,在连续的read调用之间,内核数据结构的ASCII表述会发生变化,以至于读取进程发现前后两次调用所获得的数据不一致
  • 如果把*start设为一个小的整数值,那么调用程序可以利用它来增加filp->f_pos的值,而不依赖于返回的数据量,因此也就使f_ops成为read_proc过程的一个内部记录值
  • 例如,如果read_proc函数从一个大的结构数组返回数据,并且这些结构的前5个已经在第一次调用中返回,那么可将*start设置为5。下次调用中这个值将作为偏移量,驱动程序也就知道应该从数组的第6个结构开始返回数据。这种方法被它的作者称为“hack”(可以在Linux目录树的*fs*proc/generic.c中看到)

演示案例

  • 下面是scull设备的read_proc函数的简单实现,此函数假定不会有生成多于一页的数据的请求,因此忽略了start和offset值,但是要小心不要超出缓冲区:

嵌入式(驱动-基础):25---调试技术之(/proc文件系统信息接口:read_proc、get_info、create_proc_read_entry、remove_proc_entry)

其他在/proc实现文件的方法

  • seq_file接口:在后面介绍
  • 老的接口:get_info,已经不建议使用了(参数与read_proc一样,只是没有eof和data参数)
#include <linux/proc_fs.h>
int (*get_info)(char *page,char **start,off_t offset,int count);

三、创建自己的/proc文件(create_proc_entry)

#include <linux/proc_fs.h>
struct proc_dir_entry *create_proc_read_entry(const char *name,
    mode_t mode,struct proc_dir_enrty *base
    read_proc_t *read_proc,void *data);
  • 功能:上面定义好了read_proc函数,就需要把它与一个/proc入口项(创建的文件)连接起来,这个过程通过create_proc_read_entry实现

参数:

  • name:要在/proc下创建的文件名称
  • mode:该文件的保护掩码(可传入0表示系统默认值)
  • base:创建的文件所在的目录(填NULL,该文件将创建在/proc的根目录)
  • read_proc:实现该文件的read_proc函数(就是上面我们自己定义的read_proc函数)
  • data:内核会忽略该参数,但是会将该参数传递给read_proc

base参数进一步介绍

  • base参数可用来在/proc下创建完整地目录层次结构。不过请注意,将入口项置于/proc的子目录中有更为简单的方法,即把目录名称作为入口项名称的一部分——只要目录本身已经存在
  • 例如:有个经常被忽略的约定,要求把设备驱动程序对应的proc入口项转移到/proc目录的子目录driver/中。scull可以简单地指定它的入口项名称为driver/scullmem,从而把它的proc文件方法放到子目录中

演示案例:

  • 下面是scull调用该函数创建/proc文件的代码(下面代码在/proc目录下创建一个成为scullmem的文件,并默认具有全局可读权限设置):

嵌入式(驱动-基础):25---调试技术之(/proc文件系统信息接口:read_proc、get_info、create_proc_read_entry、remove_proc_entry)

四、删除自己的/proc文件(remove_proc_entry)

  • 卸载模块时,/proc的入口项(文件)也应该被删除,remove_proc_entry就是用来撤销create_proc_read_entry所做的工作的
  • 如果删除入口项文件失败,将导致未预期的调用,如果模块已被卸载,内核会崩溃
  • 删除入口项文件的常见错误现象:
    • 删除调用可能在文件正在被使用时发生,因为/proc入口项不存在关联的所有者,因此对这些文件的使用并不会作用到模块的引用计数上。在移除模块时,执行sleep 100 < /proc/filename命令就可以触发这个问题
    • 可能会出现使用同一名字注册两个入口项的现象。内核新人驱动程序,因此不会检查某个名称是否已被注册,因此如果不小心,将可能导致两个或多个入口项具有相同的名称。由于入口项无法区分,因此不论是访问这些入口项的时候还是调用remove_proc_entry的时候,都会出现问题
  • 例如:下面是scull模块的remove_proc_entry函数

嵌入式(驱动-基础):25---调试技术之(/proc文件系统信息接口:read_proc、get_info、create_proc_read_entry、remove_proc_entry)