原文地址:
作者:
(journeyman)不知正确与否。
这样是表示对作者的尊重。
如果有不对,请通知我,我将即时更改. | |
二 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结构,并将其加入到已装载文件系统的链表中(可参考图-xx)。
最后,返回该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;
}
该函数进行了如下几步操作:
1.在该函数里,首先向作为参数传入的超级块写入文件系统的基本信息,s_blocksize设置为1024,由于1024=2^10,因此,s_blocksize_bit设置为10,然后是proc文件系统的魔数,为PROC_SUPER_MAGIC。超级块的函数集设置为proc_sops,对于proc文件系统来讲,只实现了4个超级块函数,我们将在后面进行分析。然后,设置proc文件系统中的文件最大字节数为MAX_NON_LFS,在fs.h中,定义这个宏为 ((1UL<<31)-1) 。
2.使用proc_get_inode(s, PROC_ROOT_INO, &proc_root) 函数建立根结点root_inode。这个函数根据参数ino,来得到inode,我们后面将进一步分析该函数。如果没有得到根结点,则跳到out_no_root标号,并退出。参数ino用来标志inode,在建立proc的新索引节点的时候,会动态地分配它一个ino。
3.修正root_inode的链接数。它将遍历进程链表,对于每一个进程,都使i_nlink++,这是因为在/proc目录中,每一个进程都有一个目录,换句话说,存在一个进程,就必然在proc目录中对应一个子目录,因此在建立proc的根节点时,要根据进程数修改它的i_nlink。
4.根据刚刚建立的root_inode,为超级块的s_root建立root dentry:
s->s_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)<<16)|(ino))
定义,因此,检查条件为if (PROC_INODE_PROPER(inode)) 。我们知道,/proc目录中有许多于进程相关的目录和文件,这些proc文件是另一种组织形式,它们的文件和进程密切相关,不是像其他proc文件那样使用create_proc_entry函数注册的,而是根据实际的进程链表动态生成的,因此,可能在以后的linux版本中,会单独为pid proc文件建立一个超级块,但目前的情况下,使用inode的ino来区分。由于pid目录中的inode和进程数据结构密切相关,因此,当要释放inode的时候,要做一些特殊的工作,释放一些特殊的资源。这部分工作由proc_pid_delete_inode()函数完成。
最后,调用de_put() 函数,对与此inode相关联的proc_dir_entry结构进行必要的操作。即减少proc_dir_entry的引用计数,如果计数到达零,则释放该proc_dir_entry结构。
4 statfs: proc_statfs
我们在分析VFS的时候知道,statfs函数是为了实现系统调用statfs(2)。我们看一下它的源代码:
static int proc_statfs(struct super_block *sb, struct statfs *buf)
{
buf->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文件系统来说,这些统计数据是没有意义的。