掘金 后端 ( ) • 2024-05-01 14:47

关注微信公众号: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.5
static 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 > 505
static 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 <[email protected]>");
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 运行演示

image-20240501131527354

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

image-20240501131637849

image-20240501131711182

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

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