对/proc文件系统进行读写操作

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

cat /proc/interrupts
2
cat /proc/devices
3
cat /proc/kallsyms | grep super_blocks

第一条命令用于查看系统内已注册的中断信息,包括中断号、已接受的手段请求和驱动器名称等;第二条命令用于查看系统内已注册的字符设备和块设备信息,包括设备号和设备名称;第三条命令用于在内核符号表中检索super_blocks符号的的地址,kallsyms文件包括内核中所有的标示符及其地址。1.概述proc即process的缩写,最初的proc文件系统只是存放进程的相关信息。但现在的/proc文件系统除此之外还包含系统的状态信息和配置信息。
通过ls命令就可以查看/proc文件系统所包含的内容。

edsionte@edsionte-desktop:/proc$ ls
2
1      1290   1469  1541  1627   19612  29    49    9          dri              mdstat          sys
3
10     13     1471  1544  1630   19613  3     5     908        driver           meminfo         sysrq-trigger
4
1013   1301   1474  1548  1632   19629  30    50    913        edsionte_procfs  misc            sysvipc
5
…………

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

edsionte@edsionte-desktop:/proc/1$ sudo cat maps
02
[sudo] password for edsionte:
03
00110000-00263000 r-xp 00000000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
04
00263000-00264000 ---p 00153000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
05
00264000-00266000 r--p 00153000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
06
00266000-00267000 rw-p 00155000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
07
00267000-0026a000 rw-p 00000000 00:00 0
08
0026a000-00272000 r-xp 00000000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
09
00272000-00273000 r--p 00007000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
10
00273000-00274000 rw-p 00008000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
11
00471000-0048b000 r-xp 00000000 08:07 1048610    /sbin/init
12
…………

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

edsionte@edsionte-desktop:/proc$ cat meminfo
2
MemTotal:         961368 kB
3
MemFree:          145264 kB
4
Buffers:           31648 kB
5
Cached:           297716 kB
6
SwapCached:        14436 kB
7
…………

总之,/proc文件系统相当于内核的一个快照,该目录下的所有信息都是动态的从正在运行的内核中读取。基于这种原因,/proc文件系统就成为了用户和内核之间交互的接口。一方面,用户可以从/proc文件系统中读取很多内核释放出来的信息;另一方面,内核也可以在恰当的时候从用户那里得到输入信息,从而改变内核的相关状态和配置。相比传统的文件系统,/proc是一种特殊的文件系统,即虚拟文件系统。这里的虚拟是强调/proc文件系统下的所有文件都存在于内存中而不是磁盘上。也就是说/proc文件系统只占用内存空间,而不占用系统的外存空间。2.用户态和内核态之间的数据通信既然内核的数据以/proc文件系统的形式呈现给用户,也就是说内核的信息以文件的形式存在于该文件系统中,那么/proc文件系统就应当提供一组接口对其内的文件进行读写操作。接下来我们以一个实际的内核模块程序easyProc.c为例,说明/proc文件系统的常用接口。该程序中依次创建了几个虚拟文件,然后在用户态对这些文件进行读写测试。2.0数据结构每个虚拟文件都对应一个proc_dir_entry类型的数据结构,该结构具体定义如下:

struct proc_dir_entry {
02
   const char *name;           // virtual file name
03
   mode_t mode;                // mode permissions
04
   uid_t uid;              // File's user id
05
   gid_t gid;              // File's group id
06
   struct inode_operations *proc_iops; // Inode operations functions
07
   struct file_operations *proc_fops;  // File operations functions
08
   struct proc_dir_entry *parent;      // Parent directory
09
   ...
10
   read_proc_t *read_proc;         // /proc read function
11
   write_proc_t *write_proc;       // /proc write function
12
   void *data;             // Pointer to private data
13
   atomic_t count;             // use count
14
   ...
15
};

除了保存该虚拟文件的基本信息外,该结构中还有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"
2
struct proc_dir_entry *example_dir;
3
   example_dir = proc_mkdir(MODULE_NAME, NULL);
4
   if (example_dir == NULL) {
5
   rv = -ENOMEM;
6
   goto out;
7
   }

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;
2
   foo_file = create_proc_entry("foo", 0644, example_dir);
3
   if (foo_file == NULL) {
4
   rv = -ENOMEM;
5
   goto no_foo;
6
   }

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");
2
if (symlink == NULL) {
3
   rv = -ENOMEM;
4
   goto no_symlink;
5
}

我们内核模块加载函数中完成上述几个虚拟文件的创建工作。2.4.删除文件或目录既然有创建虚拟文件的函数,必然也就有删除虚拟文件的函数接口:

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

该函数中的参数name和parent与上述函数的参数意义相同。
在示例程序中,我们在卸载函数中完成上述几个文件的删除工作:

remove_proc_entry("jiffies_too", example_dir);
2
remove_proc_entry("foo", example_dir);
3
remove_proc_entry("MODULE_NAME", NULL);

2.5读写proc文件如果只是创建了虚拟文件,那么它并不能被读写。为此,我们必须为每个虚拟文件挂接读写函数,如果该虚拟文件是只读的,那么只需挂载相应的读函数。正如上面所述,每个虚拟文件对应的proc_dir_entry结构都有read_proc和write_proc两个字段,它们均为函数指针,其各自的类型定义如下:

1
typedef int (read_proc_t)(char *page, char **start, off_t off, int count, int *eof, void *data);
2
 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)
02
{
03
   int len;
04
   struct fb_data_t *fb_data = (struct fb_data_t *)data;
05
 
06
   //将fb_data的数据写入page
07
   len = sprintf(page, "%s = %s\n", fb_data->name, fb_data->value);
08
 
09
   return len;
10
}
11
 
12
static int proc_write_foobar(struct file *file, const char *buffer, unsigned long count, void *data)
13
{
14
   int len;
15
   struct fb_data_t *fb_data = (struct fb_data_t *)data;
16
 
17
   if (count > FOOBAR_LEN)
18
   len = FOOBAR_LEN;
19
   else
20
   len = count;
21
 
22
   //写函数的核心语句,将用户态的buffer写入内核态的value中
23
   if (copy_from_user(fb_data->value, buffer, len))
24
   return -EFAULT;
25
 
26
   fb_data->value[len] = '\0';
27
 
28
   return len;
29
}

当用户读我们刚创建的虚拟文件时,该文件对应的read_proc函数将被调用。该函数将数据写入内核的缓冲区中。上述读函数的例子中,缓冲区即为page。当用户给虚拟文件写数据时,write_proc函数将被调用,该函数从缓冲区buffer中读取count个字节的数据。3.测试接下来我们将进行一系列的读写测试。由于我们只为jiffies与其符号链接文件jiffies_too实现了读回调函数,因此它们为只读文件,当对这两个文件进行写操作时就会出现错误;对于foo和bar文件,我们为其实现了读、写函数,因此既可以对它们进行读操作也可以进行写操作。

root@edsionte-desktop:/proc/edsionte_procfs# cat jiffies
02
jiffies = 833619
03
root@edsionte-desktop:/proc/edsionte_procfs# cat jiffies_too
04
jiffies = 834442
05
root@edsionte-desktop:/proc/edsionte_procfs# cat bar
06
bar = bar
07
root@edsionte-desktop:/proc/edsionte_procfs# cat foo
08
foo = foo
09
root@edsionte-desktop:/proc/edsionte_procfs# echo "time" > jiffies
10
bash: echo: 写操作出错: 输入/输出错误
11
root@edsionte-desktop:/proc/edsionte_procfs# echo "time" > jiffies_too
12
bash: echo: 写操作出错: 输入/输出错误
13
root@edsionte-desktop:/proc/edsionte_procfs# echo "hello" >> bar
14
root@edsionte-desktop:/proc/edsionte_procfs# cat bar
15
bar = hello

示例程序可以在此下载完成代码。参考:1.边干边学-Linux内核指导;作者: 李善平;出版社: 浙江大学出版社;2.使用 /proc 文件系统来访问 Linux 内核的内容;
http://www.ibm.com/developerworks/cn/linux/l-proc.html