最近在做测试,怎么实现的呢?通过实现了一个KO,然后KO加载后就会注册一个proc节点。然后通过echo给这个节点进行输入指令,调用对应的文件节点,最后实现测试的函数调用。
这里对proc这个东西一直感觉很神奇,很多cat、cpu等信息都可以从里面获取到。于是这里来看看这个文件系统。
• (1) proc是虚拟文件系统,虚拟的意思就是proc文件系统里的文件不对应硬盘上任何文件,我们用去查看proc目录下的文件大小都是零;
• (2) proc文件系统是开放给上层了解内核运行状态的窗口,通过读取proc系统里的文件,可以知道内核中一些重要数据结构的数值,从而知道内核的运行情况,也可以方便调试内核和应用程序;
• (3) proc文件系统的思路:在内核中构建一个虚拟文件系统/proc,内核运行时将内核中一些关键的数据结构以文件的方式呈现在/proc目录中的一些特定文件中,这样相当于将不可见的内核中的数据结构以可视化的方式呈现给内核的开发者。
• (4) proc文件系统是一种无存储的文件系统,当读其中的文件时,其内容动态生成,当写文件时,文件所关联的写函数被调用。每个proc文件都关联的字节特定的读写函数,因而它提供了另外的一种和内核通信的机制:内核部件可以通过该文件系统向用户空间提供接口来提供查询信息、修改软件行为,因而它是一种比较重要的特殊文件系统。
• (1)proc文件系统主要是用来调试内核,在内核运行时可以知道内核中一些重要的数据结构的值,一般都是读很少写;
• (2)proc文件系统出现的比sys文件系统早,proc文件系统的目录结构比较乱,在proc文件系统下面有很多文件夹,比如一个进程就有一个文件夹,现在内核越来越复杂,支持的设备类型也越来越多,显得很混乱;于是又开发出了sys系统,sys系统可以说是proc的升级,将来用sys系统会是主流;
• (3)proc文件系统和sys文件系统都是虚拟系统,并且有对应关系,比如"/proc/misc"对应于"sys/class/misc"下面的设备,都是描述misc类设备的;
Linux系统上的/proc目录是一种文件系统,即proc文件系统(procfs),它以文件系统的方式为用户提供访问系统内核数据的操作接口。
proc文件系统是一种内核和内核模块用来向进程(process)发送信息的机制,因此被称为proc。
与其它常见的文件系统不同的是,proc是一种伪文件系统(也即虚拟文件系统),它只存在于内存当中,因此它会在系统启动时创建并挂载到/proc目录,在系统关闭时卸载并释放。
下面是设备上/proc的挂载信息。
$ mount | grep proc
proc on /proc type proc (rw,relatime)
proc文件系统存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看系统和进程的信息,或者改变内核的运行状态,因此可以把它视为Linux内核开放给用户的控制和信息中心。实际上,proc文件系统是内核空间和用户空间之间的一种通信媒介。
proc虚拟文件系统也可以创建虚拟文件节点,实现用户空间与内核空间的交互。
在驱动中创建节点,可以实现对硬件的控制。proc_create函数原型(在kernel-3.10/include/linux/proc_fs.h文件)如下所示:
static inline struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct file_operations *proc_fops)
{
return proc_create_data(name, mode, parent, proc_fops, NULL);
}
• name:表示你要创建的设备节点的名称,可随意命名即可;
• mode:表示节点的权限,一般赋值0644;
• parent:表示父节点,如果直接在proc目录创建节点,直接赋值NULL即可;
• proc_fops:表示与节点相关联的file_operations;
如下代码是我实现的一个test程序,可供参考学习proc_create的使用:
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/ioctl.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/string.h>
#include <linux/wait.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/pinctrl/consumer.h>
#include <linux/of_gpio.h>
#include <linux/delay.h>
#include <linux/types.h>
#include <linux/proc_fs.h>
#define BUFSIZE 1024
static char *buf;
static unsigned int len;
/***********************
* file_operations->open
* 无操作
***********************/
static int test_proc_open(struct inode *inode, struct file *file)
{
return 0;
}
/************************
* file_operations->read
* 可以在adb工具进入机器的pro目录,执行adb shell && cd proc && cat tets_rw,
* 即可读出节点test_rw的内容是12345
************************/
static ssize_t test_proc_read(struct file *file,
char __user *buffer,size_t count, loff_t *f_pos)
{
if(*f_pos > 0)
return 0;
printk("---start read---\n");
printk("the string is >>>>> %s\n", buf);
if(copy_to_user(buffer, buf, len))
return -EFAULT;
*f_pos = *f_pos + len;
return len;
}
/************************
* file_operations->write
* 可以在adb工具进入机器的pro目录,
* 执行adb shell && cd proc && echo 12345 > tets_rw,即可把12345写入节点test_rw
************************/
static ssize_t test_proc_write(struct file *file, const char __user *buffer,
size_t count, loff_t *f_pos)
{
if(count <= 0)
return -EFAULT;
printk("---start write---\n");
len = count > BUFSIZE ? BUFSIZE : count;
// kfree memory by kmalloc before
if(buf != NULL)
kfree(buf);
buf = (char*)kmalloc(len+1, GFP_KERNEL);
if(buf == NULL)
{
printk("test_proc_create kmalloc fail!\n");
return -EFAULT;
}
//memset(buf, 0, sizeof(buf));
memset(buf, 0, len+1);
if(copy_from_user(buf, buffer, len))
return -EFAULT;
printk("test_proc_write writing :%s",buf);
return len;
}
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = test_proc_open,
// .release = single_release,
.read = test_proc_read,
// .llseek = seq_lseek,
.write = test_proc_write,
};
static int __init test_init(void)
{
struct proc_dir_entry* file;
//创建proc文件并关联file_operations
file = proc_create("test_rw", 0644, NULL, &test_fops);
if (!file)
return -ENOMEM;
printk("test_rw init success!\n");
return 0;
}
static void __exit test_exit(void)
{
remove_proc_entry("test_rw", NULL);
printk("test_exit\n");
}
module_init(test_init);
module_exit(test_exit);
MODULE_AUTHOR("caizd");
MODULE_DESCRIPTION("Proc_create Test Driver");
MODULE_LICENSE("GPL");
可以将上面的代码编译成一个ko,然后编近kernel,然后验证。
root@inwatch_portal:/ # cd proc
cd proc
root@inwatch_portal:/proc # echo 12345 > test_rw
echo 12345 > test_rw
root@inwatch_portal:/proc # cat test_rw
cat test_rw
12345
下面来跟着前辈来学习一下更进一步的关于proc源码的东西。
一直以为PROC文件系统很是晦涩难懂,平时仅仅是使用它,不愿意去触碰内核中的具体实现。今天突发奇想,想看看里面究竟是怎么实现的,结果……真是大跌眼镜,没想到里面并不复杂
关于PROC文件系统的功能以及在Linux中的地位就不多说了,在用户空间和内核空间交互的界面也扮演者举足轻重的地位。
我们今天就从proc_create函数开始,看看其中的实现。该函数会创建一个PROC entry,用户可以通过对文件系统中的该文件,和内核进行数据的交互。
static inline struct proc_dir_entry *proc_create(
const char *name, umode_t mode, struct proc_dir_entry *parent,
const struct file_operations *proc_fops)
{
return proc_create_data(name, mode, parent, proc_fops, NULL);
}
简要介绍下参数:
• name:名字
• mod:模式
• parent:父entry,为NULL的话,默认父entry是/proc
struct proc_dir_entry proc_root = {
.low_ino = PROC_ROOT_INO,
.namelen = 5,
.mode = S_IFDIR | S_IRUGO | S_IXUGO,
.nlink = 2,
.count = ATOMIC_INIT(1),
.proc_iops = &proc_root_inode_operations,
.proc_fops = &proc_root_operations,
.parent = &proc_root,
.name = "/proc",
};
• proc_fops:操作函数表
函数返回一个proc_dir_entry。可以看到proc_create中直接调用了proc_create_data,而该函数主要完成2个功能
• 1、调用__proc_create完成具体proc_dir_entry的创建。
• 2、调用proc_register把entry注册进系统。
struct proc_dir_entry *proc_create_data(const char *name, umode_t mode,
struct proc_dir_entry *parent,
const struct file_operations *proc_fops,
void *data)
{
struct proc_dir_entry *pde;
if ((mode & S_IFMT) == 0)
mode |= S_IFREG;
if (!S_ISREG(mode)) {
WARN_ON(1); /* use proc_mkdir() */
return NULL;
}
if ((mode & S_IALLUGO) == 0)
mode |= S_IRUGO;
pde = __proc_create(&parent, name, mode, 1);
if (!pde)
goto out;
pde->proc_fops = proc_fops;
pde->data = data;
if (proc_register(parent, pde) < 0)
goto out_free;
return pde;
out_free:
kfree(pde);
out:
return NULL;
}
先看proc_dir_entry的创建,这里通过__proc_create函数,其实该函数内部也很简单,就是为entry分配了空间,并对相关字段进行设置,主要包含name,namelen,mod,nlink等。
创建好后,就设置操作函数proc_fops和data。然后就调用proc_register进行注册,
static int proc_register(struct proc_dir_entry * dir, struct proc_dir_entry * dp)
{
struct proc_dir_entry *tmp;
int ret;
ret = proc_alloc_inum(&dp->low_ino);
if (ret)
return ret;
/*如果是 目录*/
if (S_ISDIR(dp->mode)) {
dp->proc_fops = &proc_dir_operations;
dp->proc_iops = &proc_dir_inode_operations;
dir->nlink++;
/*如果是链接*/
} else if (S_ISLNK(dp->mode)) {
dp->proc_iops = &proc_link_inode_operations;
/*如果是文件*/
} else if (S_ISREG(dp->mode)) {
BUG_ON(dp->proc_fops == NULL);
dp->proc_iops = &proc_file_inode_operations;
} else {
WARN_ON(1);
return -EINVAL;
}
spin_lock(&proc_subdir_lock);
for (tmp = dir->subdir; tmp; tmp = tmp->next)
if (strcmp(tmp->name, dp->name) == 0) {
WARN(1, "proc_dir_entry '%s/%s' already registered\n",
dir->name, dp->name);
break;
}
/*子dir链接成链表,且子dir中含有父dir的指针*/
dp->next = dir->subdir;
dp->parent = dir;
dir->subdir = dp;
spin_unlock(&proc_subdir_lock);
return 0;
}
函数首先分配一个inode number,然后根据entry的类型对其进行操作函数赋值,主要分为目录、链接、文件。
这里我们只关注文件,文件的操作函数一般由用户自己定义,即上面我们设置的ops,这里仅仅是设置inode操作函数表,设置成了全局的proc_file_inode_operations,然后插入到父目录的子文件链表中,注意是头插法。基本结构如下,其中每个子节点都有指向父节点的指针。
其实创建entry的过程就这么简单,由于PROC也是一种文件系统,所以可以和ext2/ext3等文件系统一样,作为一个实体文件系统,通过VFS给用户提供统一的接口。
相对于实体的文件系统而言,PROC文件系统要简单的多。因为其不需要管理具体磁盘上的文件,不需要和硬件打交道。
正常情况下用户发起文件操作流程为:用户层序->系统调用->VFS层->具体文件系统->磁盘驱动程序。
而针对PROC文件系统而言,其不需要和磁盘驱动打交道,最低层的部分就是操作系统各个子模块提供的操作函数表。
这个就需要根据不同的模块进行不同的操作了,所以都是某个模块自己通过PROC的接口,向PROC注册内容,针对我们普通用户添加的entry,最低层的操作自然是我们注册的ops函数表了。
前面我们看了这个怎么玩,这里我们来看看怎么用?
在Linux系统中,“/proc”文件系统十分有用,它被内核用于向用户导出信息。“/proc”文件系统是一个虚拟文件系统,通过它可以在Linux内核空间和用户空间之间进行通信。在/proc文件系统中,我们可以将对虚拟文件的读写作为与内核中实体进行通信的一种手段,与普通文件不同的是,这些虚拟文件的内容都是动态创建的。
“/proc”下的绝大多数文件是只读的,以显示内核信息为主。但是“/proc”下的文件也并不是完全只读的,若节点可写,还可用于一定的控制或配置目的,例如前面介绍的写/proc/sys/kernel/printk可以改变printk()的打印级别。
Linux系统的许多命令本身都是通过分析“/proc”下的文件来完成的,如ps、top、uptime和free等。例如,free命令通过分析/proc/meminfo文件得到可用内存信息,下面显示了对应的meminfo文件和free命令的结果。
• 1.meminfo文件[root@localhost proc]# cat meminfo
MemTotal: 29516 kB
MemFree: 1472 kB
Buffers: 4096 kB
Cached: 12648 kB
SwapCached: 0 kB
Active: 14208 kB
Inactive: 8844 kB
HighTotal: 0 kB
HighFree: 0 kB
LowTotal: 29516 kB
LowFree: 1472 kB
SwapTotal: 265064 kB
SwapFree: 265064 kB
Dirty: 20 kB
Writeback: 0 kB
Mapped: 10052 kB
Slab: 3864 kB
CommitLimit: 279820 kB
Committed_AS: 13760 kB
PageTables: 444 kB
VmallocTotal: 999416 kB
VmallocUsed: 560 kB
VmallocChunk: 998580 kB
•
1. free命令
[root@localhost proc]# free
total used free shared buffers cached
Mem: 29516 28104 1412 0 4100 12700
-/+ buffers/cache: 11304 18212
Swap: 265064 0 265064
在Linux 3.9以及之前的内核版本中,可用如下函数创建“/proc”节点:
struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,
struct proc_dir_entry *parent);
struct proc_dir_entry *create_proc_read_entry(const char *name, mode_t mode,
struct proc_dir_entry *base, read_proc_t *read_proc, void * data);
create_proc_entry()函数用于创建“/proc”节点,而create_proc_read_entry()调用create_proc_entry()创建只读的“/proc”节点。参数name为“/proc”节点的名称,parent/base为父目录的节点,如果为NULL,则指“/proc”目录,read_proc是“/proc”节点的读函数指针。当read()系统调用在“/proc”文件系统中执行时,它映像到一个数据产生函数,而不是一个数据获取函数。
下列函数用于创建“/proc”目录:
struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent);
结合create_proc_entry()和proc_mkdir(),代码清单21.5中的程序可用于先在/proc下创建一个目录procfs_example,而后在该目录下创建一个文件example_file。代码清单21.5 proc_mkdir()和create_proc_entry()函数使用范例
1 /* 创建/proc下的目录 */
2 example_dir = proc_mkdir("procfs_example", NULL);
3 if (example_dir == NULL) {
4 rv = -ENOMEM;
5 goto out;
6 }
7
8 example_dir->owner = THIS_MODULE;
9
10 /* 创建一个/proc文件 */
11 example_file = create_proc_entry("example_file", 0666, example_dir);
12 if (example_file == NULL) {
13 rv = -ENOMEM;
14 goto out;
15 }
16
17 example_file->owner = THIS_MODULE;
18 example_file->read_proc = example_file_read;
19 example_file->write_proc = example_file_write;
作为上述函数返回值的proc_dir_entry结构体包含了“/proc”节点的读函数指针(read_proc_tread_proc)、写函数指针(write_proc_twrite_proc)以及父节点、子节点信息等。/proc节点的读写函数的类型分别为:
typedef int (read_proc_t)(char *page, char **start, off_t off,
int count, int *eof, void *data);
typedef int (write_proc_t)(struct file *file, const char __user *buffer,
unsigned long count, void *data);
读函数中page指针指向用于写入数据的缓冲区,start用于返回实际的数据并写到内存页的位置,eof是用于返回读结束标志,offset是读的偏移,count是要读的数据长度。start参数比较复杂,对于/proc只包含简单数据的情况,通常不需要在读函数中设置*start,这意味着内核将认为数据保存在内存页偏移0的地方。
写函数与file_operations中的write()成员函数类似,需要一次从用户缓冲区到内存空间的复制过程。在Linux系统中可用如下函数删除/proc节点:
void remove_proc_entry(const char *name, struct proc_dir_entry *parent);
在Linux系统中已经定义好的可使用的/proc节点宏包括:proc_root_fs(/proc)、proc_net(/proc/net)、proc_bus(/proc/bus)、proc_root_driver(/proc/driver)等,proc_root_fs实际上就是NULL。
代码清单21.6所示为一个简单的“/proc”文件系统使用范例,这段代码在模块加载函数中创建/proc/test_dir目录,并在该目录中创建/proc/test_dir/test_rw文件节点,在模块卸载函数中撤销“/proc”节点,而/proc/test_dir/test_rw文件中只保存了一个32位的整数。
1 #include <linux/module.h>
2 #include <linux/kernel.h>
3 #include <linux/init.h>
4 #include <linux/proc_fs.h>
5
6 static unsigned int variable;
7 static struct proc_dir_entry *test_dir, *test_entry;
8
9 static int test_proc_read(char *buf, char **start, off_t off, int count,
10 int *eof, void *data)
11 {
12 unsigned int *ptr_var = data;
13 return sprintf(buf, "%u\n", *ptr_var);
14 }
15
16 static int test_proc_write(struct file *file, const char *buffer,
17 unsigned long count, void *data)
18 {
19 unsigned int *ptr_var = data;
20
21 *ptr_var = simple_strtoul(buffer, NULL, 10);
22
23 return count;
24 }
25
26 static __init int test_proc_init(void)
27{
28 test_dir = proc_mkdir("test_dir", NULL);
29 if (test_dir) {
30 test_entry = create_proc_entry("test_rw", 0666, test_dir);
31 if (test_entry) {
32 test_entry->nlink = 1;
33 test_entry->data = &variable;
34 test_entry->read_proc = test_proc_read;
35 test_entry->write_proc = test_proc_write;
36 return 0;
37 }
38 }
39
40 return -ENOMEM;
41 }
42 module_init(test_proc_init);
43
44 static __exit void test_proc_cleanup(void)
45{
46 remove_proc_entry("test_rw", test_dir);
47 remove_proc_entry("test_dir", NULL);
48 }
49 module_exit(test_proc_cleanup);
50
51 MODULE_AUTHOR("Barry Song <baohua@kernel.org>");
52 MODULE_DESCRIPTION("proc exmaple");
53 MODULE_LICENSE("GPL v2");
上述代码第21行调用的simple_strtoul()用于将用户输入的字符串转换为无符号长整数,第3个参数10意味着转化方式是十进制。
编译上述简单的proc.c为proc.ko,运行insmod proc.ko加载该模块后,“/proc”目录下将多出一个目录test_dir,该目录下包含一个test_rw,ls–l的结果如下:
$ ls -l /proc/test_dir/test_rw
-rw-rw-rw- 1 root root 0 Aug 16 20:45 /proc/test_dir/test_rw
测试/proc/test_dir/test_rw的读写:
$ cat /proc/test_dir/test_rw
0
$ echo 111 > /proc/test_dir/test_rw
$ cat /proc/test_dir/test_rw
说明我们上一步执行的写操作是正确的。在Linux 3.10及以后的版本中,“/proc”的内核API和实现架构变更较大,create_proc_entry()、create_proc_read_entry()之类的API都被删除了,取而代之的是直接使用proc_create()、proc_create_data()API。同时,也不再存在read_proc()、write_proc()之类的针对proc_dir_entry的成员函数了,而是直接把file_operations结构体的指针传入proc_create()或者proc_create_data()函数中,其原型为:
static inline struct proc_dir_entry *proc_create(
const char *name, umode_t mode, struct proc_dir_entry *parent,
const struct file_operations *proc_fops);
struct proc_dir_entry *proc_create_data(
const char *name, umode_t mode, struct proc_dir_entry *parent,
const struct file_operations *proc_fops, void *data);
我们把代码清单21.6的范例改造为同时支持Linux 3.10以前的内核和Linux3.10以后的内核。改造结果如代码清单21.7所示。#if LINUX_VERSION_CODE<KERNEL_VERSION(3,10,0)中的部分是旧版本的代码,与21.6相同,所以省略了。代码清单21.7 支持Linux 3.10以后内核的/proc文件系统使用范例
1 #include <linux/module.h>
2 #include <linux/kernel.h>
3 #include <linux/init.h>
4 #include <linux/version.h>
5 #include <linux/proc_fs.h>
6 #include <linux/seq_file.h>
7
8 static unsigned int variable;
9 static struct proc_dir_entry *test_dir, *test_entry;
10
11 #if LINUX_VERSION_CODE < KERNEL_VERSION(3, 10, 0)
12 ...
13 #else
14 static int test_proc_show(struct seq_file *seq, void *v)
15 {
16 unsigned int *ptr_var = seq->private;
17 seq_printf(seq, "%u\n", *ptr_var);
18 return 0;
19 }
20
21 static ssize_t test_proc_write(struct file *file, const char __user *buffer,
22 size_t count, loff_t *ppos)
23 {
24 struct seq_file *seq = file->private_data;
25 unsigned int *ptr_var = seq->private;
26
27 *ptr_var = simple_strtoul(buffer, NULL, 10);
28 return count;
29 }
30
31 static int test_proc_open(struct inode *inode, struct file *file)
32 {
33 return single_open(file, test_proc_show, PDE_DATA(inode));
34 }
35
36 static const struct file_operations test_proc_fops =
37 {
38 .owner = THIS_MODULE,
39 .open = test_proc_open,
40 .read = seq_read,
41 .write = test_proc_write,
42 .llseek = seq_lseek,
43 .release = single_release,
44 };
45 #endif
46
47 static __init int test_proc_init(void)
48 {
49 test_dir = proc_mkdir("test_dir", NULL);
50 if (test_dir) {
51 #if LINUX_VERSION_CODE < KERNEL_VERSION(3, 10, 0)
52 ...
53 #else
54 test_entry = proc_create_data("test_rw",0666, test_dir, &test_proc_fops, &variable);
55 if (test_entry)
56 return 0;
57 #endif
58 }
59
60 return -ENOMEM;
61}
62 module_init(test_proc_init);
63
64 static __exit void test_proc_cleanup(void)
65{
66 remove_proc_entry("test_rw", test_dir);
67 remove_proc_entry("test_dir", NULL);
68 }
69 module_exit(test_proc_cleanup);
感谢下面前辈的优秀文章与书籍
• https://zhuanlan.zhihu.com/p/584749553
• https://www.elecfans.com/emb/202210101902598.html
• https://zhuanlan.zhihu.com/p/584749553
• https://zhuanlan.zhihu.com/p/557870063
• https://www.cnblogs.com/ck1020/p/7475729.html
• 《Linux设备驱动开发详解》