关注微信公众号: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 <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
评论