今天要拿VFS(Virtual File System)开涮,我们知道LINUX的成功是离不开虚拟文件系统VFS的,因为正是VFS提供的文件访问抽象层,使LINUX得以支持各式各样的文件系统。那究竟VFS是何方神圣?它是如何工作的?它在整个LINUX操作系统中扮演老几?本文试图通过简单易懂图文并茂的方式带领读者初窥门径,脑袋中对VFS有一个比较清晰的概览。
“软件设计中,任何问题都可以通过增加一个中间层来解决。”,忘了这句话是谁说的了,但始终觉得非常经典,的确如此,我们在软件开发中几乎处处都会碰到分层的思想,实际上就LINUX系统而言整个设计就是层次化的,从最低层的硬件到驱动程序,到系统调用,到函数库再到APP,网络开发中的数据包封装分层思想更是展现无余,LINUX系统在处理各种纷繁的文件系统时,也采用了这么一种思想,把文件系统中核心的概念抽象出来(例如打开、关闭、读、写、定位等),把跟具体FS实现相关的操作屏蔽掉,抽象出一个中间层(即VFS),往上提供统一接口,往下接管各种类型文件系统及其相应操作函数,这样,应用层开发人员就只关心这个统一接口就行了,省去了关注具体文件系统的实现细节,这对于应用层来说不仅是有益的,也是必须的,想象一下,如果我们连简单的cp拷贝程序都要考虑底层FS(FILE SYSTEM),软件开发这份工作就太让人沮丧了吧。
一个可行的比喻:VFS就像超市结账流水中的POS机,POS终端支持银联所支持的若干家银行,付账的你只要知道你的贱行卡或者招行卡有充足的余额支付而不至于在收银员美女面前遭受尴尬即可,除非你跟我一样曾经是一名银行交易系统开发者(而且负责POS终端处理这块),否则你不会关心这部机器究竟是怎么将不同的银行卡跟不同的银行网点服务器交互的,你潇洒地把卡递给她,她报之以一个迷人的微笑,你的钱就从卡里哗啦啦地流走啦!交易过程对于你们来说是透明的,所有的细节都被隐藏在那充满哲学意味的“咔嚓”声中。
VFS和POS终端机的作用,请看下面的示意图吧,为了画这两个图,我花了1.5个小时下载visio :(
这里有必要提一下,LINUX系统支持的FS种类非常多,市场上能看到的文件系统在LINUX中基本上都能得到支持,它们大致来说主要可以分成三大类:
1 磁盘文件系统。顾名思义,这类文件系统是用来管理本地磁盘分区中的存储空间的,例如windows的NTFS、FAT16、FAT32,LINUX默认的EXT2、EXT3、EXT4等文件系统系列和REISER,CD-ROM文件系统,苹果公司Macintosh的HFS,嵌入式开发中常用的cramfs、jiffs2等等。
2 网络文件系统。通过这些文件系统我们可以像访问本地FS一样地访问远程文件,例如有NFS(交叉开发中经常用到)、CIFS等。
3 特殊文件系统。这类FS有/proc文件系统、tmpfs等,之所以称它们为特殊的文件系统,是因为它们并不管理本地或者远程的磁盘空间,它们仅仅是在系统启动后为了某些特殊的目的而在内存中临时创建的FS。
虽然FS种类繁多,而且它们之间的差异有时非常大,但LINUX的VFS都能很好地支持它们,VFS的实现虽然是用C语言写的,但却是基于面向对象思想的,换句话说,VFS用所谓的通用文件模型(Common File Model),来为系统中每个文件系统及其文件构建一组对象,正是这组对象为文件操作提供了统一接口抽象层,这组对象包括:
1 超级块对象 (superblock object)
2 索引节点对象 (inode object)
3 文件对象 (file object)
4 目录项对象 (dentry object)
这个模型严格反映UNIX文件系统提供的文件模型,这样LINUX就能以更少的开销和更高的效率运行本地FS了,对于不符合这种模型的非UNIX血统FS,LINUX的VFS必须在必要时能够快速建立对应于具体文件系统文件的每种对象模型,然后通过操作相应的对象模型来控制文件。
下面我们来看看这些对象模型。
首先是超级块对象,VFS中每种对象模型都用一个适当的结构体来表示,在LINUX内核源代码的include/linux/fs.h中定义了一个叫super_block的结构体,其字段如下:
----------------------------------------------------------------------------------
struct super_block {
struct list_head s_list; /* Keep this first */
dev_t s_dev; /* search index; _not_ kdev_t */
unsigned long s_blocksize;
unsigned char s_blocksize_bits;
unsigned char s_dirt;
unsigned long long s_maxbytes; /* Max file size */
struct file_system_type *s_type;
const struct super_operations *s_op;
struct dquot_operations *dq_op;
struct quotactl_ops *s_qcop;
const struct export_operations *s_export_op;
unsigned long s_flags;
unsigned long s_magic;
struct dentry *s_root;
struct rw_semaphore s_umount;
struct mutex s_lock;
int s_count;
int s_need_sync;
atomic_t s_active;
#ifdef CONFIG_SECURITY
void *s_security;
#endif
struct xattr_handler **s_xattr;
struct list_head s_inodes; /* all inodes */
struct list_head s_dirty; /* dirty inodes */
struct list_head s_io; /* parked for writeback */
struct list_head s_more_io; /* parked for more writeback */
struct hlist_head s_anon; /* anonymous dentries for (nfs) exporting */
struct list_head s_files;
/* s_dentry_lru and s_nr_dentry_unused are protected by dcache_lock */
struct list_head s_dentry_lru; /* unused dentry lru */
int s_nr_dentry_unused; /* # of dentry on lru */
struct block_device *s_bdev;
struct mtd_info *s_mtd;
struct list_head s_instances;
struct quota_info s_dquot; /* Diskquota specific options */
int s_frozen;
wait_queue_head_t s_wait_unfrozen;
char s_id[32]; /* Informational name */
void *s_fs_info; /* Filesystem private info */
fmode_t s_mode;
/*
* The next field is for VFS *only*. No filesystems have any business
* even looking at it. You had been warned.
*/
struct mutex s_vfs_rename_mutex; /* Kludge */
/* Granularity of c/m/atime in ns.
Cannot be worse than a second */
u32 s_time_gran;
/*
* Filesystem subtype. If non-empty the filesystem type field
* in /proc/mounts will be "type.subtype"
*/
char *s_subtype;
/*
* Saved mount options for lazy filesystems using
* generic_show_options()
*/
char *s_options;
};
----------------------------------------------------------------------------------
对于上面这个结构体,s_list指向超级块链表,其类型list_head是一种结构体指针,这种结构体的成员如下:
----------------------------------------------------------------------------------
struct list_head
{
struct list_head *next;
struct list_head *prev;
};
----------------------------------------------------------------------------------
事实上list_head结构体是LINUX内核中双向循环链表的标准实现形式,在LINUX源码中我们就是用这种包含两个指向自身类型指针的结构体实现双向循环链表的,在需要链进链表的“节点”中镶嵌这种结构体,即可把它们连接起来,以超级块对象为例,系统中所有的超级块对象都会被链进这么一个双向循环链表中:
当然,对于这种双向循环链表,我们有专门的宏和函数对其进行相关操作,比如创建、插入、删除、清空等等(具体定义请参考include/linux/list.h)。
s_dev是设备标识符,包含了主设备号和次设备号,主设备号标识设备的种类,次设备号标识该类设备在该系统中的序号。
s_dirt标识超级块对象是否为“脏”(即dirty)。无论是这里说的“脏超级块”还是以后在内存管理中遇到的所谓“脏页”,“脏”代表着数据的不同步,而使数据同步通常会说另一个相对应的术语“冲洗”(即flush),那为什么数据会发生不同步的现象呢?那是因为效率问题:超级块对象中还有一个字段叫s_fs_info,该void型指针指向具体文件系统的超级块信息,对于基于磁盘的FS而言,为了效率起见我们通常会将该指针所指向的内容复制到内存中,将来需要访问或者更改自己的磁盘分配位图的时候VFS可以直接在内存当中操作而不用时时访问慢速磁盘,以便提高效率。每次修改完VFS中超级块对象数据且尚未访问磁盘的中间这段时间间隔,超级块就是“脏”的,为此我们需要为超级块的这种状态设置一个状态标志位,即s_dirt。
与超级块关联的方法就是所谓的超级块操作,这些操作(即函数)包含在一个叫做super_operations的结构体当中,超级块对象用指向该结构体的指针s_op来调用其中的函数。这些操作包括为索引节点分配空间、撤销索引节点等。
还有像s_count是超级块引用计数,s_inodes,s_dirty,s_io等分别是所有索引节点、“脏”索引节点和等待被写回磁盘索引节点双向链表,s_files是文件对象链表,可以顺着这个双向循环链表找到该FS上被打开的所有文件。
关于索引节点对象,LINUX内核同样用一个结构体来描述,即inode结构体,其具体组成如下:
----------------------------------------------------------------------------------
struct inode {
struct hlist_node i_hash;
struct list_head i_list;
struct list_head i_sb_list;
struct list_head i_dentry;
unsigned long i_ino;
atomic_t i_count;
unsigned int i_nlink;
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev;
u64 i_version;
loff_t i_size;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
blkcnt_t i_blocks;
unsigned int i_blkbits;
unsigned short i_bytes;
umode_t i_mode;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
struct mutex i_mutex;
struct rw_semaphore i_alloc_sem;
const struct inode_operations *i_op;
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct super_block *i_sb;
struct file_lock *i_flock;
struct address_space *i_mapping;
struct address_space i_data;
#ifdef CONFIG_QUOTA
struct dquot *i_dquot[MAXQUOTAS];
#endif
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
----------------------------------------------------------------------------------
LINUX延续UNIX的传统,把文件的内容和文件的属性分开管理,文件的所有属性信息都被保存在上面这个叫做inode的结构体当中,其中i_ino就是索引节点号,i_count就是引用计数,i_mode是文件类型和相关访问权限(该字段对应于标准IO中stat结构体的st_mode字段),i_nlink是硬链接数目(当该数目小于等于0时该节点被释放),同样地,对索引节点的操作被放在一个叫做inode_operations的结构体当中,inode则包含指向这个结构体的指针i_op,这些操作包括创建索引节点、创建和解除硬链接、更改文件名、权限探测等。而i_fop是缺省文件操作,我们在加载实际FS的索引节点时,会把实际FS指向具体文件操作的指针存放在file_operations结构中,i_fop就是指向这个结构体的指针,将来用户打开某一个文件时,内核就会为其创建一个临时的文件对象(下面会讲到),而且会用这个i_fop来初始化文件对象中的f_op字段。
当然,内核也是用标准双向循环链表弄成圈圈把它们拴在一起的,对于索引节点而言,内核会为他们保留三个圈圈,根据状态的不同它们会在其中的一个之中:1有效但是未使用,2正在使用且不为脏,3脏索引节点。当然,它们也都在一个由s_inodes所指向的更大圈圈里,除此之外它们也都还在一个由i_hash所指向的哈希链表中。如下这碗面条所示:
就像图中画的那样,索引节点会根据状态的不同链进红蓝绿三个虚线圈圈里,深蓝实线圈圈是所有的索引节点对象,从图中可以看到这个圈圈的头结点放在了超级块对象的s_inodes字段中(实际上这个双向循环链表没有什么头节点,每个节点都可以成为头结点,这也是为什么将其标准实现称之为list_head的原因),相似的,超级块对象也被组织成一个圈圈。
再来看看文件对象:
--------------------------------------------------------------------------------- -
struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op;
spinlock_t f_lock; /* f_ep_links, f_flags, no IRQ */
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};
----------------------------------------------------------------------------------
要注意的是,文件对象是在文件被打开的时候才创建的,文件对象在磁盘上并没有对应的映像,因此我们看不到标识“脏”的字段,因为不需要同步。
在这个结构中f_op就是刚刚提到过的文件操作指针,它在该文件对象被创建时由索引节点的i_fop字段初始化,当然如果你不喜欢,你可以修改这个f_op的值来使它指向别的文件操作函数集。打开include/linux/fs.h,让我们来一睹文件操作集的芳容:
----------------------------------------------------------------------------------
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, 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);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
----------------------------------------------------------------------------------
我们看到,file_operarions的成员除了owner外全部都是函数指针,这些函数指针指向真正FS的操作函数,不同的FS会用其不同的操作函数集的地址来初始化索引节点中的i_fop字段,索引节点的这个i_fop将来就会用来初始化文件对象中的f_op字段,所以进程打开不同FS中的文件都可以正确地调用其相应的操作函数集。
这些操作中包含我们熟悉的一些操作,比如open用来创建一个新的文件对象并把它链接到相应的索引节点对象中,read、write就是从文件的offset处读取和写入count个字节,llseek用来更新我们的文件位置变量等等,我们注意到这个操作集中并没有close,因为VFS是我们APP和具体FS之间的中间层,有时候我们并不需要调用底层函数,VFS自己就可以完成,比如关闭一个打开的文件,这并不涉及磁盘上的相应文件,因此操作集中没有close,VFS只需释放该文件对象即可。
这些方法(就是这些操作函数,套用面向对象的惯例我们称函数为方法)对所有可能的文件类型都是可用的,换句话说,对于一个具体的文件而言,可能会用到其中的一个子集,未实现的方法将被设置为NULL。
回过来在说说文件对象中的其他字段,f_pos是文件当前位置变量,pos就是position的意思,文件的下一个操作就是发生在这个位置,f_pos之所以在文件对象中而不是在索引节点对象中,是因为一个文件可以由多个进程打开。
文件对象的引用计数用f_count字段表示,这个字段表示了当前有多少个进程正在使用该对象(普通进程对同一个文件拥有各自独立文件对象,但是线程可以共享文件对象)。
未完待续……