来源: ChinaUnix博客  日期: 2008.01.03 11:46 (共有条评论) 我要评论
 
二 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中,定义这个宏为 ((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结构建立联系。 

proc文件系统分析的更多相关文章

  1. 《Linux/Unix系统编程手册》读书笔记7 (/proc文件的简介和运用)

    <Linux/Unix系统编程手册>读书笔记 目录 第11章 这章主要讲了关于Linux和UNIX的系统资源的限制. 关于限制都存在一个最小值,这些最小值为<limits.h> ...

  2. linux伪文件与proc文件

    linux/unix系统的文件类型大致可分为三类:普通文件.目录文件和伪文件.常见的伪文件分别是特殊文件.命名管道及proc文件. 伪文件不是用来存储数据的,因此这些文件不占用磁盘空间,尽管这些文件确 ...

  3. ubuntu/linux mint 创建proc文件的三种方法(四)

    在做内核驱动开发的时候,能够使用/proc下的文件,获取对应的信息,以便调试. 大多数/proc下的文件是仅仅读的,但为了演示样例的完整性,都提供了写方法. 方法一:使用create_proc_ent ...

  4. ubuntu/linux mint 创建proc文件的三种方法(两)

    在这样做的内核驱动程序的开发时间.可以使用/proc下档.获取相应的信息.对于调试. 大多数/proc下的文件是仅仅读的.但为了演示样例的完整性.都提供了写方法. 方法一:使用create_proc_ ...

  5. Linux 小知识翻译 - 「/proc 文件夹」

    这次聊聊 「/proc 文件夹」. /proc 文件夹用来保管系统状态相关的文件的特殊文件夹,这个文件夹中有的文件只是内存上的虚拟文件. /proc 文件夹下有些文件可以反映各个进程的运行状态.所以说 ...

  6. Linux内核学习笔记之seq_file接口创建可读写proc文件

    转自:http://blog.csdn.net/mumufan05/article/details/45803219 学习笔记与个人理解,如有错误,欢迎指正. 温馨提示:建议跟着注释中的编号顺序阅读代 ...

  7. linux常用命令大全2--挂载/dpkg/文件系统分析/apt/光盘/关机

    挂载一个文件系统 mount /dev/hda2 /mnt/hda2 挂载一个叫做hda2的盘 - 确定目录 '/ mnt/hda2' 已经存在 umount /dev/hda2 卸载一个叫做hda2 ...

  8. /proc文件系统的特点和/proc文件的说明

    /proc文件系统是一种特殊的.由软件创建的文件系统,内核使用它向外界导出信息,/proc系统只存在内存当中,而不占用外存空间. /proc下面的每个文件都绑定于一个内核函数,用户读取文件时,该函数动 ...

  9. 创建你的 /proc 文件

    一旦你有一个定义好的 read_proc 函数, 你应当连接它到 /proc 层次中的一个入口项. 使用一个 creat_proc_read_entry 调用: struct proc_dir_ent ...

随机推荐

  1. StringFormat

    public class StringFormatDemo { public static void main(String[] args) { String str = null; str = St ...

  2. <摘录>CentOS6.5下添加epel源

    0.安装yum优先级插件 yum install yum-priorities 1.epel简介: https://fedoraproject.org/wiki/EPEL/zh-cn rpm -Uvh ...

  3. Java性能优化的9大工具

    在这篇文章中,我会带着大家一起看一下9个可以帮助我们优化Java性能的工具.有一些我们已经在IDR Solutions中使用了,而另外一些有可能在个人项目中使用. NetBeans Profiler ...

  4. CMOS DACs act as digitally controlled voltage dividers

    Digital potentiometers, such as Analog Devices’ AD5160, make excellent digitally controlled voltage ...

  5. mysql 5.7源码安装

    http://blog.itpub.net/29733787/viewspace-1590891/

  6. 在Windows上以服务方式运行 MSOPenTech/Redis

    ServiceStack.Redis 使用教程里 提到Redis最好还是部署到Linux下去,Windows只是用来做开发环境,现在这个命题发生改变了,在Windows上也可以部署生产环境的 Redi ...

  7. Android系统信息获取

    在Android中可以通过android.os.Build这个类和System.getProperty(“xxx”);来获取设备信息,下面列举的常见设备信息摘自Android群英传 Build.BOA ...

  8. SQL Server快速部署作业到多台服务器

    问题: 需要在很多的SQL Server服务器上创建相同的作业.我们可以一台一台的运行相同的脚本创建作业,但是有没有什么简便的做法呢? 解决方法: 可能很多人都没有注意到可以用多服务器环境管理SQL ...

  9. Code a simple telnet client using sockets in python

    测试端口是否开放的python脚本 原文: https://www.binarytides.com/code-telnet-client-sockets-python/ 配置: 120.79.14.8 ...

  10. 一篇文章让你读懂Pivotal的GemFire家族产品

    一篇文章让你读懂Pivotal的GemFire家族产品 学习了:https://www.sohu.com/a/217157517_747818