背景:
虚拟文件系统(有时也称作虚拟文件交换,更常见的是简称VFS)作为内核子系统,为用户空间程序提供了文件和文件系统相关的接口。系统中所有文件系统不但依赖VFS共存,而且也依靠VFS系统协同工作。通过虚拟文件系统,程序可以利用标准的Uinx系统调用对不同的文件系统,甚至不同介质上的文件系统进行读写操作。
实现:
VFS执行的动作:使用cp(1)命令从ext3文件系统格式的硬盘拷贝数据到ext2文件系统格式的可移动磁盘上。两种不同的文件系统,两种不同的介质,连接到同一个 VFS 上。
Unix的文件系统:
概述:
1.Unix使用了四种和文件系统相关的传统抽象概念:文件、目录项、索引节点和安装点( mount point)。
从本质上讲文件系统是特殊的数据分层存储结构,它包含文件、目录和相关的控制信息。文件系统的通用操作包含创建、删除和安装等。
2.在 Unix中,文件系统被安装在一个特定的安装点上,该安装点在全局层次结构中被称作命名空间,所有的已安装文件系统都作为根文件系统树的枝叶出现在系统中。
3.文件:文件其实可以做一个有序字节串,字节串中第一个字节是文件的头,最后一个字节是文件的尾。每一个文件为了便于系统和用户识别,都被分配了一个便于理解的名字。典型的文件操作有读、写、创建和删除等。简单的面向字节流抽象的Unix文件则以简单性和相当的灵活性为代价。
4.目录:文件通过目录组织起来。文件目录好比一个文件夹,用来容纳相关文件。因为目录也可以包含其他目录,即子目录,所以目录可以层层嵌套,形成文件路径。路径中的每一部分都被称作目录条目。“/home/wolfman/butter”是文件路径的一个例子——根目录/,目录home,wolfman和文件 butter都是目录条目,它们统称为目录项。在Unix中,目录属于普通文件,它列出包含在其中的所有文件。由于VFS把目录当作文件对待,所以可以对目录执行和文件相同的操作。
5.文件相关信息:Unix系统将文件的相关信息和文件本身这两个概念加以区分,例如访问控制权限、大小、拥有者、创建时间等信息。文件相关信息,有时被称作文件的元数据(也就是说,文件的相关数据),被存储在一个单独的数据结构中,该结构被称为索引节点( inode),它其实是index node的缩写,不过近来术语“inode”使用得更为普遍一些。
所有这些信息都和文件系统的控制信息密切相关,文件系统的控制信息存储在超级块中,超级块是一种包含文件系统信息的数据结构。有时,把这些收集起来的信息称为文件系统数据元,它集单独文件信息和文件系统的信息于一身。
VFS介绍:
VFS使得用户可以直接使用open()、read)和write()这样的系统调用而无须考虑具体文件系统和实际物理介质。现在听起来这并没什么新奇的(我们早就认为这是理所当然的),但是,使得这些通用的系统调用可以跨越各种文件系统和不同介质执行,绝非是微不足道的成绩。更了不起的是,系统调用可以在这些不同的文件系统和介质之间执行——我们可以使用标准的系统调用从一个文件系统拷贝或移动数据到另一个文件系统。
老式的操作系统(比如DOS〉是无力完成上述工作的,任何对非本地文件系统的访问都必须依靠特殊工具才能完成。正是由于现代操作系统引入抽象层,比如Linux,通过虚拟接口访问文件系统,才使得这种协作性和泛型存取成为可能。
文件系统抽象层:
VFS抽象层之所以能衔接各种各样的文件系统,是因为它定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。同时实际文件系统也将自身的诸如“如何打开文件”,“目录是什么”等概念在形式上与VFS的定义保持一致。因为实际文件系统的代码在统一的接口和数据结构下隐藏了具体的实现细节,所以在VFS层和内核的其他部分看来,所有文件系统都是相同的,它们都支持像文件和目录这样的概念,同时也支持像创建文件和删除文件这样的操作。
内核通过抽象层能够方便、简单地支持各种类型的文件系统。实际文件系统通过编程提供VFS所期望的抽象接口和数据结构,这样,内核就可以毫不费力地和任何文件系统协同工作,并这样提供给用户空间的接口,也可以和任何文件系统无绛地连接在一起,完成实际工作。
一个简单的用户空间操作:
ret=write(fd,buf,len)
该系统调用将buf指针指向的长度为len字节的数据写入文件描述符fd对应的文件的当前位置。
这个系统调用首先被一个通用系统调用sys_write()处理,sys_write()函数要找到fd所在的文件系统实际给出的是哪个写操作,然后再执行该操作。实际文件系统的写方法是文件系统实现的一部分,数据最终通过该操作写入介质(或执行这个文件系统想要完成的写动作)。
下图描述了从用户空间的write()调用到数据被写入磁盘介质的整个流程。
一方面,系统调用是通用VFS接口,提供给用户空间的前端﹔
另一方面,系统调用是具体文件系统的后端,处理实现细节。
VFS的对象
VFS其实采用的是面向对象的设计思路,使用一组数据结构来代表通用文件对象。这些数据结构类似于对象。因为内核纯粹使用C代码实现,没有直接利用面向对象的语言,所以内核中的数据结构都使用C语言的结构体实现,而这些结构体包含数据的同时也包含操作这些数据的函数指针,其中的操作函数由具体文件系统实现。
VFS中有四个主要的对象类型,它们分别是:
超级块对象:它代表一个具体的已安装文件系统。
索引节点对象:它代表一个具体文件。
目录项对象,它代表一个目录项,是路径的一个组成部分。
文件对象,它代表由进程打开的文件。
每个主要对象中都包含一个操作对象,这些操作对象描述了内核针对主要对象可以使用的方法:
super_operations对象,其中包括内核针对特定文件系统所能调用的方法,比如 write_inode()和sync_fs()等方法。
inode_operations对象,其中包括内核针对特定文件所能调用的方法,比如create()和link)等方法。
dentry_operations对象,其中包括内核针对特定目录所能调用的方法,比如d_compare()和d_delete(等方法。
file_operations对象,其中包括进程针对已打开文件所能调用的方法,比如read()和write(等方法。
超级块对象
各种文件系统都必须实现超级块对象,该对象用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块(所以称为超级块对象)。对于并非基于磁盘的文件系统(如基于内存的文件系统,比如sysfs),它们会在使用现场创建超级块并将其保存到内存中。
引用于:/usr/src/kernels/3.10.0-1160.el7.x86_64/include/linux/fs.h
struct super_block {
struct list_head s_list; /* Keep this first */
dev_t s_dev; /* search index; _not_ kdev_t */
unsigned char s_blocksize_bits;
unsigned long s_blocksize;
loff_t s_maxbytes; /* Max file size */
struct file_system_type *s_type;
const struct super_operations *s_op;
const struct dquot_operations *dq_op;
const 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;
int s_count;
atomic_t s_active;
#ifdef CONFIG_SECURITY
void *s_security;
#endif
const struct xattr_handler **s_xattr;
struct list_head s_inodes; /* all inodes */
struct hlist_bl_head s_anon; /* anonymous dentries for (nfs) exporting */
#ifdef __GENKSYMS__
#ifdef CONFIG_SMP
struct list_head __percpu *s_files;
#else
struct list_head s_files;
#endif
#else
#ifdef CONFIG_SMP
struct list_head __percpu *s_files_deprecated;
#else
struct list_head s_files_deprecated;
#endif
#endif
struct list_head s_mounts; /* list of mounts; _not_ for fs use */
/* s_dentry_lru, s_nr_dentry_unused protected by dcache.c lru locks */
struct list_head s_dentry_lru; /* unused dentry lru */
RH_KABI_REPLACE_UNSAFE(
int s_nr_dentry_unused,
long s_nr_dentry_unused) /* # of dentry on lru */
1401,2-9 41%
...
}
创建、管理和撤销超级块对象的代码位于文件 fs/super.c中。超级块对象通过alloc_super()函数创建并初始化。在文件系统安装时,文件系统会调用该函数以便从磁盘读取文件系统超级块,并且将其信息填充到内存中的超级块对象中。
超级块的操作函数
超级块对象中最重要的一个域是s_op,它指向超级块的操作函数表。超级块操作函数表由iper operations结构休表示,定义在文件<linux/fs.h>中,其形式如下;
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);
void (*dirty_inode) (struct inode *, int flags);
int (*write_inode) (struct inode *, struct writeback_control *wbc);
int (*drop_inode) (struct inode *);
void (*evict_inode) (struct inode *);
void (*put_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
int (*freeze_fs) (struct super_block *);
int (*unfreeze_fs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*umount_begin) (struct super_block *);
int (*show_options)(struct seq_file *, struct dentry *);
int (*show_devname)(struct seq_file *, struct dentry *);
int (*show_path)(struct seq_file *, struct dentry *);
int (*show_stats)(struct seq_file *, struct dentry *);
#ifdef CONFIG_QUOTA
ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
#endif
int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
int (*nr_cached_objects)(struct super_block *);
void (*free_cached_objects)(struct super_block *, int);
RH_KABI_EXTEND(struct list_head *(*inode_to_wblist)(struct inode *))
RH_KABI_EXTEND(struct inode *(*wblist_to_inode)(struct list_head *))
};
该结构体中的每一项都是一个指向超级块操作函数的指针,超级块操作函数执行文件系统和索引节点的低层操作。
当文件系统需要对其超级块执行操作的时候,首先要在超级块的对象中寻找需要的操作方法。
sb->s_op->write_super(sb);
/*
在这个调用中,sb是指向文件系统超级块的指针,沿着该指针进入超级块操作函数表s_op,并从表中取得希望得到的write_super()函数,该函数执行写入超级块的实际操作。注意,尽管write_super)方法来自超级块,但是在调用时,还是要把超级块作为参数传递给它,这是因为C缺少对面向对象的支持
*/
由于在C语言中无法直接得到操作函数的父对象,所以必须将父对象以参数形式传给操作函数。
下面给出super_operation中,超级块操作函数的用法
struct inode *alloc_inode (struct super_block *sb)
在给定的超级块下创建和初始化一个新的索引节点对象。
void destroy_inode (struct inode *inode)
释放给定的索引节点
void dirty_inode(struct inode *inode)
VFS在索引节点脏(被修改)时会调用此函数。日志文件系统(如ext3和ext4)执行该函数进行日志更新。
void write _inode (struct inode *inode ,int wait)
用于将给定的索引节点写入磁盘。wait参数指明写操作是否需要同步
void drop_inode(struct inode *inode)
在最后一个指向索引节点的引用被释放后,VFS会调用该函数。VFS只需要简单地删除这个索引节点后,普通Unix文件系统就不会定义这个函数了。
void de1ete_inode(struct inode *inode)
用于从磁盘上删除给定的索引节点。
void put_super(struct super_block *sb)
在卸载文件系统时由VFS 调用,用来释放超级块。调用者必须一直持有s_lock锁
oid write_super(struct super_block *sb)
用给定的超级块更新磁盘上的超级块。VFS通过该函数对内存中的超级块和磁盘中的超级块进行同步。调用者必须一直持有s_lock锁。
int sync_fs (struct super_block *sb, int wait)
使文件系统的数据元与磁盘上的文件系统同步。wait参数指定操作是否同步。
索引节点对象
索引节点对象包含了内核在操作文件或目录时需要的全部信息。
对于Unix风格的文件系统来说
这些信息可以从磁盘索引节点直接读入。
如果一个文件系统没有索引节点,那么,不管这些相关信息在磁盘上是怎么存放的,文件系统都必须从中提取这些信息。
没有索引节点的文件系统通常将文件的描述信息作为文件的一部分来存放。
这些文件系统与Unix风格的文件系统不同,没有将数据与控制信息分开存放。有些现代文件系统使用数据库来存储文件的数据。不管哪种情况、采用哪种方式,索引节点对象必须在内存中创建,以便于文件系统使用。
引自:/usr/src/kernels/3.10.0-1160.el7.x86_64/include/linux/fs.h
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
...
}
一个索引节点代表文件系统中(但是索引节点仅当文件被访问时,才在内存中创建〉的一个文件,它也可以是设备或管道这样的特殊文件。因此索引节点结构体中有一些和特殊文件相关的项,比如i_pipe项就指向一个代表有名管道的数据结构,i_bdev指向块设备结构体,i_cdev指向字符设备结构体。这三个指针被存放在一个公用体中,因为一个给定的索引节点每次只能表示三者之一(或三者均不)。
索引节点操作
struct inode_operations {
struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
void * (*follow_link) (struct dentry *, struct nameidata *);
int (*permission) (struct inode *, int);
struct posix_acl * (*get_acl)(struct inode *, int);
int (*readlink) (struct dentry *, char __user *,int);
void (*put_link) (struct dentry *, struct nameidata *, void *);
int (*create) (struct inode *,struct dentry *, umode_t, bool);
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 *,umode_t);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
...
}
下面这些接口由各种函数组成,在给定的节点上,可能由VFS执行这些函数,也可能由具体的文件系统执行:
int create(struct inode *dir,struct dentry *dentry,int mode)
VFS通过系统调用create()和open()来调用该函数,从而为dentry对象创建一个新的索引节点。在创建时使用mode指定的初始模式。
struct dentry * lookup(struct inode *dir,struct dentry *dentry)
该函数在特定目录中寻找索引节点,该索引节点要对应于denrty中给出的文件名。
int link(struct dentry *old_dentry,struct inode *dir,struct dentry *dentry)
该函数被系统调用link()调用,用来创建硬连接。硬连接名称由dentry参数指定,连接对象是dir目录中old_dentry目录项所代表的文件。
int unlink (struct inode *dir,struct dentry *dentry)
该函数被系统调用unlink()调用,从目录dir中删除由目录项dentry 指定的索引节点对象。
int symlink (struct inode *dir,struct dentry *dentry.const char symname)
该函数被系统调用symlik()调用,创建符号连接。该符号连接名称由symname指定,连接对象是dir目录中的dentry目录项。
int mkdir(struct inode *dir,struct dentry *dentry.int mode)
该函数被系统调用mkdir()调用,创建一个新目录。创建时使用mode指定的初始模式
nt rmdir(struct inode *dir.struct dentry dentry)
调用rmdir()调用,删除dir目录中的dentry目录项代表的文件
...