关注微信公众号:Linux内核拾遗
Linux内核提供了多种用户态和内核态的通信机制,本文将重点介绍procfs文件系统。其他的通信机制可以参考前文:
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
};
这些操作函数的实现与前面介绍的文件操作类似,此处不再赘述。
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 运行演示
如果编译参数中内核版本设置错误,则会遇到如下的编译报错:
关注微信公众号:Linux内核拾遗