使用 /proc 文件系统来访问 linux操作系统 内核的内容 && 虚拟文件系统vfs及proc详解

时间:2023-11-17 08:32:56

http://blog.163.com/he_junwei/blog/static/19793764620152743325659/

http://www.01yun.com/other/20130422/366044.html

使用 /proc 文件系统来访问 Linux 内核的内容

这个虚拟文件系统在内核空间和用户空间之间打开了一个通信窗口

简介: /proc 文件系统是一个虚拟文件系统,通过它可以使用一种新的方法在 Linux? 内核空间和用户空间之间进行通信。在 /proc 文件系统中,我们可以将对虚拟文件的读写作为与内核中实体进行通信的一种手段,但是与普通文件不同的是,这些虚拟文件的内容都是动态创建的。本文对 /proc 虚拟文件系统进行了介绍,并展示了它的用法。

最初开发 /proc 文件系统是为了提供有关系统中进程的信息。但是由于这个文件系统非常有用,因此内核中的很多元素也开始使用它来报告信息,或启用动态运行时配置。

/proc 文件系统包含了一些目录(用作组织信息的方式)和虚拟文件。虚拟文件可以向用户呈现内核中的一些信息,也可以用作一种从用户空间向内核发送信息的手段。实际上我们并不会同时需要实现这两点,但是本文将向您展示如何配置这个文件系统进行输入和输出。

尽管像本文这样短小的一篇文章无法详细介绍 /proc 的所有用法,但是它依然对这两种用法进行了展示,从而可以让我们体会一下 /proc 是多么强大。清单 1 是对 /proc 中部分元素进行一次交互查询的结果。它显示的是 /proc 文件系统的根目录中的内容。注意,在左边是一系列数字编号的文件。每个实际上都是一个目录,表示系统中的一个进程。由于在 GNU/Linux 中创建的第一个进程是 init进程,因此它的 process-id 为 1。然后对这个目录执行一个 ls 命令,这会显示很多文件。每个文件都提供了有关这个特殊进程的详细信息。例如,要查看 init 的 command-line 项的内容,只需对 cmdline 文件执行 cat 命令。

/proc 中另外一些有趣的文件有:cpuinfo,它标识了处理器的类型和速度;pci,显示在 PCI 总线上找到的设备;modules,标识了当前加载到内核中的模块。

清单 1. 对 /proc 的交互过程

[root@plato]# ls /proc

1 2040 2347 2874 474 fb mdstat sys

104 2061 2356 2930 9 filesystems meminfo sysrq-trigger

113 2073 2375 2933 acpi fs misc sysvipc

1375 21 2409 2934 buddyinfo ide modules tty

1395 2189 2445 2935 bus interrupts mounts uptime

1706 2201 2514 2938 cmdline iomem mtrr version

179 2211 2515 2947 cpuinfo ioports net vmstat

180 2223 2607 3 crypto irq partitions

181 2278 2608 3004 devices kallsyms pci

182 2291 2609 3008 diskstats kcore self

2 2301 263 3056 dma kmsg slabinfo

2015 2311 2805 394 driver loadavg stat

2019 2337 2821 4 execdomains locks swaps

[root@plato 1]# ls /proc/1

auxv cwd exe loginuid mem oom_adj root statm task

cmdline environ fd maps mounts oom_score stat status wchan

[root@plato]# cat /proc/1/cmdline

init [5]

[root@plato]#

清单 2 展示了对 /proc 中的一个虚拟文件进行读写的过程。这个例子首先检查内核的 TCP/IP 栈中的 IP 转发的目前设置,然后再启用这种功能。

清单 2. 对 /proc 进行读写(配置内核)

[root@plato]# cat /proc/sys/net/ipv4/ip_forward

0

[root@plato]# echo "1" > /proc/sys/net/ipv4/ip_forward

[root@plato]# cat /proc/sys/net/ipv4/ip_forward

1

[root@plato]#

另外,我们还可以使用 sysctl 来配置这些内核条目。有关这个问题的更多信息,请参阅 参考资料 一节的内容。

顺便说一下,/proc 文件系统并不是 GNU/Linux 系统中的惟一一个虚拟文件系统。在这种系统上,sysfs 是一个与 /proc 类似的文件系统,但是它的组织更好(从 /proc 中学习了很多教训)。不过 /proc 已经确立了自己的地位,因此即使 sysfs 与 /proc 相比有一些优点,/proc 也依然会存在。还有一个 debugfs 文件系统,不过(顾名思义)它提供的更多是调试接口。debugfs 的一个优点是它将一个值导出给用户空间非常简单(实际上这不过是一个调用而已)。

内核模块简介

可加载内核模块(LKM)是用来展示 /proc 文件系统的一种简单方法,这是因为这是一种用来动态地向 Linux 内核添加或删除代码的新方法。LKM 也是 Linux 内核中为设备驱动程序和文件系统使用的一种流行机制。

如果您曾经重新编译过 Linux 内核,就可能会发现在内核的配置过程中,有很多设备驱动程序和其他内核元素都被编译成了模块。如果一个驱动程序被直接编译到了内核中,那么即使这个驱动程序没有运行,它的代码和静态数据也会占据一部分空间。但是如果这个驱动程序被编译成一个模块,就只有在需要内存并将其加载到内核时才会真正占用内存空间。有趣的是,对于 LKM 来说,我们不会注意到有什么性能方面的差异,因此这对于创建一个适应于自己环境的内核来说是一种功能强大的手段,这样可以根据可用硬件和连接的设备来加载对应的模块。

下面是一个简单的 LKM,可以帮助您理解它与在 Linux 内核中看到的标准(非动态可加载的)代码之间的区别。清单 3 给出了一个最简单的 LKM。(可以从本文的 下载 一节中下载这个代码)。

清单 3 包括了必须的模块头(它定义了模块的 API、类型和宏)。然后使用 MODULE_LICENSE 定义了这个模块使用的许可证。此处,我们定义的是 GPL,从而防止会污染到内核。

清单 3 然后又定义了这个模块的 init 和 cleanup 函数。my_module_init 函数是在加载这个模块时被调用的,它用来进行一些初始化方面的工作。my_module_cleanup 函数是在卸载这个模块时被调用的,它用来释放内存并清除这个模块的踪迹。注意此处printk 的用法:这是内核的 printf 函数。KERN_INFO 符号是一个字符串,可以用来对进入内核回环缓冲区的信息进行过滤(非常类似于 syslog)。

最后,清单 3 使用 module_init 和 module_exit 宏声明了入口函数和出口函数。这样我们就可以按照自己的意愿来对这个模块的init 和 cleanup 函数进行命名了,不过我们最终要告诉内核维护函数就是这些函数。

清单 3. 一个简单的但可以正常工作的 LKM(simple-lkm.c)

#include

/* Defines the license for this LKM */

MODULE_LICENSE("GPL");

/* Init function called on module entry */

int my_module_init( void )

{

printk(KERN_INFO "my_module_init called. Module is now loaded.\n");

return 0;

}

/* Cleanup function called on module exit */

void my_module_cleanup( void )

{

printk(KERN_INFO "my_module_cleanup called. Module is now unloaded.\n");

return;

}

/* Declare entry and exit functions */

module_init( my_module_init );

module_exit( my_module_cleanup );

清单 3 尽管非常简单,但它却是一个真正的 LKM。现在让我们对其进行编译并在一个 2.6 版本的内核上进行测试。2.6 版本的内核为内核模块的编译引入了一种新方法,我发现这种方法比原来的方法简单了很多。对于文件 simple-lkm.c,我们可以创建一个 makefile,其惟一内容如下:

obj-m += simple-lkm.o

要编译 LKM,请使用 make 命令,如清单 4 所示。

清单 4. 编译 LKM

结果会生成一个 simple-lkm.ko 文件。这个新的命名约定可以帮助将这些内核对象(LKM)与标准对象区分开来。现在可以加载或卸载这个模块了,然后可以查看它的输出。要加载这个模块,请使用 insmod 命令;反之,要卸载这个模块,请使用 rmmod 命令。lsmod 可以显示当前加载的 LKM(参见清单 5)。

清单 5. 插入、检查和删除 LKM

[root@plato]# insmod simple-lkm.ko

[root@plato]# lsmod

Module Size Used by

simple_lkm 1536 0

autofs4 26244 0

video 13956 0

button 5264 0

battery 7684 0

ac 3716 0

yenta_socket 18952 3

rsrc_nonstatic 9472 1 yenta_socket

uhci_hcd 32144 0

i2c_piix4 7824 0

dm_mod 56468 3

[root@plato]# rmmod simple-lkm

[root@plato]#

注意,内核的输出进到了内核回环缓冲区中,而不是打印到 stdout 上,这是因为 stdout 是进程特有的环境。要查看内核回环缓冲区中的消息,可以使用 dmesg 工具(或者通过 /proc 本身使用 cat /proc/kmsg 命令)。清单 6 给出了 dmesg 显示的最后几条消息。

清单 6. 查看来自 LKM 的内核输出

[root@plato]# dmesg | tail -5

cs: IO port probe 0xa00-0xaff: clean.

eth0: Link is down

eth0: Link is up, running at 100Mbit half-duplex

my_module_init called. Module is now loaded.

my_module_cleanup called. Module is now unloaded.

[root@plato]#

可以在内核输出中看到这个模块的消息。现在让我们暂时离开这个简单的例子,来看几个可以用来开发有用 LKM 的内核 API。

集成到 /proc 文件系统中

内核程序员可以使用的标准 API,LKM 程序员也可以使用。LKM 甚至可以导出内核使用的新变量和函数。有关 API 的完整介绍已经超出了本文的范围,因此我们在这里只是简单地介绍后面在展示一个更有用的 LKM 时所使用的几个元素。

创建并删除 /proc 项

要在 /proc 文件系统中创建一个虚拟文件,请使用 create_proc_entry 函数。这个函数可以接收一个文件名、一组权限和这个文件在 /proc 文件系统中出现的位置。create_proc_entry 的返回值是一个 proc_dir_entry 指针(或者为 NULL,说明在 create时发生了错误)。然后就可以使用这个返回的指针来配置这个虚拟文件的其他参数,例如在对该文件执行读操作时应该调用的函数。create_proc_entry 的原型和 proc_dir_entry 结构中的一部分如清单 7 所示。

清单 7. 用来管理 /proc 文件系统项的元素

struct proc_dir_entry *create_proc_entry( const char *name, mode_t mode,

struct proc_dir_entry *parent );

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

...

};

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

稍后我们就可以看到如何使用 read_proc 和 write_proc 命令来插入对这个虚拟文件进行读写的函数。

要从 /proc 中删除一个文件,可以使用 remove_proc_entry 函数。要使用这个函数,我们需要提供文件名字符串,以及这个文件在 /proc 文件系统中的位置(parent)。这个函数原型如清单 7 所示。

parent 参数可以为 NULL(表示 /proc 根目录),也可以是很多其他值,这取决于我们希望将这个文件放到什么地方。表 1 列出了可以使用的其他一些父 proc_dir_entry,以及它们在这个文件系统中的位置。

表 1. proc_dir_entry 快捷变量

proc_dir_entry在文件系统中的位置

proc_root_fs/proc

proc_net/proc/net

proc_bus/proc/bus

proc_root_driver /proc/driver

回调函数

我们可以使用 write_proc 函数向 /proc 中写入一项。这个函数的原型如下:

int mod_write( struct file *filp, const char __user *buff,

unsigned long len, void *data );

filp 参数实际上是一个打开文件结构(我们可以忽略这个参数)。buff 参数是传递给您的字符串数据。缓冲区地址实际上是一个用户空间的缓冲区,因此我们不能直接读取它。len 参数定义了在 buff 中有多少数据要被写入。data 参数是一个指向私有数据的指针(参见 清单 7)。在这个模块中,我们声明了一个这种类型的函数来处理到达的数据。

Linux 提供了一组 API 来在用户空间和内核空间之间移动数据。对于 write_proc 的情况来说,我们使用了 copy_from_user 函数来维护用户空间的数据。

读回调函数

我们可以使用 read_proc 函数从一个 /proc 项中读取数据(从内核空间到用户空间)。这个函数的原型如下:int mod_read( char *page, char **start, off_t off,

int count, int *eof, void *data );

page 参数是这些数据写入到的位置,其中 count 定义了可以写入的最大字符数。在返回多页数据(通常一页是 4KB)时,我们需要使用 start 和 off 参数。当所有数据全部写入之后,就需要设置 eof(文件结束参数)。与 write 类似,data 表示的也是私有数据。此处提供的 page 缓冲区在内核空间中。因此,我们可以直接写入,而不用调用 copy_to_user。

其他有用的函数

我们还可以使用 proc_mkdir、symlinks 以及 proc_symlink 在 /proc 文件系统中创建目录。对于只需要一个 read 函数的简单 /proc 项来说,可以使用 create_proc_read_entry,这会创建一个 /proc 项,并在一个调用中对 read_proc 函数进行初始化。这些函数的原型如清单 8 所示。

清单 8. 其他有用的 /proc 函数

/* Create a directory in the proc filesystem */

struct proc_dir_entry *proc_mkdir( const char *name,

struct proc_dir_entry *parent );

/* Create a symlink in the proc filesystem */

struct proc_dir_entry *proc_symlink( const char *name,

struct proc_dir_entry *parent,

const char *dest );

/* Create a proc_dir_entry with a read_proc_t in one call */

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 );

/* Copy buffer to user-space from kernel-space */

unsigned long copy_to_user( void __user *to,

const void *from,

unsigned long n );

/* Copy buffer to kernel-space from user-space */

unsigned long copy_from_user( void *to,

const void __user *from,

unsigned long n );

/* Allocate a 'virtually' contiguous block of memory */

void *vmalloc( unsigned long size );

/* Free a vmalloc'd block of memory */

void vfree( void *addr );

/* Export a symbol to the kernel (make it visible to the kernel) */

EXPORT_SYMBOL( symbol );

/* Export all symbols in a file to the kernel (declare before module.h) */

EXPORT_SYMTAB

通过 /proc 文件系统实现财富分发

下面是一个可以支持读写的 LKM。这个简单的程序提供了一个财富甜点分发。在加载这个模块之后,用户就可以使用 echo 命令向其中导入文本财富,然后再使用 cat 命令逐一读出。

清单 9 给出了基本的模块函数和变量。init 函数(init_fortune_module)负责使用 vmalloc 来为这个点心罐分配空间,然后使用 memset 将其全部清零。使用所分配并已经清空的 cookie_pot 内存,我们在 /proc 中创建了一个 proc_dir_entry 项,并将其称为 fortune。当 proc_entry 成功创建之后,对自己的本地变量和 proc_entry 结构进行了初始化。我们加载了 /proc read 和write 函数(如清单 9 和清单 10 所示),并确定这个模块的所有者。cleanup 函数简单地从 /proc 文件系统中删除这一项,然后释放 cookie_pot 所占据的内存。

cookie_pot 是一个固定大小(4KB)的页,它使用两个索引进行管理。第一个是 cookie_index,标识了要将下一个 cookie 写到哪里去。变量 next_fortune 标识了下一个 cookie 应该从哪里读取以便进行输出。在所有的 fortune 项都读取之后,我们简单地回到了 next_fortune。

清单 9. 模块的 init/cleanup 和变量

#include

#include

#include

#include

#include

#include

MODULE_LICENSE("GPL");

MODULE_DESCRIPTION("Fortune Cookie Kernel Module");

MODULE_AUTHOR("M. Tim Jones");

#define MAX_COOKIE_LENGTH PAGE_SIZE

static struct proc_dir_entry *proc_entry;

static char *cookie_pot; // Space for fortune strings

static int cookie_index; // Index to write next fortune

static int next_fortune; // Index to read next fortune

int init_fortune_module( void )

{

int ret = 0;

cookie_pot = (char *)vmalloc( MAX_COOKIE_LENGTH );

if (!cookie_pot) {

ret = -ENOMEM;

} else {

memset( cookie_pot, 0, MAX_COOKIE_LENGTH );

proc_entry = create_proc_entry( "fortune", 0644, NULL );

if (proc_entry == NULL) {

ret = -ENOMEM;

vfree(cookie_pot);

printk(KERN_INFO "fortune: Couldn't create proc entry\n");

} else {

cookie_index = 0;

next_fortune = 0;

proc_entry->read_proc = fortune_read;

proc_entry->write_proc = fortune_write;

proc_entry->owner = THIS_MODULE;

printk(KERN_INFO "fortune: Module loaded.\n");

}

}

return ret;

}

void cleanup_fortune_module( void )

{

remove_proc_entry("fortune", &proc_root);

vfree(cookie_pot);

printk(KERN_INFO "fortune: Module unloaded.\n");

}

module_init( init_fortune_module );

module_exit( cleanup_fortune_module );

向这个罐中新写入一个 cookie 非常简单(如清单 10 所示)。使用这个写入 cookie 的长度,我们可以检查是否有这么多空间可用。如果没有,就返回 -ENOSPC,它会返回给用户空间。否则,就说明空间存在,我们使用 copy_from_user 将用户缓冲区中的数据直接拷贝到 cookie_pot 中。然后增大 cookie_index(基于用户缓冲区的长度)并使用 NULL 来结束这个字符串。最后,返回实际写入 cookie_pot 的字符的个数,它会返回到用户进程。

清单 10. 对 fortune 进行写入操作所使用的函数

ssize_t fortune_write( struct file *filp, const char __user *buff,

unsigned long len, void *data )

{

int space_available = (MAX_COOKIE_LENGTH-cookie_index)+1;

if (len > space_available) {

printk(KERN_INFO "fortune: cookie pot is full!\n");

return -ENOSPC;

}

if (copy_from_user( &cookie_pot[cookie_index], buff, len )) {

return -EFAULT;

}

cookie_index += len;

cookie_pot[cookie_index-1] = 0;

return len;

}

对 fortune 进行读取也非常简单,如清单 11 所示。由于我们刚才写入数据的缓冲区(page)已经在内核空间中了,因此可以直接对其进行操作,并使用 sprintf 来写入下一个 fortune。如果 next_fortune 索引大于 cookie_index(要写入的下一个位置),那么我们就将 next_fortune 返回为 0,这是第一个 fortune 的索引。在将这个 fortune 写入用户缓冲区之后,在 next_fortune 索引上增加刚才写入的 fortune 的长度。这样就变成了下一个可用 fortune 的索引。这个 fortune 的长度会被返回并传递给用户。

清单 11. 对 fortune 进行读取操作所使用的函数

int fortune_read( char *page, char **start, off_t off,

int count, int *eof, void *data )

{

int len;

if (off > 0) {

*eof = 1;

return 0;

}

/* Wrap-around */

if (next_fortune >= cookie_index) next_fortune = 0;

len = sprintf(page, "%s\n", &cookie_pot[next_fortune]);

next_fortune += len;

return len;

}

从这个简单的例子中,我们可以看出通过 /proc 文件系统与内核进行通信实际上是件非常简单的事情。现在让我们来看一下这个 fortune 模块的用法(参见清单 12)。

清单 12. 展示 fortune cookie LKM 的用法

[root@plato]# insmod fortune.ko

[root@plato]# echo "Success is an individual proposition.

Thomas Watson" > /proc/fortune

[root@plato]# echo "If a man does his best, what else is there

Gen. Patton" > /proc/fortune

[root@plato]# echo "Cats: All your base are belong to us.

Zero Wing" > /proc/fortune

[root@plato]# cat /proc/fortune

Success is an individual proposition. Thomas Watson

[root@plato]# cat /proc/fortune

If a man does his best, what else is there General Patton

[root@plato]#

/proc 虚拟文件系统可以广泛地用来报告内核的信息,也可以用来进行动态配置。我们会发现它对于驱动程序和模块编程来说都是非常完整的。在下面的 参考资料 中,我们可以学习到更多相关知识。

转载请保留出处:http://www.01yun.com/other/20130422/366044.html

Linux虚拟文件系统vfs及proc详解

/proc文件系统下的多种文件提供的系统信息不是针对某个特定进程的,而是能够在整个系统范围的上下文中使用。可以使用的文件随系统配置的变化而变化。命令procinfo能够显示基于其中某些文件的多种系统信息。以下详细描述/proc下的文件。

/proc/cmdline文件

这个文件给出了内核启动的命令行。它和用于进程的cmdline项非常相似。

示例:

[root@localhost proc]# cat cmdline

ro root=LABEL=/ rhgb quiet

/proc/cpuinfo文件

这个文件提供了有关系统CPU的多种信息。这些信息是从内核里对CPU的测试代码中得到的。文件列出了CPU的普通型号(386,486,586,686 等),以及能得到的更多特定信息(制造商,型号和版本)。文件还包含了以bogomips表示的处理器速度,而且如果检测到CPU的多种特性或者bug,文件还会包含相应的标志。这个文件的格式为:文件由多行构成,每行包括一个域名称,一个冒号和一个值。

示例:

[root@localhost proc]# cat cpuinfo

processor : 0

vendor_id : AuthenticAMD

cpu family : 6

model : 8

model name : AMD Athlon(tm) XP 1700+

stepping : 1

cpu MHz : 1530.165

cache size : 256 KB

fdiv_bug : no

hlt_bug : no

f00f_bug : no

coma_bug : no

fpu : yes

fpu_exception : yes

cpuid level : 1

wp : yes

flags : fpu vme de pse tsc msr pae mce cx8 apic mtrr pge mca cmov pat pse36 mmx fxsr sse syscall mmxext 3dnowext 3dnow

bogomips : 2998.27

/proc/devices文件

这个文件列出字符和块设备的主设备号,以及分配到这些设备号的设备名称。

示例:

[root@localhost /]# cat /proc/devices

Character devices:

1 mem

4 /dev/vc/0

4 tty

4 ttyS

5 /dev/tty

5 /dev/console

5 /dev/ptmx

6 lp

7 vcs

10 misc

13 input

14 sound

29 fb

36 netlink

116 alsa

128 ptm

136 pts

180 usb

Block devices:

1 ramdisk

2 fd

3 ide0

9 md

22 ide1

253 device-mapper

254 mdp

/proc/dma文件

这个文件列出由驱动程序保留的DMA通道和保留它们的驱动程序名称。casade项供用于把次DMA控制器从主控制器分出的DMA行所使用;这一行不能用于其它用途。

示例:

[root@localhost ~]# cat /proc/dma

4: cascade

/proc/filesystems文件

这个文件列出可供使用的文件系统类型,一种类型一行。虽然它们通常是编入内核的文件系统类型,但该文件还可以包含可加载的内核模块加入的其它文件系统类型。

示例:

[root@localhost proc]# cat /proc/filesystems

nodev sysfs

nodev rootfs

nodev bdev

nodev proc

nodev sockfs

nodev binfmt_misc

nodev usbfs

nodev usbdevfs

nodev futexfs

nodev tmpfs

nodev pipefs

nodev eventpollfs

nodev devpts

ext2

nodev ramfs

nodev hugetlbfs

iso9660

nodev mqueue

nodev selinuxfs

ext3

nodev rpc_pipefs

nodev autofs

/proc/interrupts文件

这个文件的每一行都有一个保留的中断。每行中的域有:中断号,本行中断的发生次数,可能带有一个加号的域(SA_INTERRUPT标志设置),以及登记 这个中断的驱动程序的名字。可以在安装新硬件前,像查看/proc/dma和/proc/ioports一样用cat命令手工查看手头的这个文件。这几个文件列出了当前投入使用的资源(但是不包括那些没有加载驱动程序的硬件所使用的资源)。

示例:

[root@localhost SPECS]# cat /proc/interrupts

CPU0

0: 7039406 XT-PIC timer

1: 6533 XT-PIC i8042

2: 0 XT-PIC cascade

3: 0 XT-PIC uhci_hcd

5: 108 XT-PIC VIA8233, uhci_hcd

8: 1 XT-PIC rtc

9: 0 XT-PIC acpi

10: 0 XT-PIC ehci_hcd

11: 17412 XT-PIC uhci_hcd, eth0

12: 140314 XT-PIC i8042

14: 37897 XT-PIC ide0

15: 60813 XT-PIC ide1

NMI: 0

ERR: 1

/proc/ioports文件

这个文件列出了诸如磁盘驱动器,以太网卡和声卡设备等多种设备驱动程序登记的许多I/O端口范围。

示例:

[root@localhost SPECS]# cat /proc/ioports

0000-001f : dma1

0020-0021 : pic1

0040-0043 : timer0

0050-0053 : timer1

0060-006f : keyboard

0070-0077

: rtc

0080-008f : dma page reg

00a0-00a1 : pic2

00c0-00df : dma2

00f0-00ff : fpu

0170-0177 : ide1

01f0-01f7 : ide0

0376-0376 : ide1

0378-037a : parport0

037b-037f : parport0

03c0-03df : vga+

03f6-03f6 : ide0

03f8-03ff : serial

0800-0803 : PM1a_EVT_BLK

0804-0805 : PM1a_CNT_BLK

0808-080b : PM_TMR

0810-0815 : ACPI CPU throttle

0820-0823 : GPE0_BLK

0cf8-0cff : PCI conf1

dc00-dcff : 0000:00:12.0

dc00-dcff : via-rhine

e000-e0ff : 0000:00:11.5

e000-e0ff : VIA8233

e400-e41f : 0000:00:10.0

e400-e41f : uhci_hcd

e800-e81f : 0000:00:10.1

e800-e81f : uhci_hcd

ec00-ec1f : 0000:00:10.2

ec00-ec1f : uhci_hcd

fc00-fc0f : 0000:00:11.1

fc00-fc07 : ide0

fc08-fc0f : ide1

/proc/kcore文件

这个文件是系统的物理内存以core文件格式保存的文件。例如,GDB能用它考察内核的数据结构。它不是纯文本,而是/proc目录下为数不多的几个二进制格式的项之一。

/proc/kmsg文件

这个文件用于检索用printk生成的内核消息。任何时刻只能有一个具有超级用户权限的进程可以读取这个文件。也可以用系统调用syslog检索这些消息,通常使用工具dmesg或守护进程klogd检索这些消息。

/proc/ksyms文件

这个文件列出了已经登记的内核符号;这些符号给出了变量或函数的地址。每行给出一个符号的地址,符号名称以及登记这个符号的模块。程序ksyms,insmod和kmod使用这个文件。它还列出了正在运行的任务数,总任务数和最后分配的PID。

/proc/loadavg文件

这个文件给出以几个不同的时间间隔计算的系统平均负载,这就如同uptime命令显示的结果那样。前三个数字是平均负载。这是通过计算过去1分钟,5分钟,15分钟里运行队列中的平均任务数得到的。随后是正在运行的任务数和总任务数。最后是上次使用的进程号。

示例:

[root@localhost ~]# cat /proc/loadavg

0.11 0.16 0.14 3/126 3912

/proc/locks文件

这个文件包含在打开的文件上的加锁信息。文件中的每一行描述了特定文件和文档上的加锁信息以及对文件施加的锁的类型。内核也可以需要时对文件施加强制性锁。

示例:

[root@localhost redhat]# cat /proc/locks

1: POSIX ADVISORY READ 3822 03:0a:1067117 0 EOF

2: POSIX ADVISORY READ 3822 03:0a:1067138 0 EOF

3: POSIX ADVISORY WRITE 3326 03:0a:2326540 0 EOF

4: POSIX ADVISORY WRITE 2639 03:0a:2966595 0 EOF

5: FLOCK ADVISORY WRITE 2591 03:0a:2966586 0 EOF

6: POSIX ADVISORY WRITE 2540 03:0a:2966578 0 EOF

7: POSIX ADVISORY WRITE 2530 03:0a:2966579 0 EOF

8: POSIX ADVISORY WRITE 2402 03:0a:2966563 0 EOF

9: POSIX ADVISORY WRITE 2371 03:0a:2966561 0 EOF

一、VFS分析

Linux 操作系统支持多种不同的文件系统,包括 ext2(the Second Extended file-system),nfs(the Network File-system),FAT(the MS-DOS File Allocation Table file system),minix,以及其他许多文件系统。为了使得 linux 内核中的高层子系统能够以相同的方式处理这些不同的文件系统,Linux 定义了一个抽象层,即虚拟文件系统VFS,又叫作虚拟文件系统转换(Virtual Filesystem Switch)。VFS 是 Linux 内核中的一个子系统,其他的子系统,如IPC、SCHED、MM、NET、都只与 VFS 联系,换句话说,具体的逻辑文件系统对于 Linux 内核中的其他子系统是透明的。

而proc文件系统,对于 Linux 来说,正是一个逻辑文件系统,因此 proc 文件系统的实现,也完全遵循 VFS 的规范,在对 proc 文件系统进行分析之前,我们必须对 VFS 进行一个详细的分析。

(一) 基本设计原理

对于逻辑文件系统来说,VFS 是一个管理者,而对于内核的其他部分,则是一个接口。VFS提供了一个统一的接口(即几个有关操作的数据结构),一个逻辑文件系统要想被 Linux 支持,那么就必须按照这个接口来编写自己的操作函数,从而将自己的细节对其他子系统隐藏起来。因而,对于内核其他子系统来说,所有的文件系统都是一样的。

(二) 基本对象与方法

虚拟文件系统的接口由一组对象及其由这些对象调用的一组方法所构成的。这些基本的对象是 files(文件),file-systems(文件系统),inodes (索引节点)以及 names for inodes(索引节点名字),下面对这些对象进行简单的介绍:

1 Files:

文件是一个可读可写的对象,它也可以映射到内存中,这和 UNIX 中文件描述符的概念很接近。文件在 Linux 中使用一个"struct file"结构来实现,并且该结构有一组操作函数,保存在结构"struct file_operations"中。

2 Inodes:

索引节点是文件系统中的基本对象。它可以是一个正常文件、一个目、,一个符号链接,或者是其他什么东西。VFS 并不明显地区分这些对象,而把它们留给真正的文件系统,让它们自己实现适宜的行为。从而使内核的高层子系统对于不同的对象区别对待。每一个索引节点节点都由一个"struct inode"结构表现,它的一组方法保存在结构"struct inode_operations"中。

文件(Files)和索引节点(Inodes)也许看起来很相像,但它们之间有一些非常重要的不同,要注意的一点是,有些东西有索引节点,但却没有文件,比如,一个符号链接。与之相对应,有些文件却没有索引节点,如管道(pipes)和 sockets。

3 File_systems

文件系统就是 inode 的集合,其中有一个不同的节点,被称为根结点(root)。其他的 inode 以 root 为起始点进行访问,并且通过文件名来查找其他的 inode 。

每一个文件系统有一组唯一的特征,应用于本文件系统内的所有 inode 之上。其中有一些是标志,比如只读 READ-ONLY 标志。另一个重要的内容是 blocksize。

每一个文件系统都通过一个结构"struct super_block"来表现,而针对超级块的一组方法则存储在结构"struct super_operations"之中。

在 Linux 中,超级块(super-blocks)和 设备号(device number)之间有紧密的联系。每一个文件系统必须有一个唯一的设备号,该文件系统即建立在此设备之上。有一些文件系统(比如 nfs 和 我们要研究的 proc 文件系统)被标志为不需要真实的设备,因此,对于这些文件系统,主设备号(major number)为0的匿名设备将会自动地分配给它们。

Linux VFS 了解不同的文件系统类型,每一个文件系统类型都使用一个"struct file_system_type"结构来表示,在这个结构中,只包含一个方法,即 "read_super",使用这个方法来实例化一个指定文件系统的超级块。

4 Names

在一个文件系统内,所有的 inodes 都是通过名字来访问的。由于对于某些文件系统来说,名字到 inode 的转换非常耗时的,因此Linux 的 VFS 层为当前活动的和最近使用的名字维护了一个 cache,这个 cache 被称为 目录高速缓存(dcache)。

dcache 在内存中组织为树状结构。树中的每一个节点都对应于一个指定目录,指定名称的inode。一个inode可以与多个树中的节点相联系。

如果dcache不是一棵完整的文件树,那么它一定是文件树的前缀部分,也就是说,如果一个文件树的节点在cache中,那么该节点的所以祖先也一定在cache中。

每一个树的节点都使用一个结构"struct dentry"来表现,它的一组方法存储在"struct dentry_operations"之中。dentry 在 Files 和 Inodes 之间扮演了中间人的角色。每一个打开的文件都指向一个dentry,而每一个dentry 则指向它所涉及的inode。这意味着,对于每一个打开的文件,该文件的dentry 和该文件所有的父节点都在内存中被cache,这使得被打开文件的全路径可以更容易地检测。

(三) 文件系统的注册和装载过程

1 文件系统的注册

在使用一个文件系统之前,必须要对该文件系统进行注册。在Linux编译的时候,可以选定支持哪些文件系统,这些编译进内核的文件系统,在系统引导的时候,就会在VFS中注册。而如果一个文件系统被编译为内核可装载模块,那么将在模块安装的时候进行注册,在模块卸载的时候注销。

每一个文件系统,都会在自己的初始化例程中填写一个 file_system_type 的数据结构,然后调用注册函数register_filesystem(struct file_system_type *fs) 进行注册。下面我们分析一下 file_system_type 的结构:

file_system_type 在 include/linux/fs.h 中定义:

struct file_system_type {

const char *name;

int fs_flags;

struct super_block *(*read_super) (struct super_block *, void *, int);

struct module *owner;

struct vfsmount *kern_mnt; /* For kernel mount, if it's FS_SINGLE fs */

struct file_system_type * next;

};

而文件系统的注册和注销函数也在该头文件中声明:

extern int register_filesystem(struct file_system_type *);

extern int unregister_filesystem(struct file_system_type *);

函数 register_filesystem 成功时返回0,当 fs == NULL时返回 -EINVAL,而当fs->next!=NULL 或者已经有同名文件系统注册时,则返回-EBUSY。当文件系统作为模块时,必须直接或者间接地在init_module中调用这个注册函数,而如果要编 译进内核,则必须在fs/filesystem.c中的filesystem_setup中注册。而unregister_filesystem 则只能在模块的cleanup_module例程中调用。

所有的已注册文件系统的 file_system_type 结构最终会形成一个链表,被称之为"注册链表"。下图即为内核中 file_system_type 的链表示意图,链表头由 file_systems 指定。

2 文件系统的安装

要真正使用一个文件系统,仅仅注册是不行的,还必须安装这个文件系统。在安装linux时,已经(默认)安装了EXT2文件系统,作为根文件系统。我们可 以在文件/etc/fstab中指定自动安装的文件系统,和使用mount命令一样,我们要为每种文件系统的安装提供三种信息:文件系统的名称,包含该文 件系统的物理设备,以及该文件系统的安装点。例如下面的命令:

mount -t vfat /dev/fd0 /mnt/floppy

将把软盘(物理设备fd0)中的vfat文件系统安装到/mnt/floppy目录上,下面我们分析一下上述命令的执行过程:寻找对应的文件系统的信息。VFS通过file_systems,在file_system_type组成的链表中根据指定的文件系统的名称查看文件系统的类型信息。

如果在上述链表中找到匹配的文件系统,则说明内核支持该文件系统,并已经注册。否则,说明该文件系统有可能由LKM(LinuxKernelModule)可装载模块支持,因此,VFS会请求内核装入相应的文件系统模块,此时,该文件系统在VFS中注册并初始化。

1.如果VFS仍然找到指定的文件系统,那么将返回错误。

2.然后,VFS检验指定的物理块设备是否已经安装。如果指定的物理块设备已经被安装,那么将返回错误。也就是说,一个块设备只能安装到一个目录,不能同时多次安装。

3.VFS查找新文件系统的安装点目录的VFS索引节点。该VFS索引节点可能在索引节点高速缓存中,也有可能需要从安装点所在的块设备中读取。

4.如果该安装点目录已经装有其他的文件系统,那么将返回错误。因为在同一目录只能同时安装一个文件系统。

5.VFS安装代码为新的文件系统分配超级块,并将安装信息传递给该文件系统的超级块读取例程。系统中所有的VFS超级块保存在由super_blocks指向的super_block数据结构指针数组中。

6.文件系统的超级块读取例程将对应的文件系统的信息映射到VFS超级块中。如果在此过程中发生错误,例如所读取的超级块魔数和指定的文件系统不一致,则返回错误。

7.如果成功安装,则所有已经安装的文件系统形成相应的结构:

每一个已经挂装的文件系统由vfsmount结构描述。所有的vfsmount结构形成了一个链表,用vfsmntlist来指向链表头。这个 链表可以称为"已安装文件系统链表"。系统中还有另外两个指向这种结构体的指针,vfsmnttail和mru_vfsmnt分别指向链表尾和最近使用过 的vfsmount结构。

fsmount结构在include/mount.h中定义:

struct vfsmount{

struct dentry *mnt_mountpoint; /* dentry of mountpoint */

struct dentry *mnt_root; /* root of the mounted tree */

struct vfsmount *mnt_parent; /* fs we are mounted on */

struct list_head mnt_instances; /* other vfsmounts of the same fs */

struct list_head mnt_*; /* those who are mounted on (other instances) of the same dentry */

struct super_block *mnt_sb; /* pointer to superblock */

struct list_head mnt_mounts; /* list of children, anchored here */

struct list_head mnt_child; /* and going through their mnt_child */

atomic_t mnt_count;

int mnt_flags;

char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */

struct list_head mnt_list;

uid_t mnt_owner;

};

每个vfsmount结构包含该文件系统所在的块设备号、文件系统安装点的目录名称,以及指向为该文件系统分配的VFS超级块的指针。而VFS超级块中则包含描述文件系统的file_system_type结构指针和该文件系统根结点指针。

下面三个函数是用来操作已安装文件系统链表的,它们都在fs/super.c中实现:

lookup_vfsmnt():在链表中寻找指定设备号的vfsmnt结构,成功则返回指向该结构的指针,否则返回0。

add_vfsmnt():在链表尾加入一个vfsmnt结构,返回指向该结构的指针。

remove_vfsmnt():从链表中移走指定设备号的vfsmnt结构,并释放其所占有的内核内存空间。该函数无返回值。

3 文件系统的卸载

当文件系统被卸载的时候,系统将检查在该文件系统上是否有正被使用。如果有文件正在使用,则不能被卸载。如果该文件系统中的文件或者目录正在使用,则 VFS索引节点高速缓存中可能包含相应的VFS索引节点,检查代码将在索引节点高速缓存中,根据文件系统所在的设备标识符,查找是否有来自该文件系统的 VFS索引节点,如果有而且使用计数大于0,则说明该文件系统正在被使用。因此,该文件系统不能被卸载。

否则,将查看对应的VFS超级块,如果该文件系统的VFS超级块标志为“脏”,那么必须将超级块信息写回磁盘。上述过程结束后,对应的VFS超级块被释放,vfsmount数据结构将从vfsmntlist链表中断开并释放。

(四) VFS 数据结构分析

现在我们已经大致了解了VFS操作的基本过程。下面我们分析一下在VFS中使用的几个重要的数据结构,它们是VFS实现的核心,更是与逻辑文件系统交互的接口,因此必须进行详细的分析。

1 VFS超级块及其操作

许多逻辑文件系统都有超级块结构,超级块是这些文件系统中最重要的数据结构,用来描述整个文件系统的信息,是一个全局的数据结构。MINIX、EXT2等 都有自己的超级块,VFS也有超级块,但和逻辑文件系统的超级块不同,VFS超级块是存在于内存中的结构,它在逻辑文件系统安装时建立,并且在文件系统卸 载时自动删除,因此,VFS对于每一个逻辑文件系统,都有一个对应的VFS超级块。

VFS超级块在include/fs/fs.h中定义,即数据结构super_block,该结构主要定义如下:

struct super_block {

struct list_head s_list; /* Keep this first */

kdev_t s_dev;

unsigned long s_blocksize;

unsigned char s_blocksize_bits;

unsigned char s_lock;

unsigned char s_dirt;

unsigned long long s_maxbytes; /* Max file size */

struct file_system_type *s_type;

struct super_operations *s_op;

struct dquot_operations *dq_op;

unsigned long s_flags;

unsigned long s_magic;

struct dentry *s_root;

wait_queue_head_t s_wait;

struct list_head s_dirty; /* dirty inodes */

struct list_head s_files;

struct block_device *s_bdev;

struct list_head s_mounts; /* vfsmount(s) of this one */ struct quota_mount_options s_dquot; /*Diskquota specific options */

union {

struct minix_sb_info minix_sb;

struct ext2_sb_info ext2_sb;

……

……

void *generic_sbp;

} u;

struct semaphore s_vfs_rename_sem; /*Kludge */

struct semaphore s_nfsd_free_path_sem;

};

下面对该结构的主要域进行一个简单的分析:

s_list:所有已装载文件系统的双向链表(参考 linux/list.h)。

s_dev:装载该文件系统的设备(可以是匿名设备)标识号,举例来说,对于/dev/hda1,其设备标识号为ox301。

s_blocksize:该文件系统的基本数据块的大小。以字节为单位,并且必须是2的n次方。

s_blocksize_bits:块大小所占的位数,即log2(s_blocksize)。

s_lock:用来指出当前超级块是否被锁住。

s_wait:这是一个等待队列,其中的进程都在等待该超级块的s_lock。

s_dirt:这是一个标志位。当超级块被改变时,将置位;当超级块被写入设备时,将清位。(当文件系统被卸载或者调用sync 时,有可能会将超级块写入设备。)

s_type:指向文件系统的file_system_type结构。

s_op:指向一个超级块操作集super_operations,我们将在后面进行讨论。

dq_op:指向一个磁盘限额(DiscQuota)操作集。

s_flags:这是一组操作权限标志,它将与索引节点的标志进行逻辑或操作,从而确定某一特定的行为。这里有一个标志,可以应用于整个文件系统,就是 MS_RDONLY。一个设置了如此标志的文件系统将被以只读的方式装载,任何直接或者间接的写操作都被禁止,包括超级块中装载时间和文件访问时间的改变 等等。

s_root:这是一个指向dentry结构的指针。它指向该文件系统的根。通常它是由装载文件系统的根结点(root inode)时创建的,并将它传递给d_alloc_root。这个dentry将被mount命令加入到dcache中。

s_dirty:“脏”索引节点的链表。当一个索引节点被mark_inode_dirty标志为“脏”时,该索引节点将被放入到这个链表中;当sync_inode被调用时,这个链表中的所有索引节点将被传递给该文件系统的write_inode方法。

s_files:该文件系统所有打开文件的链表。

u.generic_sbp:在联合结构u中,包括了一个文件系统特定的超级块信息,在上面的结构中,我们可以看到有minix_sb 和ext2_sb 等等结构。这些信息是编译时可知的信息,对于那些当作模块装载的文件系统,则必须分配一个单独的结构,并且将地址放入u.generic_sbp中。

s_vfs_rename_sem:这个信号量可以在整个文件系统的范围内使用,当重命名一个目录的时候,将使用它来进行锁定。这是为了防止把一个目录重命名为它自己的子目录。当重命名的目标不是目录时,则不使用该信号量。

针对上面的超级块,定义了一组方法,也叫作操作,在结构super_operations中:

struct super_operations {

void (*read_inode) (struct inode *);

void (*read_inode2) (struct inode *, void *) ;

void (*dirty_inode) (struct inode *);

void (*write_inode) (struct inode *, int);

void (*put_inode) (struct inode *);

void (*delete_inode) (struct inode *);

void (*put_super) (struct super_block *);

void (*write_super) (struct super_block *);

void (*write_super_lockfs) (struct super_block *);

void (*unlockfs) (struct super_block *);

int (*statfs) (struct super_block *, struct statfs *);

int (*remount_fs) (struct super_block *,

int *, char *);

void (*clear_inode) (struct inode *);

void (*umount_begin) (struct super_block *);

};

因此在实现实现自己的逻辑文件系统时,我们必须提供一套自己的超级块操作函数。对这些函数的调用都来自进程正文(process context),而不是来自在中断例程或者bottom half,并且所有的方法调用时,都会使用内核锁,因此,操作可以安全地阻塞,但我们也要避免并发地访问它们。

根据函数的名字,我们可以大概地了解其功能,下面简单地介绍一下:

read_inode:该方法是从一个装载的文件系统中读取一个指定的索引节点。它由get_new_inode调用,而get_new_inode则由fs/inode.c中的iget调用。一般来说,文件系统使用iget来读取特定的索引节点。

write_inode:当一个文件或者文件系统要求sync时,该方法会被由mark_inode_dirty标记为“脏”的索引节点调用,用来确认所有信息已经写入设备。

put_inode:如果该函数被定义了,则每当一个索引节点的引用计数减少时,都会被调用。这并不意味着该索引节点已经没人使用了,仅仅意味着它减少了 一个用户。要注意的是,put_inode在i_count减少之前被调用,所以,如果put_inode想要检查是否这是最后一个引用,则应检查 i_count是否为1。大多数文件系统都会定义该函数,用来在一个索引节点的引用计数减少为0之前做一些特殊的工作。

delete_inode:如果被定义,则当一个索引节点的引用计数减少至0,并且链接计数(i_nlink)也是0的时候,便调用该函数。以后,这个函数有可能会与上一个函数合并。

notify_change:当一个索引节点的属性被改变时,会调用该函数。它的参数struct iattr *指向一个新的属性组。如果一个文件系统没有定义该方法(即NULL),则VFS会调用例程fs/iattr.c:inode_change_ok,该方 法实现了一个符合POSIX标准的属性检验,然后VFS会将该索引节点标记为“脏”。如果一个文件系统实现了自己的notify_change方法,则应 该在改变属性后显式地调用mark_inode_dirty(inode)方法。

put_super:在umount(2)系统调用的最后一步,即将入口从vfsmntlist中移走之前,会调用该函数。该函数调用时,会对 super_block上锁。一般来说,文件系统会针对这个装载实例,释放特有的私有资源,比如索引节点位图,块位图。如果该文件系统是由动态装载模块实 现的,则一个buffer header将保存该super_block,并且减少模块使用计数。

write_super:当VFS决定要将超级块写回磁盘时,会调用该函数。有三个地方会调用它:fs/buffer.c:fs_fsync,fs/super.c:sync_supers和fs/super.c:do_umount,显然只读文件系统不需要这个函数。

statfs:这个函数用来实现系统调用statfs(2),并且如果定义了该函数,会被fs/open.c:sys_statfs调用,否则将返回ENODEV错误。

remountfs:当文件系统被重新装载时,也就是说,当mount(2)系统调用的标志MS_REMOUNT被设置时,会调用该函数。一般用来在不卸载文件系统的情况下,改变不同的装载参数。比如,把一个只读文件系统变成可写的文件系统。

clear_inode:可选方法。当VFS清除索引节点的时候,会调用该方法。当一个文件系统使用了索引节点结构中的generic_ip域,向索引节点增加了特别的(使用kmalloc动态分配的)数据时,便需要此方法来做相应的处理。

2 VFS的文件及其操作

文件对象使用在任何需要读写的地方,包括通过文件系统,或者管道以及网络等进行通讯的对象。文件对象和进程关系紧密,进程通过文件描述符(file descriptors)来访问文件。文件描述符是一个整数,linux通过fs.h中定义的NR_OPEN来规定每个进程最多同时使用的文件描述符个数:

#define NR_OPEN (1024*1024)

一共有三个与进程相关的结构,第一个是files_struct,在include/linux/sched.h中定义,主要是一个fd数组,数组的下标是文件描述符,其内容就是对应的下面将要介绍的file结构。

另外一个结构是fs_struct,主要有两个指针,pwd指向当前工作目录的索引节点;root指向当前工作目录所在文件系统的根目录的索引节点。

最后一个结构是file结构,定义它是为了保证进程对文件的私有记录,以及父子进程对文件的共享,这是一个非常巧妙的数据结构。我们将在下面进行详细的分析。

仔细分析其联系,对于我们理解进程对文件的访问操作很有帮助。

结构file定义在linux/fs.h中:

struct file {

struct list_head f_list;

struct dentry *f_dentry;

struct vfsmount *f_vfsmnt;

struct file_operations *f_op;

atomic_t f_count;

unsigned int f_flags;

mode_t f_mode;

loff_t f_pos;

unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;

struct fown_struct f_owner;

unsigned int f_uid, f_gid;

int f_error;

unsigned long f_version;

/* needed for tty driver, and maybe others */

void *private_data;

};

下面对其作一个简单的分析:

f_list:该域将文件链接到打开的文件链表中,链表由超级块中的s_files开始。

f_dentry:该域指向该文件的索引节点的dcache入口。如果文件的索引节点不在普通的文件系统中,而是诸如管道pipe之类的对象,那么,dentry将是一个由d_alloc_root创建的root dentry。

f_vfsmnt:该域指向该文件所在文件系统的vfsmount结构。

f_op:指向应用于文件的操作集。

f_count:引用该文件的计数。是用户进程的引用数加上内部的引用数。

f_flags:该域存储了进程对该文件的访问类型,比如O_NONBLOCK,O_APPEND等等。有些标志比如O_EXCL,O_CREAT等等,只在打开文件的时候使用,因此并不存储在f_flags中。

f_mode:对文件的操作标志,只读,只写,以及读写。

f_pos:该域存储了文件的当前位置。

f_reada, f_ramax, f_raend, f_ralen, f_rawin:这五个域用来跟踪对文件的连续访问,并决定预读多少内容。

f_owner:该结构存储了一个进程id,以及当特定事件发生在该文件时发送的一个信号,比如当有新数据到来的时候等等。

f_uid, f_gid:打开该文件的进程的uid和gid,没有实际的用途。

f_version:用来帮助底层文件系统检查cache的状态是否合法。当f_pos变化时,它的值就会发生变化。

private_data:这个域被许多设备驱动所使用,有些特殊的文件系统为每一个打开的文件都保存一份额外的数据(如coda),也会使用这个域。

下面我们看一看针对文件的操作,在file结构中,有一个指针指向了一个文件操作集file_operations,它在linux/fs.h中被定义:

struct file_operations {

struct module *owner;

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

int (*readdir) (struct file *, void *, filldir_t);

unsigned int (*poll) (struct file *, struct poll_table_struct *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

int (*mmap) (struct file *, struct vm_area_struct *);

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *);

int (*release) (struct inode *, struct file *);

int (*fsync) (struct file *, struct dentry *, int datasync);

int (*fasync) (int, struct file *, int);

int (*lock) (struct file *, int, struct file_lock *);

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

};

这些操作用来将VFS对file结构的操作转化为逻辑文件系统处理相应操作的函数。因此,要了解一个逻辑文件系统,就要从这些接口函数入手。下面对这些操作进行一个简单的分析:

llseek:该函数用来实现lseek系统调用。如果它没有定义,则缺省执行fs/read_write.c中的default_llseek函数。它将更新fs_pos域,并且,也有可能会改变f_reada和f_version域。

read:该函数用来实现read系统调用,同时也支持其他诸如装载可执行文件等等操作。

write:该方法用来写文件。但它并不关心数据是否真正写入到设备,只将数据放入队列中。

readdir:该函数从一个假定为目录的文件读取目录结构,并且使用回调函数filldir_t将其返回。当readdir到达目录的结尾处时,它会返回0。

poll:该函数用来实现select和poll系统调用。

ioctl:该函数实现专门的ioctl功能。如果一个ioctl请求不在标准请求中(FIBMAP,FIGETBSZ,FIONREAD),那么该请求将传递给底层的文件实现。

mmap:该例程用来实现文件的内存映射。它通常使用generic_file_map来实现。使用它的任务会被检验是否允许作映射,并且会设置vm_area_struct中的vm_ops。

open:如果该方法被定义,那么当一个新的文件在索引节点上被打开时,会调用它。它可以做一些打开文件所必须的设置。在许多文件系统上,都不需要它。一个例外是coda,它需要在打开时试图获得本地缓存的文件。

flush:当一个文件描述符被关闭时,会调用该函数。由于此时可能有其他的描述符在该文件上被打开,因此,它并不意味着该文件被最终关闭。目前在文件系统中,只有NFS的客户端定义了该方法。

release:当文件的最后一个句柄被关闭时,release将被调用。它会做一些必要的清理工作。该函数不能向任何方面返回错误值,因此应该将其定义为void。

fsync:该方法用来实现fsync和fdatasync系统调用(它们一般是相同的)。它将一直等到所有对该文件挂起的写操作全部成功写到设备后才返 回。fsync可以部分地通过generic_buffer_fdatasync实现,这个函数将索引节点映射的页面中所有标记为脏的缓冲区,全部写回。

fasync:该方法在一个文件的FIOASYNC标志被改变的时候被调用。它的int类型的参数包含了该标志位的新值。目前还没有文件系统实现该方法。

lock:该方法允许一个文件服务提供额外的POSIX锁。它不被FLOCK类型的锁使用,它对于网络文件系统比较有用。

3 VFS索引节点及其操作

Linux 维护了一个活动的及最近使用过的索引节点的高速缓存(cache)。有两种方法来访问这些索引节点。第一种是通过dcache,我们将在下一节介绍。在 dcache中的每一个dentry都指向一个索引节点,并且因此而将索引节点维护在缓存中。第二种方法是通过索引节点的哈希表。每一个索引节点都被基于该文件系统超级块的地址和索引节点的编号,被哈希为一个8位的数字。所有拥有同样哈希值的索引节点通过双项链表被链接在一起。

通过哈希表访问是通过函数iget而实现的。iget只被个别的文件系统实现所调用(当索引节点不再dcache中而进行查找的时候)。

下面我们来分析索引节点inode的结构,在include/linux/fs.h中有inode的定义:

struct inode {

struct list_head i_hash;

struct list_head i_list;

struct list_head i_dentry;

struct list_head i_dirty_buffers;

unsigned long i_ino;

atomic_t i_count;

kdev_t i_dev;

umode_t i_mode;

nlink_t i_nlink;

uid_t i_uid;

gid_t i_gid;

kdev_t i_rdev;

loff_t i_size;

time_t i_atime;

time_t i_mtime;

time_t i_ctime;

unsigned long i_blksize;

unsigned long i_blocks;

unsigned long i_version;

unsigned short i_bytes;

struct semaphore i_sem;

struct semaphore i_zombie;

struct inode_operations *i_op;

struct file_operations *i_fop;

struct super_block * i_shadow;

struct inode_shadow_operations * i_shadow_op;

struct super_block *i_sb;

wait_queue_head_t i_wait;

struct file_lock *i_flock;

struct address_space *i_mapping;

struct address_space i_data;

struct dquot *i_dquot[MAXQUOTAS];

struct pipe_inode_info *i_pipe;

struct block_device *i_bdev;

unsigned long i_dnotify_mask; /* Directory notify events */

struct dnotify_struct *i_dnotify; /* for directory notifications */

unsigned long i_state;

unsigned int i_flags;

unsigned char i_sock;

atomic_t i_writecount;

unsigned int i_attr_flags;

__u32 i_generation;

union {

struct minix_inode_info minix_i;

struct ext2_inode_info ext2_i;

………

(略)

struct proc_inode_info proc_i;

struct socket socket_i;

struct usbdev_inode_info usbdev_i;

struct supermount_inode_info supermount_i;

void *generic_ip;

} u;

};

下面我们对它所一个分析,在上面的结构中,大部分字段的意义都很明显,因此我们将对一些特殊的字段(针对linux)和一些特殊的地方进行分析。

i_hash:i_hash将所有拥有相同哈希值的索引节点链接在一起。哈希值基于超级块结构的地址和索引节点的索引号。

i_list:i_list用来将索引节点链接到不同的状态上。inode_in_use链表将正在使用的未改变的索引节点链接在一 起,inode_unused将未使用的索引节点链接在一起,而superblock->s_dirty维护指定文件系统内所有标记为“脏”的索引 节点。

i_dentry:i_dentry链表中,链接了所有引用该索引节点的dentry结构。它们通过dentry中的d_alias链接在一起。

i_version:它被文件系统用来记录索引节点的改变。一般来说,i_version被设置为全局变量event的值,然后event回自增。有时候文件系统的代码会把i_version的当前值分配给相关的file结构中的f_version,在随后file结构的应用中,它可以被用来告诉我们,inode是否被改变了,如果需要的话,在file结构中缓存的数据要被刷新。

i_sem:这个信号灯用来保护对inode的改变。所有对inode的非原子操作代码,都要首先声明该信号灯。这包括分配和销毁数据块,以及通过目录进行查找等等操作。并且不能对只读操作声明共享锁。

i_flock:它指向在该inode上加锁的file_lock结构链表。

i_state:对于2.4内核来说,共有六种可能的inode状态:I_DIRTY_SYNC,I_DIRTY_DATASYNC, I_DIRTY_PAGES,I_LOCK,I_FREEING和 I_CLEAR。所有脏节点在相应超级块的s_dirty链表中,并且在下一次同步请求时被写入设备。在索引节点被创建,读取或者写入的时候,会被锁住,即I_LOCK状态。当一个索引节点的引用计数和链接计数都到0时,将被设置为I_CLEAR状态。

i_flags:i_flags对应于超级块中的s_flags,有许多标记可以被系统范围内设置,也可以针对每个索引节点设置。

i_writecount:如果它的值为正数,那么它就记录了对该索引节点有写权限的客户(文件或者内存映射)的个数。如果是负数,那么该数字的绝对值就是当前VM_DENYWRITE映射的个数。其他情况下,它的值为0。

i_attr_flags:未被使用。

最后要注意的是,在linux 2.4中,inode结构中新增加了一项,就是struct file_operations *i_fop,它指向索引节点对应的文件的文件操作集,而原来它放在inode_operations中(即inode结构的另一个项目struct inode_operations *i_op之中),现在它已经从inode_operations中移走了,我们可以从下面对inode_operations结构的分析中看到这一点。

下面我们分析一下对于inode进行操作的函数。所有的方法都放在inode_operations结构中,它在include/linux/fs.h中被定义:

struct inode_operations {

int (*create) (struct inode *,struct dentry *,int);

struct dentry * (*lookup) (struct inode *,struct dentry *);

int (*link) (struct dentry *,struct inode *,struct dentry *);

int (*unlink) (struct inode *,struct dentry *);

int (*symlink) (struct inode *,struct dentry *,const char *);

int (*mkdir) (struct inode *,struct dentry *,int);

int (*rmdir) (struct inode *,struct dentry *);

int (*mknod) (struct inode *,struct dentry *,int,int);

int (*rename) (struct inode *, struct dentry *,

struct inode *, struct dentry *);

int (*readlink) (struct dentry *, char *,int);

int (*follow_link) (struct dentry *, struct nameidata *);

void (*truncate) (struct inode *);

int (*permission) (struct inode *, int);

int (*revalidate) (struct dentry *);

int (*setattr) (struct dentry *, struct iattr *);

int (*getattr) (struct dentry *, struct iattr *);

};

同样,我们对这些方法做一个简单的分析。

create:这个方法,以及下面的8个方法,都只在目录索引节点中被维护。

当 VFS想要在给定目录创建一个给定名字(在参数dentry中)的新文件时,会调用该函数。VFS将提前确定该名字并不存在,并且作为参数的dentry 必须为负值(即其中指向inode的指针为NULL,根据include/dcache.h中的定义,其注释为“NULL is negative”)。

如果create调用成功,将使用get_empty_inode从cache中得到一个新的空索引节点,填充它的内容,并使用 insert_inode_hash将其插入到哈希表中,使用mark_inode_dirty标记其为脏,并且使用d_instantiate将其在 dcache中实例化。

int参数包含了文件的mode并指定了所需的许可位。

lookup:该函数用来检查是否名字(由dentry提供)存在于目录(由inode提供)中,并且如果存在的话,使用d_add更新dentry。

link:该函数用来将一个名字(由第一个dentry提供)硬链接到在在指定目录(由参数inode提供)中的另一个名字(由第二个dentry参数提供)。

unlink:删除目录中(参数inode指定)的名字(由参数dentry提供)。

symlink:创建符号链接。

mkdir:根据给定的父节点,名字和模式,创建一个目录。

rmdir:移除指定的目录(如果为空目录),并删除(d_delete)dentry。

mknod:根据给定的父节点,名字,模式以及设备号,创建特殊的设备文件,然后使用d_instantiate将新的inode在dentry中实例化。

rename:重命名。所有的检测,比如新的父节点不能是旧名字的孩子等等,都已经在调用前被完成。

readlink:通过dentry参数,读取符号链接,并且将其拷贝到用户空间,最大长度由参数int指定。

permission:在该函数中,可以实现真正的权限检查,与文件本身的mode无关。

4 VFS名字以及dentry

根据我们上面的介绍,可以看出,文件和索引节点的联系非常紧密,而在文件和索引节点之间,是通过dentry结构来联系的。

VFS层处理了文件路径名的所有管理工作,并且在底层文件系统能够看到它们之前,将其转变为dcache中的入口(entry)。唯一的一个例外是对于符号链接的目标,VFS将不加改动地传递给底层文件系统,由底层文件系统对其进行解释。

目录高速缓存dcache由许多dentry结构组成。每一个dentry都对应文件系统中的一个文件名,并且与之联系。每一个dentry的父节点都必须存在于dcache中。同时,dentry还记录了文件系统的装载关系。

dcache是索引节点高速缓存的管理者。不论何时,只要在dcache中存在一个入口,那么相应的索引节点一定在索引节点高速缓存中。换句话说,如果一个索引节点在高速缓存中,那么它一定引用dcache中的一个dentry。

下面我们来分析一下dentry的结构,以及在dentry上的操作。在include/linux/dcache.h中,由其定义:

struct dentry {

atomic_t d_count;

unsigned int d_flags;

struct inode * d_inode; /* Where the name belongs to - NULL is negative */

struct dentry * d_parent; /* parent directory */

struct list_head d_vfsmnt;

struct list_head d_hash; /* lookup hash list */

struct list_head d_lru; /* d_count = 0 LRU list */

struct list_head d_child; /* child of parent list */

struct list_head d_subdirs; /* our children */

struct list_head d_alias; /* inode alias list */

struct qstr d_name;

unsigned long d_time; /* used by d_revalidate */

struct dentry_operations *d_op;

struct super_block * d_sb; /* The root of the dentry tree */

unsigned long d_reftime; /* last time referenced */

void * d_fsdata; /* fs-specific data */

unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */

};

在该结构的注释中,大部分域的含义已经非常的清楚,下面我再简单地介绍一下。

d_flags:在目前,只有两个可取值,而且都是给特殊的文件系统使用的,它们是DCACHE_AUTOFS_PENDING和DCACHE_NFSFS_RENAMED,因此,在这里我们可以暂时忽略它。

d_inode:它简单地指向与该名字联系的索引节点。这个域可以是NULL,它标明这是一个负入口(negative entry),暗示着该名字并不存在。

d_hash:这是一个双向链表,将所有拥有相同哈希值的入口链接在一起。

d_lru:它提供了一个双向链表,链接高速缓存中未被引用的叶节点。这个链表的头是全局变量dentry_unused,按照最近最少使用的顺序存储。

d_child:这是一个容易让人误会的名字,其实该链表链接d_parent的所有子节点,因此把它称为d_sibling(同胞)更恰当一些。

d_subdirs:该链表将该dentry的所有子节点链接在一起,所以,它实际上是它子节点的d_child链表的链表头。这个名字也容易产生误会,因为它的子节点不仅仅包括子目录,也可以是文件。

d_alias:由于文件(以及文件系统的其他一些对象)可能会通过硬链接的方法,拥有多个名字,因此有可能会有多个dentry指向同一个索引节点。在这种情况下,这些dentry将通过d_alias链接在一起。而inode的i_dentry就是该链表的头。

d_name:该域包含了这个入口的名字,以及它的哈希值。它的子域name有可能会指向该dentry的d_iname域(如果名字小于等于16个字符),否则的话,它将指向一个单独分配出来的字符串。

d_op:指向dentry的操作函数集。

d_sb:指向该dentry对应文件所在的文件系统的超级块。使用d_inode->i_sb有相同的效果。

d_iname:它存储了文件名的前15个字符,目的是为了方便引用。如果名字适合,d_name.name将指向这里。

下面我们再看一下对dentry的操作函数,同样在include/linux/dcache.h中有dentry_operations的定义:

struct dentry_operations {

int (*d_revalidate)(struct dentry *, int);

int (*d_hash) (struct dentry *, struct qstr *);

int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);

int (*d_delete)(struct dentry *);

void (*d_release)(struct dentry *);

void (*d_iput)(struct dentry *, struct inode *);

};

我们再简单地介绍一下:

d_revalidate:这个方法在entry在dcache中做路径查找时调用,目的是为了检验这个entry是否依然合法。如果它依旧可以被信赖,则返回1,否则返回0。

d_hash:如果文件系统没有提供名字验证的规则,那么这个例程就被用来检验并且返回一个规范的哈希值。

d_compare:它被用来比较两个qstr,来看它们是否是相同的。

d_delete:当引用计数到0时,在这个dentry被放到dentry_unused链表之前,会调用该函数。

d_release:在一个dentry被最终释放之前,会调用该函数。

_iput:如果定义了该函数,它就被用来替换iput,来dentry被丢弃时,释放inode。它被用来做iput的工作再加上其他任何想要做的事情。

(五) 总结

在上面的部分中,我们对VFS进行了一个大概的分析。了解了文件系统的注册,安装,以及卸载的过程,并对VFS对逻辑文件系统的管理,尤其是接口部分的数据结构,进行了详细的分析。

proc文件系统作为一个特殊的逻辑文件系统,其实现也遵循VFS接口,因此根据上面对VFS的分析,我们可以基本确定对proc文件系统进行分析的步骤。

二、proc文件系统分析

根据前面的分析,我们可以基本确定对proc文件系统的分析步骤。我将按照proc文件系统注册,安装的顺序对其进行分析,然后基于代码,对proc文件 系统的结构进行分析,尤其是proc文件系统用于内部管理的数据结构。最后我们将根据分析结果,提出可行的xml封装计划。

在对proc文件系统的数据结构的分析中,我将把重点放在数据输出的分析上,它是提出一种标准的XML封装方法的基础。

(一) Linux 相关源代码简介

在linux代码树中,所有文件系统的代码都放在linux/fs/目录中,其中,proc文件系统的源代码在linux/fs/proc中,下面我简单介绍一下proc目录中的源文件。

在目录*有11个相关文件,它们是:

procfs_syms.c inode.c generic.c base.c

array.c root.c proc_tty.c proc_misc.c

kmsg.c kcore.c proc_devtree.c

其中,procfs_syms.c,generic.c以及inode.c与proc文件系统的管理相关,包括proc文件系统的注册,以及向内核其他子系统提供的例程等等,这是最重要的一部分代码,我们将从这里开始对proc文件系统进行分析。

源文件root.c与proc文件系统的根结点的管理相关。而base.c,array.c则用来处理/proc目录中进程的信息,包括命令行,进程状态,内存状态等等与进程相关的内容。proc_tty.c用来处理/proc/tty信息,proc_misc.c则用来管理与/proc目录中的大多数文件。除此之外,还有两个非常重要的头文件proc_fs.h,proc_fs_i.h,我们可以在/linux/include/linux/目录中找到。

(二) proc文件系统的注册

proc文件系统遵循VFS的规范,因此在使用之前,必须进行注册。我们知道,每一个文件系统,都会在自己的初始化例程中填写一个 file_system_type 的数据结构,然后调用注册函数register_filesystem(struct file_system_type *fs) 进行注册。

proc文件系统中与之相关的文件是procfs_syms.c,在该文件中,声明了proc文件系统的类型:

static DECLARE_FSTYPE(proc_fs_type, "proc", proc_read_super, FS_SINGLE);

而我们在 fs.h 中可以找到宏DECLARE_FSTYPE的定义:

#define DECLARE_FSTYPE(var,type,read,flags) /

struct file_system_type var = { /

name: type, /

read_super: read, /

fs_flags: flags, /

owner: THIS_MODULE, /

}

因此我们可以看到,我们声明了一个文件类型proc_fs_type,它的名字是“proc”,读取超级块的函数是 proc_read_super,fs_flags设置为FS_SINGLE,根据源码中的说明,我们知道,当文件系统的fs_flags声明为 FS_SINGLE时,说明文件系统只有一个超级块,并且必须在注册函数之后调用kern_mount(),使得在内核范围内的vfsmnt被放置在 ->kern_mnt处。

下面就是proc文件系统的注册,函数init_proc_fs()的代码如下所示:

static int __init init_proc_fs(void) {

int err = register_filesystem(&proc_fs_type);

if (!err) {

proc_mnt = kern_mount(&proc_fs_type);

err = PTR_ERR(proc_mnt);

if

(IS_ERR(proc_mnt))

unregister_filesystem(&proc_fs_type);

else

err = 0;

}

return err;

}

可以看到,proc文件系统的注册非常简单,主要有如下几个步骤:

1.调用register_filesystem(&proc_fs_type),用一个非常巧妙的方法将proc文件类型加入到文件类型的单向链表中,如果发生错误,则返回。

2.调用kern_mount函数,该函数基本完成三个步骤,首先调用read_super()函数,在这个函数里,VFS将为proc文件系统分配一个 超级块结构,并设置s_dev,s_flags等域,然后将调用proc文件系统的自己的read_super例程,对应proc文件系统,该例程是 proc_read_super(),该例程将设置超级块结构的其他值。我们将在下一节进行分析。

其次,使用add_vfsmnt()函数建立proc文件系统的vfsmount结构,并将其加入到已装载文件系统的链表中。

最后,返回该vfsmount结构,并利用返回值,使用指针proc_mnt指向该vfsmount结构。

3.判断返回值是否错误,如果错误,那么就卸载文件系统。

这样,一个文件系统就成功注册到核心了。同样,proc文件系统的卸载,也非常简单,代码如下:

static void __exit exit_proc_fs(void){

unregister_filesystem(&proc_fs_type);

kern_umount(proc_mnt);

}

(三) 建立proc文件系统的超级块

我们刚才看到,在kern_mount函数中,调用read_proc建立了超级块结构,然后就会调用文件系统自己提供的读取超级块的例程,用来填充自己 的超级块结构,下面我们看一下proc文件系统的超级块读取例程proc_read_super()是如何工作的,以及它最终完成了哪些工作,该函数在 fs/proc/inode.c中实现:

struct super_block *proc_read_super(struct super_block *s,void *data,

int silent) {

struct inode * root_inode;

struct task_struct *p;

s->s_blocksize = 1024;

s->s_blocksize_bits = 10;

s->s_magic = PROC_SUPER_MAGIC;

s->s_op = &proc_sops;

s->s_maxbytes = MAX_NON_LFS;

root_inode = proc_get_inode(s, PROC_ROOT_INO, &proc_root);

if (!root_inode)

goto out_no_root;

/*

* Fixup the root inode's nlink value

*/

read_lock(&tasklist_lock);

for_each_task(p) if (p->pid) root_inode->i_nlink++;

read_unlock(&tasklist_lock);

s->s_root = d_alloc_root(root_inode);

if (!s->s_root)

goto out_no_root;

parse_options(data, &root_inode->i_uid, &root_inode->i_gid);

return s;

out_no_root:

printk("proc_read_super: get root inode failed/n");

iput(root_inode);

return NULL;

}

该函数进行了如下几步操作:

在该函数里,首先向作为参数传入的超级块写入文件系统的基本信息,s_blocksize设置为1024,由于1024=2^10,因此,s_blocksize_bit设置为10,然后是proc文件系统的魔数,为PROC_SUPER_MAGIC。超级块的函数集设置为 proc_sops,对于proc文件系统来讲,只实现了4个超级块函数,我们将在后面进行分析。然后,设置proc文件系统中的文件最大字节数为 MAX_NON_LFS,在fs.h中,定义这个宏为 ((1ULs_root = d_alloc_root(root_inode)

其中root_inode 的类型是struct inode *, 而s_root的类型是struct dentry *。我们在介绍VFS的时候知道,目录高速缓存以树状结构存在,因此在建立文件系统的根结点后,需要使用d_alloc_root()函数建立一个根目 录(root dentry),也就是说,该dentry结构的。

最终成功返回超级块,这时超级块已经填上了必要的数据信息。因此可以看到,超级块读取例程主要完成了两部分的工作,首先向超级块写入必要的数据,其次建立了该文件系统的根结点,并在目录高速缓存中建立了相应的dentry结构。

(四) proc文件系统超级块的操作函数集

在上一节我们看到了proc文件系统如何设置自己的超级块,并且将超级块操作函数集设置为proc_sops,这一节我们就分析一下,对于proc文件系统的超级块,需要提供什么操作,以及如何实现这些操作。

在文件fs/proc/inode.c中,有如下定义:

static struct super_operations proc_sops = {

read_inode: proc_read_inode,

put_inode: force_delete,

delete_inode: proc_delete_inode,

statfs: proc_statfs,

};

我们可以看到,proc文件系统仅仅实现了4个超级块操作函数。它使用了一种比较特殊的方法来初始化结构,这种方法叫作labeled elements,这是GNU的C扩展,这样在初始化结构时,不必按照结构的顺序,只要指明域名,就可初始化其值,而对于没有提到的域,将自动设置为0。

所以我们看到,proc文件系统仅仅定义了4个超级块操作函数,我们看一下为什么其他的操作函数不必定义。

首先,我们知道,proc文件系统仅仅存在于内存中,并不需要物理设备,因此write_inode函数就不需要定义了。而函数 notify_change,在索引节点的属性被改变的时候会被调用,而对于proc文件系统的inode来说,并未提供setattr 函数,换句话说,文件的属性不会被改变,所以,notif_change也就不会被调用(proc文件系统对于inode_operations,同样仅 仅提供了很少的几种操作,并且,在建立文件树的时候,还针对不同的文件/目录,设置了不同的索引节点操作函数,这将在以后进行详细的介绍)。基于类似的原 因,其他的函数,诸如put_super,write_super,以及clear_inode等等函数,都没有进行定义。

下面我们看一下定义的这4个函数:

1 read_inode: proc_read_inode

这个函数用来从已装载文件系统中,读取指定索引节点的信息。实际上,在需要读取特定的索引节点时,会调用VFS的iget(sb, ino)函数,其中,sb指定了文件系统的超级块,而ino是索引节点的标号。这个函数会在该超级块的dcache中寻找该索引节点,如果找到,则返回索引节点,否则就必须从逻辑文件系统中读取指定的索引节点,这时会调用get_new_inode()函数,在这个函数里,会分配一个inode结构,填写一些基本的信息,然后就会调用超级块的操作函数read_inode,对于proc文件系统而言,就是proc_read_inode()函数。

在后面的介绍里我们会知道,proc文件系统为了方便自己对文件的管理,对于每一个已经注册的proc文件,都建立并维护了一个的 proc_dir_entry结构。这个结构非常的重要,对于proc文件系统来说,这个结构是自己的私有数据,相当于其他逻辑文件系统(比如ext2文 件系统)在物理硬盘上的索引节点。因此,只有在必要的时候,才会把proc文件系统的proc_dir_entry结构链接到VFS的索引节点中。

因此,proc_read_inode函数的主要目的,是建立一个新的索引节点,只需填充一些基本的信息即可。所以我们可以看到proc_read_inode函数非常的简单:

static void proc_read_inode(struct inode * inode){

inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;

}

要说明的是,在调用proc_read_inode函数之前,VFS的get_new_inode()函数已经为inode设置了其他的基本信息,比如i_sb,i_dev,i_ino,i_flags 以及i_count等等。

2 put_inode: force_delete

put_inode函数是在索引节点的引用计数减少的时候调用,我们看到,proc文件系统没有实现自己的put_inode函数,而是简单地设置了VFS的force_delete 函数,我们看一下这个函数的内容:

void force_delete(struct inode *inode){

/*

* Kill off unused inodes ... iput() will unhash and

* delete the inode if we set i_nlink to zero.

*/

if (atomic_read(&inode->i_count) == 1)

inode->i_nlink = 0;

}

我们知道,put_inode函数是在引用计数i_count减少之前调用的,因此,对于proc文件系统来说,在每一次inode引用计数减少之前,都要检查引用计数会不会减少至零,如果是,那么就将改索引节点的链接数直接设置为零。

3 delete_inode: proc_delete_inode

当一个索引节点的引用计数和链接数都到零的时候,会调用超级块的delete_inode函数。由于我们使用force_delete实现了proc超级 块的put_inode方法,因此我们知道,对于proc文件系统来说,当一个inode的引用计数为零的时候,它的链接数也必为零。我们看一下该函数的源码:

/*

* Decrement the use count of the proc_dir_entry.

*/

static void proc_delete_inode(struct inode *inode) {

struct proc_dir_entry *de = inode->u.generic_ip;/* for the procfs, inode->u.generic_ip is a 'proc_dir_entry' */

inode->i_state = I_CLEAR;

if (PROC_INODE_PROPER(inode)) {

proc_pid_delete_inode(inode);

return;

}

if (de) {

if (de->owner)

__MOD_DEC_USE_COUNT(de->owner);

de_put(de);

}

}

我们看到,这个函数基本上做了三个工作,首先,将这个索引节点的状态位设置为I_CLEAR,这标志着,这个inode结构已经不再使用了。其次根据这个索引节点的ino号,检查它是否是pid目录中的索引节点,因为pid目录的索引节点号使用。

#define fake_ino(pid,ino) (((pid)f_type = PROC_SUPER_MAGIC; /* here use the super_block's s_magic ! */

buf->f_bsize = PAGE_SIZE/sizeof(long); /* optimal transfer block size */

buf->f_bfree = 0; /* free blocks in fs */

buf->f_bavail = 0; /* free blocks avail to non-superuser */

buf->f_ffree = 0; /* free file nodes in fs */

buf->f_namelen = NAME_MAX; /* maximum length of filenames */

return 0;

}

我们看到,它将文件系统的统计数据填充到一个buf中,文件系统类型为PROC_SUPER_MAGIC,在文件系统中的空闲块以及文件系统中的文件节点都设置为0,因此对于只存在于内存中的proc文件系统来说,这些统计数据是没有意义的。

(五) 对proc文件的管理

前面我们提过,相对于其他逻辑文件系统的具体文件组织形式(比如ext2文件系统的inode),proc文件系统也有自己的组织结构,那就是 proc_dir_entry结构,所有属于proc文件系统的文件,都对应一个proc_dir_entry结构,并且在VFS需要读取proc文件的 时候,把这个结构和VFS的inode建立链接(即由inode->u.generic_ip指向该prc_dir_entry结构)。

因此,proc文件系统实现了一套对proc_dir_entry结构的管理,下面我们就此进行一个分析。

1 proc_dir_entry结构

首先我们看一下proc_dir_entry结构,这个结构在proc_fs.h中定义:

struct proc_dir_entry {

unsigned short low_ino;

unsigned short namelen;

const char *name;

mode_t mode;

nlink_t nlink;

uid_t uid;

gid_t gid;

unsigned long size;

struct inode_operations * proc_iops;

struct file_operations * proc_fops;

get_info_t *get_info;

struct module *owner;

struct proc_dir_entry *next, *parent, *subdir;

void *data;

read_proc_t *read_proc;

write_proc_t *write_proc;

atomic_t count; /* use count */

int deleted; /* delete flag */

kdev_t rdev;

};

在这个结构中,描述了一个proc文件的全部信息,每一个proc文件正是使用proc_dir_entry结构来表示的。下面我们看一下它最重要的几个域:

low_ino:这是用来唯一标志proc_dir_entry结构的节点号,也就是proc文件系统内的索引节点的标号,除了根结点,其他的节点号都是在创建proc_dir_entry的时候,由make_inode_number()动态创建的。

name:即这个proc文件的名字。

mode:该proc文件的模式由两部分用位或运算组成,第一部分是文件的类型,可以参考include/linux/stat.h中的定义,比 如,S_IFREG表示普通文件,而S_IFDIR表示目录文件。第二部分是该文件的权限,同样可以参考include/linux/stat.h中的定 义,比如,S_IRUSR表示该文件能够被拥有者读,S_IROTH 表示该文件可以被其他人读取。但真正的权限检查,我们可以放到后面提到的inode_operations结构中。

size:即我们使用“ls”命令时,所显示出的文件大小。

proc_iops:这是一个inode_operations结构,其中设置了针对这个proc索引节点的操作函数,这样,我们就可以针对不同类型的 proc文件,提供不同的方法,以完成不同的工作。比如我们上面提到的对proc文件的权限检查,就可以放在这个结构中。

proc_fops:这是一个file_operations结构,其中放置了针对这个proc文件的操作函数,我们可以把对proc文件的读写操作,放在这个结构中,用以实现对/proc目录中的文件的读,写功能。

get_info:当用户向proc文件读取的数据小于一个页面大小时,可以使用这个函数向用户返回数据。

struct proc_dir_entry *next, *parent, *subdir:使用这些链表,在内存中,proc_dir_entry结构就以树的形式链接在一起。read_proc_t *read_proc 和write_proc_t *write_proc:这两个函数提供了对proc文件进行操作的简单接口。我们知道,对于proc文件,我们可以从中读取核心数据,还可以向其中写入 数据,因此,对于一些功能比较简单的proc文件,我们只要实现这两个函数(或其中之一)即可,而不用设置inode_operations结构,这样整个操作比较简单。实际上,我们会在后面的分析中看到,在注册proc文件的时候,会自动为proc_fops设置一个缺省的 file_operations结构,如果我们只实现了上面提到的两个读写操作,而没有设置自己file_operations结构,那么会由缺省的 inode_operations结构中的读写函数检查调用这两个函数。

atomic_t count:该结构的使用计数。当一个proc_dir_entry结构的count减为零时,会释放该结构,这种结果就像把一个ext2文件系统的文件从磁盘上删除掉一样。

int deleted:这是一个删除标志,当我们调用remove_proc_entry函数要删除一个proc_dir_entry时,如果发现该结构还在使用,就会设置该标志并且推出。

2 建立proc文件

在了解了proc_dir_entry结构之后,我们来看一看proc文件系统是如何管理自己的文件结构的。首先我们看一看它是如何创建proc文件的,参考文件fs/proc/generic.c,其中,有一个函数create_proc_entry,由它创建并注册proc文件,下面我们看一下它的源码:

struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent) {

struct proc_dir_entry *ent = NULL;

const char *fn = name;

int len;

if (!parent && xlate_proc_name(name, &parent, &fn) != 0)

goto out;

len = strlen(fn);

ent = kmalloc(sizeof(struct proc_dir_entry) + len + 1, GFP_KERNEL);

if (!ent)

goto out;

memset(ent, 0, sizeof(struct proc_dir_entry));

memcpy(((char *) ent) + sizeof(*ent), fn, len + 1);

ent->name = ((char *) ent) + sizeof(*ent);

ent->namelen = len;

if (S_ISDIR(mode)) {

if ((mode & S_IALLUGO) == 0)

mode |= S_IRUGO | S_IXUGO;

ent->proc_fops = &proc_dir_operations;

ent->proc_iops = &proc_dir_inode_operations;

ent->nlink = 2;

} else {

if ((mode & S_IFMT) == 0)

mode |= S_IFREG;

if ((mode & S_IALLUGO) == 0)

mode |= S_IRUGO;

ent->nlink = 1;

}

ent->mode = mode;

proc_register(parent, ent); /* link ent to parent */

out:

return ent;

}

我们看到,首先该函数会做一些必要的检查,比如要确保它的父节点必须存在等等。其次会创建一个proc_dir_entry结构,并且为该文件的名字也分配空间,并用->name指向它。再次,会根据该文件的类型,设置适当的模式和链接数。最后,会调用proc_register(parent, ent)函数,将这个结构链接到proc文件树中。下面我们看一下它的实现代码:

static int proc_register(struct proc_dir_entry * dir, struct proc_dir_entry * dp) {

int i;

i = make_inode_number();

if (i low_ino = i;

dp->next = dir->subdir;

dp->parent = dir;

dir->subdir = dp;

if (S_ISDIR(dp->mode)) {

if (dp->proc_iops == NULL) {

dp->proc_fops = &proc_dir_operations;

dp->proc_iops = &proc_dir_inode_operations;

}

dir->nlink++;

} else if (S_ISLNK(dp->mode)) {

if (dp->proc_iops == NULL)

dp->proc_iops = &proc_link_inode_operations;

} else if (S_ISREG(dp->mode)) {

if (dp->proc_fops == NULL)

dp->proc_fops = &proc_file_operations;

}

return 0;

}

这个函数主要完成三部分的工作,

第一,使用make_inode_number()函数动态的到一个节点号,并且设置low_ino。

第二步,将这个 proc_dir_entry结构链接到它的父节点上。

第三步,根据文件类型的不同,设置不同的(索引节点和文件)缺省操作函数集。这样,一个proc文件就注册成功了。

3 删除proc文件

在同一源文件中,提供了删除proc_dir_entry结构的函数,即remove_proc_entry,下面我们分析一下它的实现过程。

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

struct proc_dir_entry **p;

struct proc_dir_entry *de;

const char *fn = name;

int len;

if (!parent && xlate_proc_name(name, &parent, &fn) != 0)

goto out;

len = strlen(fn);

for (p = &parent->subdir; *p; p=&(*p)->next ) {

if (!proc_match(len, fn, *p))

continue;

de = *p;

*p = de->next;

de->next = NULL;

if (S_ISDIR(de->mode))

parent->nlink--;

clear_bit(de->low_ino-PROC_DYNAMIC_FIRST,

(void *) proc_alloc_map);

proc_kill_inodes(de);

de->nlink = 0;

if (!atomic_read(&de->count))

free_proc_entry(de);

else {

de->deleted = 1;

printk("remove_proc_entry: %s/%s busy, count=%d/n",

parent->name, de->name, atomic_read(&de->count));

}

break;

}

out:

return;

}

该函数在参数parent的所有孩子中查找指定的名字,如果找到匹配的节点,即proc_match(len, fn, *p),那么,就将该结构从树结构中去掉。然后,如果删除的proc_dir_entry是目录结构,那么就减少其父节点的链接数。

然后调用clear_bit(de->low_ino-PROC_DYNAMIC_FIRST, (void *) proc_alloc_map)函数,清除该节点号。最后,将该结构的链接数置零,并调用atomic_read(&de->count)来检查它的引用计数,如果是零,那么就使用函数free_proc_entry释放该节点,否则就将它的删除标记位置一,在以后适当地机会中,再将其释放。

4 其他管理函数

除此之外,我们看到还有一些函数,可以方便我们管理和使用proc文件系统,我们简单地介绍一下:

struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent)函数,这个函数用来在proc文件系统中注册一个子目录,根据它的参数,我们就可以看出它的功能。在这个函数里,将动态分配一个 proc_dir_entry结构以及它的名字,然后设置目录文件的缺省操作(proc_iops以及proc_fops)以及nlink值,最后,调 用proc_register函数将其注册。

struct proc_dir_entry *proc_mknod(const char *name, mode_t mode, struct proc_dir_entry *parent, kdev_t rdev)函数,用来在proc文件系统中建立一个设备文件,因此,在创建proc_dir_entry结构后,没有设置缺省操作,而是使用 ->rdev = rdev指定了设备。最后,调用proc_register函数将其注册。

struct proc_dir_entry *proc_symlink(const char *name, struct proc_dir_entry *parent, const char *dest)函数,该函数创建了一个链接文件,使用->mode = S_IFLNK|S_IRUGO|S_IWUGO|S_IXUGO来标志,它和其他文件的建立很相似,只是它将链接的目标文件名放在了 ->data域中。最后它同样调用proc_register函数将该结构注册。

(六) 对proc文件默认操作的分析

现在,我们已经基本清楚了proc文件系统对自己proc_dir_entry结构的管理了。下面我们回过头来,再看一下在文件注册函数中的一段代码:

if (S_ISDIR(dp->mode)) {

if (dp->proc_iops == NULL) {

dp->proc_fops = &proc_dir_operations;

dp->proc_iops = &proc_dir_inode_operations;

}

dir->nlink++;

} else if (S_ISLNK(dp->mode)) {

if (dp->proc_iops == NULL)

dp->proc_iops = &proc_link_inode_operations;

} else if (S_ISREG(dp->mode)) {

if (dp->proc_fops == NULL)

dp->proc_fops = &proc_file_operations;

}

我在前面已经提过,这段代码根据注册的proc文件类型的不同,为proc_dir_entry结构设置了不同的操作函数集。也就是说,我们使用封装的 create_proc_entry函数在proc文件系统中注册文件时,可以不用去管这些操作函数集,因为该结构总是自动地设置了相应的 proc_iops和proc_fops操作函数。下面我们就对这些默认的操作进行一个分析,因为这对我们了解proc文件系统和VFS的结构非常重要。

1 对普通文件的操作

我们首先看一下普通proc文件的函数集,根据代码段:

if (S_ISREG(dp->mode)) {

if (dp->proc_fops == NULL)

dp->proc_fops = &proc_file_operations;

}

我们可以看到,对于普通的proc文件,只设置了文件操作,即proc_file_operations,从这一点上可以看出,对于普通的proc文件,只缺省提供了文件操作,因此在必要的时候,我们必须手工设置需要的索引节点操作函数集,比如inode_operations中的权限检查函数 permission等等。 对于proc_file_operations,我们可以看到,只实现了三个函数:

static struct file_operations proc_file_operations = {

llseek: proc_file_lseek,

read: proc_file_read,

write: proc_file_write,

};

下面我们简单的看一下它们实现的功能:

(1)llseek: proc_file_lseek

这个函数,用来实现lseek系统调用,其功能是设置file结构的->f_pos域,因此根据第三个参数orig的不同,将f_pos设置为相应的值,该函数非常简单,因此不作过多的介绍。

(2)read: proc_file_read

这个函数是file_operations结构中的成员,在后面我们将看到,在proc_dir_entry结构中实现的file_operations 和inode_operations将链接至VFS的inode中,因此该函数将用来实现read系统调用。在这个函数中,首先根据file结构,得到相应的inode,然后由

struct proc_dir_entry

* dp;

dp = (struct proc_dir_entry *) inode->u.generic_ip;

而得到proc_dir_entry结构,然后,开始调用该proc_dir_entry结构中的函数,向用户空间返回指定大小的数据,我们看一下下面的代码片断:

if (dp->get_info) {

/*

* Handle backwards compatibility with the old net

* routines.

*/

n = dp->get_info(page, &start, *ppos, count);

if (n read_proc) {

n = dp->read_proc(page, &start, *ppos,

count, &eof, dp->data);

} else

break;

由此我们看出,该函数的实现依赖于proc_dir_entry结构中的get_info和read_proc函数,因此如果我们要注册自己的proc 文件,在不设置自己的proc_fops操作函数集的时候,必须实现上面两个函数中的一个,否则,这个缺省的proc_file_read函数将做不了任何工作:

在这个函数中,实现了从内核空间向用户空间传递数据的功能,其中使用了许多技巧,在这里就不作讨论了,具体实现可以参考源码。

(3)write: proc_file_write

与上面的函数类似,我们可以看到proc_file_write函数同样依赖于proc_dir_entry中的write_proc(file, buffer, count, dp->data)函数,它的实现非常简单:

static ssize_t

proc_file_write(struct file * file, const char * buffer,

size_t count, loff_t *ppos) {

struct inode *inode = file->f_dentry->d_inode;

struct proc_dir_entry * dp;

dp = (struct proc_dir_entry *) inode->u.generic_ip;

if (!dp->write_proc)

return -EIO;

/* FIXME: does this routine need ppos probably... */

return dp->write_proc(file, buffer, count, dp->data);

}

我们看到,它只是简单地检测了->write_proc函数是否存在,如果我们在proc_dir_entry结构中实现了这个函数,那么就调用它,否则就退出。

根据上面的讨论,我们看到,对于普通文件的操作函数,proc文件系统为我们提供了一个简单的封装,因此,我们只要在proc_dir_entry中实现相关的读写操作即可。

但是,如果我们想提供读写操作之外的函数,那么我们就可以定义自己的file_operations函数集,并且在proc文件注册后,将它链接到proc_dir_entry的proc_fops上,这样,就可以使用自己的函数集了。

2 对链接文件的操作

根据代码段:

else if (S_ISLNK(dp->mode)) {

if (dp->proc_iops == NULL)

dp->proc_iops = &proc_link_inode_operations;

我们可以看出,对于链接文件,proc文件系统为它设置了索引节点操作proc_iops。因为我们知道,一个符号链接,只拥有inode结构,而没有文件结构,所以,为它提供proc_link_inode_operations函数集就可以了。下面我们看一下,这个函数集的内容:

static struct inode_operations proc_link_inode_operations = {

readlink: proc_readlink,

follow_link: proc_follow_link,

};

这个函数集实现了和链接相关的两个函数,我们分别来看一下:

(1)readlink: proc_readlink

该函数用来实现readlink系统调用,它的功能是获得目标文件的文件名,我们在前面看到,对于一个链接文件,在注册时已经将链接目标的文件放在了 proc_dir_entry结构的->data域中(参考前面介绍的函数proc_symlink),因此,我们只要将->data中的数 据返回就可以了,它的代码如下:

static int proc_readlink(struct dentry *dentry, char *buffer, int buflen) {

char *s=

((struct proc_dir_entry *)dentry->d_inode->u.generic_ip)->data;

return vfs_readlink(dentry, buffer, buflen, s);

}

我们看到,这个函数使用一个指针指向->data,然后,使用VFS函数vfs_readlink将数据返回到用户空间,非常的简单。

(2)follow_link: proc_follow_link

这个函数代码如下:

static int proc_follow_link(struct dentry *dentry, struct nameidata *nd) {

char *s=

((struct proc_dir_entry *)dentry->d_inode->u.generic_ip)->data;

return vfs_follow_link(nd, s);

}

和上面介绍的函数类似,它同样利用VFS的函数实现其功能,对于vfs_follow_link,可以参考fs/namei.c文件。

3 对目录文件的操作

最后我们看一下proc文件系统对目录文件的操作函数集,在文件注册的时候,有如下代码:

if (S_ISDIR(dp->mode)) {

if (dp->proc_iops == NULL) {

dp->proc_fops = &proc_dir_operations;

dp->proc_iops = &proc_dir_inode_operations;

}

dir->nlink++;

}

从中我们可以看到,在proc文件系统中注册目录文件的时候,它会检查是否该proc_dir_entry结构已经注册了proc_iops函数集,如果没有,那么就为proc_fops和proc_iops设置相应的缺省函数集。下面我们对它们分别进行讨论:

1.对目录的文件操作proc_dir_operations:

static struct file_operations proc_dir_operations = {

read: generic_read_dir,

readdir: proc_readdir,

};

这个函数集的主要功能,是在由proc_dir_entry结构构成的proc文件树中解析目录。下面我们对这两个函数进行一个简单的分析:

(1)read: generic_read_dir

我们知道,对于read系统调用,当其参数文件句柄指向目录的时候,将返回EISDIR错误。因此,目录文件的read函数将完成这个工作。generic_read_dir函数是VFS提供的通用函数,可以参考fs/read_write.c文件:

ssize_t generic_read_dir(struct file *filp, char *buf, size_t siz, loff_t *ppos){

return –EISDIR;

}

这个函数很简单,只要返回错误码就可以了。

(2)readdir: proc_readdir

这个函数用来实现readdir系统调用,它从目录文件中读出dirent结构到内存中。我们可以参考fs/readdir.c中的filldir()函数。

2.对目录文件索引节点的操作函数:proc_dir_inode_operations

首先,我们看一下proc_dir_inode_operations的定义:

/*

* proc directories can do almost nothing..

*/

static struct inode_operations proc_dir_inode_operations = {

lookup: proc_lookup,

};

我们看到,对于目录文件的索引节点,只定义了一个函数lookup。因为我们在前面对VFS进行分析的时候知道,以下操作,是只在目录节点中定义的:

int (*create) (struct inode *,struct dentry *,int);

struct dentry * (*lookup) (struct inode *,struct dentry *);

int (*link) (struct dentry *,struct inode *,struct dentry *);

int (*unlink) (struct inode *,struct dentry *);

int (*symlink) (struct inode *,struct dentry *,const char *);

int (*mkdir) (struct inode *,struct dentry *,int);

int (*rmdir) (struct inode *,struct dentry *);

int (*mknod) (struct inode *,struct dentry *,int,int);

int (*rename) (struct inode *, struct dentry *,

struct inode *, struct dentry *);

但是经过我们对proc文件系统的分析,我们知道,proc文件系统中的文件都是在内核代码中通过proc_dir_entry实现的,因此它不提供目录索引节点的create,link,unlink,symlink,mkdir,rmdir,mknod,rename方法,也就是说,用户是不能通过 shell命令在/proc目录中对proc文件进行改名、删除、建子目录等操作的。这也算是proc文件系统的一种保护策略。

而在内核中,则使用proc_mkdir,proc_mknod等函数,在核心内通过代码来维护proc文件树。由此可以看出虚拟文件系统的一些特性。对目录文件的默认操作,可以参见下面的讲解:

下面我们就来看一下唯一定义的函数lookup: proc_lookup,到底实现了什么功能。

在进行具体分析之前,我们先考虑一个问题,我们知道,proc文件系统维护了自己的proc_dir_entry结构,因此提供了 create_proc_entry,remove_proc_entry等等函数,并且为了方便实现对proc文件的读写功能,特意在 proc_dir_entry结构中设置了get_info,read_proc和write_proc函数指针(我们在前面介绍过,这三个函数被封装在 proc_file_operations中),并且提供了自己的inode_operations和file_operations,分别是 proc_iops 和proc_fops。也就是说,我们在建立proc文件以及为proc文件建立操作函数的时候,似乎可以不用考虑VFS的实现,只要建立并注册该 proc_dir_entry结构,然后实现其proc_iops 和proc_fops(或者get_info,read_proc和write_proc)就可以了。

但是我们知道,在linux系统中,所有的子系统都是与VFS层交互,而VFS是通过inode结构进行管理的,并且在其上的操作(文件和索引节点的操 作)也是通过该inode结构的inode_operations和file_operations实现的。因此,proc文件系统必须将自己的文件与 VFS的inode链接起来。

那么proc文件系统是在何时,通过何种方法将自己的proc_dir_entry结构和VFS的inode联系在一起的,并且将对inode的 inode_operations和file_operations操作定位到自己结构中的proc_iops 和proc_fops上呢?通过我们对lookup: proc_lookup的分析,就会明白这一过程。我们先看一下它的代码:

struct dentry *proc_lookup(struct inode * dir, struct dentry *dentry){

struct inode *inode;

struct proc_dir_entry * de;

int error;

error = -ENOENT;

inode = NULL;

de = (struct proc_dir_entry *) dir->u.generic_ip;

if (de) {

for (de = de->subdir; de ; de = de->next) {

if (!de || !de->low_ino)

continue;

if (de->namelen != dentry->d_name.len)

continue;

if (!memcmp(dentry->d_name.name,

de->name, de->namelen)) {

int ino = de->low_ino;

error = -EINVAL;

inode = proc_get_inode(dir->i_sb, ino, de);

break;

}

}

}

if (inode){

dentry->d_op = &proc_dentry_operations;

d_add(dentry, inode);

return NULL;

}

return ERR_PTR(error);

}

这个函数的参数是struct inode * dir和struct dentry *dentry,它的功能是查找由dentry指定的文件,是否在由dir指定的目录中。

我们知道,proc文件系统通过proc_dir_entry结构维护文件信息,并且该结构与相应的inode->u.generic_ip联系,因此这个函数首先通过struct inode * dir得到了相应目录文件的proc_dir_entry结构,并使用指针de指向它,然后,开始在该结构的孩子中查找指定的dentry。

判断是否找到的条件很简单,就是de->namelen等于 dentry->d_name.len,并且dentry->d_name.name等于de->name,根据程序流程,如果没有找到,那么将返回-ENOENT错误(使用inode指针作为判断条件),如果找到该文件,那么就根据ino = de->low_ino(要注意的是,这时候的de已经指向由dentry确定的proc_dir_entry结构了。)调用函数:

inode = proc_get_inode(dir->i_sb,

ino, de);

这个proc_get_inode的功能很容易猜到,就是从由超级块i_sb确定的文件系统中,得到索引节点号为ino的inode。因此考虑两种情况,第一种情况,这个索引节点已经被读入缓存了,那么直接返回该inode即可。第二种情况是,指定ino的索引节点不在缓存中,那么就需要调用相应的函数,将该索引节点从逻辑文件系统中读入inode中。

下面我们就来分析一下proc_get_inode函数,尤其注意上面所说的第二种情况,因为这正是inode和proc_dir_entry建立联系并重定位操作函数集的时机。先看一下源码:

struct inode * proc_get_inode(struct super_block * sb, int ino,

struct proc_dir_entry * de) {

struct inode * inode;

/*

* Increment the use count so the dir entry can't disappear.

*/

de_get(de);

#if 1

/* shouldn't ever happen */

if (de && de->deleted)

printk("proc_iget: using deleted entry %s, count=%d/n", de->name, atomic_read(&de->count));

#endif

inode = iget(sb, ino);

if (!inode)

goto out_fail;

inode->u.generic_ip = (void *) de; /* link the proc_dir_entry to inode */

/*

* set up other fields in the inode

*/

if (de) {

if (de->mode) {

inode->i_mode = de->mode;

inode->i_uid = de->uid;

inode->i_gid = de->gid;

}

if (de->size)

inode->i_size = de->size;

if (de->nlink)

inode->i_nlink = de->nlink;

if (de->owner)

__MOD_INC_USE_COUNT(de->owner);

if (S_ISBLK(de->mode)||S_ISCHR(de->mode)||S_ISFIFO(de->mode))

init_special_inode(inode,de->mode,kdev_t_to_nr(de->rdev));

else {

if (de->proc_iops)

inode->i_op = de->proc_iops;

if (de->proc_fops)

inode->i_fop = de->proc_fops;

}

}

out:

return inode;

out_fail:

de_put(de);

goto out;

}

我们根据程序流程,分析它的功能:

1.使用de_get(de)增加proc_dir_entry结构de的引用计数。

2.使用VFS的iget(sb, ino)函数,从sb指定的文件系统中得到节点号为ino的索引节点,并使用指针inode指向它。如果没有得到,则直接跳到标号out_fail,减少de的引用计数后退出。

因此我们要了解一下iget,这个函数由VFS提供,可以参考源文件fs/inode.c和头文件include/linux/fs.h,在fs.h头文件中,有如下定义:

static inline struct inode *iget(struct super_block *sb, unsigned long ino) {

return iget4(sb, ino, NULL, NULL);

}

因此该函数是由fs/inode.c中的iget4实现的。主要步骤是,首先根据sb和ino得到要查找的索引节点的哈希链表,然后调用 find_inode函数在该链表中查找该索引节点。如果找到了,那么就增加该索引节点的引用计数,并将其返回;否则调用get_new_inode函 数,以便从逻辑文件系统中读出该索引节点。

get_new_inode函数也很简单,它分配一个inode结构,并试图重新查找指定的索引节点,如果还是没有找到,那么就给新分配的索引节点加入 到哈希链表和使用链表中,并设置一些基本信息,如i_ino,i_sb,i_dev等,并且,将其引用计数i_count初始化为1。然后调用超级块 sb的read_inode函数,来作逻辑文件系统自己特定的工作,但对于proc文件系统来说,read_inode函数基本没有实质性的功能,可参考 前文对该函数的分析。最后返回这个新建的索引节点。

3.这时,我们已经得到了指定的inode(或者是从缓存中返回,或者是利用get_new_inode函数刚刚创建),那么就使用语句

inode->u.generic_ip = (void *) de;

将proc_dir_entry结构de与相应的索引节点链接起来。因此我们就可以在其他时刻,利用proc文件索引节点的->u.generic_ip得到相应的proc_dir_entry结构了。

对于新创建的inode来说,将其->u.generic_ip域指向(void *) de没什么问题,因为该域还没有被赋值,但是如果这个inode是从缓存中得到的,那么,说明该域已经指向了一个proc_dir_entry结构,这样 直接赋值,会不会引起问题呢?

这有两种情况,第一种情况,它指向的proc_dir_entry结构没有发生过变化,那么,由于索引节点是由ino确定的,而且在一个文件系统中,确保 了索引节点号ino的唯一性,因此,使用inode->u.generic_ip = (void *) de语句对其重新进行赋值,不会发生任何问题。

另一种情况是在这之前,程序曾调用remove_proc_entry要将该proc_dir_entry结构删除,那么由于它的引用计数count不等于零,因此,该结构不会被释放,而只是打上了删除标记。所以这种情况下,该赋值语句也不会引起问题。

我们知道,当inode的i_count变为0的时候,会调用sb的proc_delete_inode函数,这个函数将inode的i_state设置 为I_CLEAR,这可以理解为将该inode删除了,并调用de_put,减少并检查proc_dir_entry的引用计数,如果到零,也将其释放。因此我们看到,引用计数的机制使得VFS的inode结构和proc的proc_dir_entry结构能够保持同步,也就是说,对于一个存在于缓存中的 的inode,必有一个proc_dir_entry结构存在。

4.这时,我们已经得到了inode结构,并且将相应的proc_dir_entry结构de与inode链接在了一起。因此,就可以根据de的信息,对inode的一些域进行填充了。其中最重要的是使用语句:

if (de->proc_iops)

inode->i_op = de->proc_iops;

if (de->proc_fops)

inode->i_fop = de->proc_fops;

将inode的操作函数集重定向到proc_dir_entry结构提供的函数集上。这是因为我们可以通过proc_dir_entry结构进行方便的设 置和调整,但最终要将文件提交至VFS进行管理。正是在这种思想下,proc文件系统提供提供了一套封装函数,使得我们可以只对 proc_dir_entry结构进行操作,而忽略与VFS的inode的联系。

5.最后,成功地返回所要的inode结构。

(七) 小结

至此,已经对proc文件系统进行了一个粗略的分析,从文件系统的注册,到proc_dir_entry结构的管理,以及与VFS的联系等等。下面我们对proc文件系统的整体结构作一个总结。

proc文件系统使用VFS接口,注册自己的文件类型,并且通过注册时提供的proc_read_super函数,创建自己的超级块,然后装载 vfsmount结构。在proc文件系统内部,则使用proc_dir_entry结构来维护自己的文件树,并且通过目录文件的lookup函数,将 proc_dir_entry结构与VFS的inode结构建立联系。

转载请保留出处:http://www.01yun.com/XML/20130425/376869.html