proc文件系统分析(四)

【转】proc文件系统分析(四)

(六) 对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结构建立联系。 


本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u2/74524/showart_1129842.html

你可能感兴趣的:(proc文件系统分析(四))