本博客之前的文章中多次涉及到/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内核指导;作者: 李善平;出版社: 浙江大学出版社;
2.使用 /proc 文件系统来访问 Linux 内核的内容;
http://www.ibm.com/developerworks/cn/linux/l-proc.html