写点什么

Linux 设备驱动系列(九)——procfs 文件系统

  • 2024-05-01
    浙江
  • 本文字数:3285 字

    阅读完需:约 11 分钟

Linux设备驱动系列(九)——procfs文件系统

关注微信公众号:Linux 内核拾遗

文章来源:https://mp.weixin.qq.com/s/50EdD3YXTOALPeBG_3roBQ


Linux 内核提供了多种用户态和内核态的通信机制,本文将重点介绍 procfs 文件系统。其他的通信机制可以参考前文:


Linux设备驱动系列(八)——ioctl系统调用

1 procfs 介绍

procfs 是一种特殊的文件系统,用于提供关于正在运行的进程和系统内核的信息。在许多类 Unix 操作系统中,包括 Linux,procfs 被挂载在/proc 目录下。通过查看/proc 目录下的文件和子目录,可以获取有关系统中运行进程的各种信息,例如进程 ID、进程状态、内存使用情况、打开的文件等。


  • **/proc/meminfo**:系统内存信息。

  • **/proc/devices**:系统中已注册的字符设备和块设备的主设备号。

  • **/proc/modules **:内核中已插入的内核模块列表(类似于 lsmod 命令的输出)。

  • **/proc/iomem**:系统中的物理 RAM 和总线设备地址。

  • **/proc/ioports**:系统中的 I/O 端口地址 。

  • **/proc/interrupts**:已注册的中断请求号。

  • **/proc/softirqs**:已注册的软中断请求号。

  • **/proc/swaps**:当前活跃的交换区信息。

  • **/proc/kallsyms**:内核符号表,包括可加载模块中的符号。

  • **/proc/partitions**:系统中的块设备分区表信息。

  • **/proc/filesystems**:系统中支持的文件系统。

  • **/proc/cpuinfo**:系统 CPU 信息。


procfs 不是一个实际的文件系统,而是通过内核动态生成的一个虚拟文件系统,它允许用户空间进程访问内核信息。因此,procfs 的内容实际上是内核中数据结构的一种反映,而不是存储在磁盘上的文件。


procfs 中的文件通常是只读的,只在一些特殊情况下允许写入,例如禁用 nmi_watchdog:


echo 1 > /proc/sys/kernel/nmi_watchdog
复制代码


在内核模块调试的时候,procfs 文件系统非常有用,可以通过 procfs 将内核模块运行时的一些变量参数信息展示出来,以便定位问题。


也可以通过 procfs 往内核空间传达数据,因此存在两种类型的 proc 条目:


  • 只允许从内核读取数据的条目;

  • 允许从内核空间读取和写入数据的条目。

2 procfs 使用

可以通过头文件**linux/proc_fs.h**中定义的 API 来使用 procfs。

2.1 创建 procfs 目录

可以通过以下的 API 在/proc/*下面创建一个目录:


struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent);
复制代码


其中 parent 是当前要创建目录的父目录,NULL 则表示父目录是/proc/。

2.2 创建 procfs 条目

Linux v3.10 之后,可以通过下面的 API 来创建一个 procfs 条目:


struct proc_dir_entry *proc_create (const char *name, umode_t mode,                                    struct proc_dir_entry *parent,                                    const struct file_operations *proc_fops);
复制代码


其中 proc_fops 则是该 procfs 条目要关联的文件操作,当读写 proc 条目的时候会调用到相应的函数。


在 Linux v5.6 及以后的版本中,const struct file_operations *proc_fops 参数改成了 const struct proc_ops *proc_ops。

2.3 实现 procfs 操作函数

接下来就是要实现以下的 procfs 操作函数:


// Linux v3.10 ~ v5.5static struct file_operations proc_fops = {  .open = open_proc,  .read = read_proc,  .write = write_proc,  .release = release_proc};
// Linux v5.6及之后static struct proc_ops proc_fops = { .proc_open = open_proc, .proc_read = read_proc, .proc_write = write_proc, .proc_release = release_proc};
复制代码


这些操作函数的实现与前面介绍的文件操作类似,此处不再赘述。


Linux设备驱动系列(六)——文件操作

2.4 移除 procfs 条目

当设备驱动程序退出的时候,也需要相应地移除先前创建的 proc 条目:


void remove_proc_entry(const char *name, struct proc_dir_entry *parent);
复制代码


其中"parent/name"则指定了要移除的 proc 目录的父目录和条目名称。


也可以一次性移除整个 proc 目录及其下面的所有条目:


void proc_remove(struct proc_dir_entry *parent);
复制代码

3 procfs 完整示例

3.1 多内核版本适配

相比如系统调用接口,Linux 内核的 API 接口没有想象中的那么稳定,相反地,它是很容易发生变动的。作为设备驱动开发人员,为设备驱动程序适配多个内核版本是不可避免的事情(除非你只想让你的设备驱动运行在单一内核版本上)。


多内核版本适配最常用的实践方法是定义一个内核版本宏,根据不同的内核版本编写不同的代码片段(内核 API),最后在内核编译的时候根据实际运行的目标内核版本,将其作为编译参数传递进去。


例如可以这样定义内核版本宏 LINUX_KERNEL_VERSION:


  • v3.10:310

  • v5.6:506

  • v5.10:510

3.2 设备驱动代码

kernel_driver.c


#include <linux/kernel.h>#include <linux/init.h>#include <linux/module.h>#include <linux/kdev_t.h>#include <linux/fs.h>#include <linux/cdev.h>#include <linux/device.h>#include <linux/slab.h>#include <linux/proc_fs.h>#include <linux/err.h>
#ifndef LINUX_KERNEL_VERSION#define LINUX_KERNEL_VERSION 510#endif
int32_t value = 0;char buf[20] = "test procfs\n";static int len = 1;
dev_t dev = 0;static struct class *dev_class;static struct cdev my_cdev;static struct proc_dir_entry *parent;
static ssize_t read_proc(struct file *filp, char __user *buffer, size_t length, loff_t *offset){ if (len) len = 0; else { len = 1; return 0; }
if (copy_to_user(buffer, buf, 20)) pr_err("Data Send Error!\n");
return length;}
static ssize_t write_proc(struct file *filp, const char *buff, size_t len, loff_t *off){ if (copy_from_user(buf, buff, len)) pr_err("Data Write Error\n"); return len;}
#if (LINUX_KERNEL_VERSION > 505)static struct proc_ops proc_fops = { .proc_read = read_proc, .proc_write = write_proc,};#else // LINUX_KERNEL_VERSION > 505static struct file_operations proc_fops = { .read = read_proc, .write = write_proc,};#endif // LINUX_KERNEL_VERSION > 505
static struct file_operations fops = { .owner = THIS_MODULE,};
static int __init my_driver_init(void){ if ((alloc_chrdev_region(&dev, 0, 1, "my_dev")) < 0) return -1;
cdev_init(&my_cdev, &fops);
if ((cdev_add(&my_cdev, dev, 1)) < 0) goto r_class;
if (IS_ERR(dev_class = class_create(THIS_MODULE, "my_class"))) goto r_class;
if (IS_ERR(device_create(dev_class, NULL, dev, NULL, "my_device"))) goto r_device;
if (IS_ERR(parent = proc_mkdir("my_proc", NULL))) goto r_device; proc_create("my_entry", 0666, parent, &proc_fops);
return 0;
r_device: class_destroy(dev_class);r_class: unregister_chrdev_region(dev, 1); return -1;}
static void __exit my_driver_exit(void){ // remove_proc_entry("my_proc/my_entry", parent); proc_remove(parent); device_destroy(dev_class, dev); class_destroy(dev_class); cdev_del(&my_cdev); unregister_chrdev_region(dev, 1);}
module_init(my_driver_init);module_exit(my_driver_exit);
MODULE_LICENSE("GPL");MODULE_AUTHOR("feifei <feifei@gmail.com>");MODULE_DESCRIPTION("Simple Linux device driver");
复制代码

3.3 编译参数

可以通过在 Makefile 文件中声明 EXTRA_CFLAGS 来传达 gcc 编译参数:


Makefile


obj-m += kernel_driver.o KDIR = /lib/modules/$(shell uname -r)/build
EXTRA_CFLAGS += -DLINUX_KERNEL_VERSION="510" all: make -C $(KDIR) M=$(shell pwd) modules clean: make -C $(KDIR) M=$(shell pwd) clean
复制代码

3.4 运行演示


如果编译参数中内核版本设置错误,则会遇到如下的编译报错:




关注微信公众号:Linux 内核拾遗

文章来源:https://mp.weixin.qq.com/s/50EdD3YXTOALPeBG_3roBQ


发布于: 刚刚阅读数: 6
用户头像

聚沙成塔 2023-01-12 加入

分享Linux内核开发相关的编程语言、开发调试工具链、计算机组成及操作系统内核知识、Linux社区最新资讯等

评论

发布
暂无评论
Linux设备驱动系列(九)——procfs文件系统_文件系统_Linux内核拾遗_InfoQ写作社区