为了技术,我不会停下学习的脚步,我相信我还能走二十年。
分类:
2012-07-01 21:10:42
原文地址:对 proc 文件系统的分析(一) 作者:embededgood
一 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 是一个管理者,而对于内核的其他部分,则是一个接口,整个linux 中文件系统的逻辑关系,如图
VFS提供了一个统一的接口(即几个有关操作的数据结构),一个逻辑文件系统要想被 Linux 支持,那么就必须按照这个接口来编写自己的操作函数,从而将自己的细节对其他子系统隐藏起来。因而,对于内核其他子系统来说,所有的文件系统都是一样的。
(二) 基本对象与方法
虚拟文件系统的接口由一组对象及其由这些对象调用的一组方法所构成的。这些基本的对象是 files(文件),file-systems(文件系统),inodes (索引节点)以及 names for inodes(索引节点名字),下面对这些对象进行简单的介绍:
1 Files:
文件是一个可读可写的对象,它也可以映射到内存中,这和 UNIX 中文件描述符的概念很接近。文件在 Linux 中使用一个"struct file"结构来实现,并且该结构有一组操作函数,保存在结构"struct file_operations"中。
图 2.1.1
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_clash; /* 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被最终释放之前,会调用该函数。
d_iput:如果定义了该函数,它就被用来替换iput,来dentry被丢弃时,释放inode。它被用来做iput的工作再加上其他任何想要做的事情。
(五) 总结
在上面的部分中,我们对VFS进行了一个大概的分析。了解了文件系统的注册,安装,以及卸载的过程,并对VFS对逻辑文件系统的管理,尤其是接口部分的数据结构,进行了详细的分析。
proc文件系统作为一个特殊的逻辑文件系统,其实现也遵循VFS接口,因此,根据上面对VFS的分析,我们可以基本确定对proc文件系统进行分析的步骤。