二 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文件系统来说,这些统计数据是没有意义的。
(五) 对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 < 0)
return -EAGAIN;
dp->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 < count)
eof = 1;
} else if (dp->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结构建立联系。