本博客之前的文章中多次涉及到/proc文件系统,下面的几条命令都在曾经的文章中出现过:

cat /proc/interrupts

cat /proc/devices

cat /proc/kallsyms | grep super_blocks

第一条命令用于查看系统内已注册的中断信息,包括中断号、已接受的手段请求和驱动器名称等;第二条命令用于查看系统内已注册的字符设备和块设备信息,包括设备号和设备名称;第三条命令用于在内核符号表中检索super_blocks符号的的地址,kallsyms文件包括内核中所有的标示符及其地址。

1.概述

proc即process的缩写,最初的proc文件系统只是存放进程的相关信息。但现在的/proc文件系统除此之外还包含系统的状态信息和配置信息。

通过ls命令就可以查看/proc文件系统所包含的内容。

edsionte@edsionte-desktop:/proc$ ls

1 1290 1469 1541 1627 19612 29 49 9 dri mdstat sys

10 13 1471 1544 1630 19613 3 5 908 driver meminfo sysrq-trigger

1013 1301 1474 1548 1632 19629 30 50 913 edsionte_procfs misc sysvipc

…………

其中以数字为名的目录即为系统中正在运行的进程信息,数字即为进程的pid。比如我们可以进入init进程的目录,查看它的地址空间:

edsionte@edsionte-desktop:/proc/1$ sudo cat maps

[sudo] password for edsionte:

00110000-00263000 r-xp 00000000 08:07 704702 /lib/tls/i686/cmov/libc-2.11.1.so

00263000-00264000 ---p 00153000 08:07 704702 /lib/tls/i686/cmov/libc-2.11.1.so

00264000-00266000 r--p 00153000 08:07 704702 /lib/tls/i686/cmov/libc-2.11.1.so

00266000-00267000 rw-p 00155000 08:07 704702 /lib/tls/i686/cmov/libc-2.11.1.so

00267000-0026a000 rw-p 00000000 00:00 0

0026a000-00272000 r-xp 00000000 08:07 704713 /lib/tls/i686/cmov/libnss_nis-2.11.1.so

00272000-00273000 r--p 00007000 08:07 704713 /lib/tls/i686/cmov/libnss_nis-2.11.1.so

00273000-00274000 rw-p 00008000 08:07 704713 /lib/tls/i686/cmov/libnss_nis-2.11.1.so

00471000-0048b000 r-xp 00000000 08:07 1048610 /sbin/init

…………

除了查看进程的相关信息,我们还可以通过打印相关文件来查看系统的当前运行状态。比如查看当前内存的使用情况:

edsionte@edsionte-desktop:/proc$ cat meminfo

MemTotal: 961368 kB

MemFree: 145264 kB

Buffers: 31648 kB

Cached: 297716 kB

SwapCached: 14436 kB

…………

总之,/proc文件系统相当于内核的一个快照,该目录下的所有信息都是动态的从正在运行的内核中读取。

基于这种原因,/proc文件系统就成为了用户和内核之间交互的接口。一方面,用户可以从/proc文件系统中读取很多内核释放出来的信息;另一方面,内核也可以在恰当的时候从用户那里得到输入信息,从而改变内核的相关状态和配置。

相比传统的文件系统,/proc是一种特殊的文件系统,即虚拟文件系统。这里的虚拟是强调/proc文件系统下的所有文件都存在于内存中而不是磁盘上。也就是说/proc文件系统只占用内存空间,而不占用系统的外存空间。

2.用户态和内核态之间的数据通信

既然内核的数据以/proc文件系统的形式呈现给用户,也就是说内核的信息以文件的形式存在于该文件系统中,那么/proc文件系统就应当提供一组接口对其内的文件进行读写操作。接下来我们以一个实际的内核模块程序easyProc.c为例,说明/proc文件系统的常用接口。该程序中依次创建了几个虚拟文件,然后在用户态对这些文件进行读写测试。

2.0数据结构

每个虚拟文件都对应一个proc_dir_entry类型的数据结构,该结构具体定义如下:

struct proc_dir_entry {

const char *name;// virtual file name

mode_t mode;// mode permissions

uid_t uid;// File's user id

gid_t gid;// File's group id

struct inode_operations *proc_iops;// Inode operations functions

struct file_operations *proc_fops;// File operations functions

struct proc_dir_entry *parent;// Parent directory

...

read_proc_t *read_proc;// /proc read function

write_proc_t *write_proc;// /proc write function

void *data;// Pointer to private data

atomic_t count;// use count

...

};

除了保存该虚拟文件的基本信息外,该结构中还有read_proc和write_proc两个字段,下文中将有详细说明。

2.1创建目录

/proc文件系统中创建一个目录对应的函数接口如下:

struct proc_dir_entry *proc_mkdir(const char *name,struct proc_dir_entry *parent);

其中name为要创建的目录名;parent为这个目录的父目录,当要创建的目录位于/proc下时此参数为空。比如我们使用该函数在/proc下创建一个目录edsionte_procfs。

#define MODULE_NAME "edsionte_procfs"

struct proc_dir_entry *example_dir;

example_dir = proc_mkdir(MODULE_NAME, NULL);

if (example_dir == NULL) {

rv = -ENOMEM;

goto out;

}

2.2创建普通文件

在/proc文件系统中创建一个虚拟文件可以使用如下的函数:

static inline struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent) ;

该函数中name为要创建的文件名;mode为创建文件的属性;parent指向该文件父目录的指针,如果创建的虚拟文件位于/proc下,则这个参数为NULL。

比如我们通过该函数在/proc/edsionte_procfs目录下创建一个虚拟文件foo,其权限为644。其中example_dir指向我们刚创建的目录文件edsionte_procfs。

struct proc_dir_entry *foo_file;

foo_file = create_proc_entry("foo", 0644, example_dir);

if (foo_file == NULL) {

rv = -ENOMEM;

goto no_foo;

}

2.3.创建符号链接文件

当我们需要在/proc文件系统下创建一个符号链接文件时,可使用如下接口:

struct proc_dir_entry *proc_symlink(const char *name, struct proc_dir_entry *parent, const char *dest);

name参数为要创建的符号链接文件名;parent为该符号链接文件的父目录;dest为符号链接所指向的目标文件。

下面的代码演示了如何通过该函数来对已存在的虚拟文件jiffies创建符号链接文件jiffies_too:

symlink = proc_symlink("jiffies_too", example_dir, "jiffies");

if (symlink == NULL) {

rv = -ENOMEM;

goto no_symlink;

}

我们内核模块加载函数中完成上述几个虚拟文件的创建工作。

2.4.删除文件或目录

既然有创建虚拟文件的函数,必然也就有删除虚拟文件的函数接口:

void remove_proc_entry(const char *name, struct proc_dir_entry *parent);

该函数中的参数name和parent与上述函数的参数意义相同。

在示例程序中,我们在卸载函数中完成上述几个文件的删除工作:

remove_proc_entry("jiffies_too", example_dir);

remove_proc_entry("foo", example_dir);

remove_proc_entry("MODULE_NAME", NULL);

2.5读写proc文件

如果只是创建了虚拟文件,那么它并不能被读写。为此,我们必须为每个虚拟文件挂接读写函数,如果该虚拟文件是只读的,那么只需挂载相应的读函数。

正如上面所述,每个虚拟文件对应的proc_dir_entry结构都有read_proc和write_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);

如果要实现对虚拟文件的读写,则需要实现上述两个函数接口。对于我们的示例程序,我们的实现方法如下:

static int proc_read_foobar(char *page, char **start, off_t off, int count, int *eof, void *data)

{

int len;

struct fb_data_t *fb_data = (struct fb_data_t *)data;

//将fb_data的数据写入page

len = sprintf(page, "%s = %s\n", fb_data->name, fb_data->value);

return len;

}

static int proc_write_foobar(struct file *file, const char *buffer, unsigned long count, void *data)

{

int len;

struct fb_data_t *fb_data = (struct fb_data_t *)data;

if (count > FOOBAR_LEN)

len = FOOBAR_LEN;

else

len = count;

//写函数的核心语句,将用户态的buffer写入内核态的value中

if (copy_from_user(fb_data->value, buffer, len))

return -EFAULT;

fb_data->value[len] = '\0';

return len;

}

当用户读我们刚创建的虚拟文件时,该文件对应的read_proc函数将被调用。该函数将数据写入内核的缓冲区中。上述读函数的例子中,缓冲区即为page。当用户给虚拟文件写数据时,write_proc函数将被调用,该函数从缓冲区buffer中读取count个字节的数据。

3.测试

接下来我们将进行一系列的读写测试。由于我们只为jiffies与其符号链接文件jiffies_too实现了读回调函数,因此它们为只读文件,当对这两个文件进行写操作时就会出现错误;对于foo和bar文件,我们为其实现了读、写函数,因此既可以对它们进行读操作也可以进行写操作。

root@edsionte-desktop:/proc/edsionte_procfs# cat jiffies

jiffies = 833619

root@edsionte-desktop:/proc/edsionte_procfs# cat jiffies_too

jiffies = 834442

root@edsionte-desktop:/proc/edsionte_procfs# cat bar

bar = bar

root@edsionte-desktop:/proc/edsionte_procfs# cat foo

foo = foo

root@edsionte-desktop:/proc/edsionte_procfs# echo "time" > jiffies

bash: echo: 写操作出错: 输入/输出错误

root@edsionte-desktop:/proc/edsionte_procfs# echo "time" > jiffies_too

bash: echo: 写操作出错: 输入/输出错误

root@edsionte-desktop:/proc/edsionte_procfs# echo "hello" >> bar

root@edsionte-desktop:/proc/edsionte_procfs# cat bar

bar = hello

示例程序可以在此下载完成代码。

参考:

1.边干边学-Linux内核指导;作者: 李善平;出版社: 浙江大学出版社;

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐