【简介】在文件系统方面,Linux可以算得上操作系统中的“瑞士军刀”。Linux 支持许多种文件系统,从日志型文件系统到集群文件系统和加密文件系统。Linux允许众多不同的文件系统共存,并支持跨文件系统的文件操作,这是因为有虚拟文件系统(VFS,有时候称为虚拟文件系统交换器)存在。VFS是Linux内核中的一个软件抽象层。它通过一些数据结构及其方法向实际的文件系统如 ext2,vfat 提供接口机制。本文简要讨论 Linux 内核中的虚拟文件系统。
首先回答最常见的问题,“什么是文件系统”。文件系统是对一个存储设备上的数据和元数据进行组织的机制。由于定义如此宽泛,支持它的代码会很有意思。正如前面提到的,有许多种文件系统和媒体。由于存在这么多类型,可以预料到 Linux 文件系统接口实现为分层的体系结构,从而将用户接口层、文件系统实现和操作存储设备的驱动程序分隔开。另一种看待文件系统的方式是把它看作一个协议。网络协议(比如 IP)规定了互联网上传输的数据流的意义,同样,文件系统会给出特定存储媒体上数据的意义。
Linux 文件系统体系结构是一个对复杂系统进行抽象化的有趣例子。通过使用一组通用的 API 函数,Linux 可以在许多种存储设备上支持许多种文件系统。如图 1 所示,我们可以使用 cp 命令从 vfat 文件系统格式的硬盘拷贝数据到 ext3 文件系统格式的硬盘;而这样的操作涉及到两个不同的文件系统。
“一切皆是文件”是 Unix/Linux 的基本哲学之一。不仅普通的文件,目录、字符设备、块设备、套接字等在 Unix/Linux 中都是以文件被对待;它们虽然类型不同,但是对其提供的却是同一套操作界面。
虚拟文件系统(Virtual File System, 简称 VFS),是 Linux 内核中的一个软件层,用于给用户空间的程序提供文件系统接口;同时,它也提供了内核中的一个抽象功能,允许不同的文件系统共存。系统中所有的文件系统不但依赖 VFS 共存,而且也依靠 VFS 协同工作。
为了能够支持各种实际文件系统,VFS 定义了所有文件系统都支持的基本的、概念上的接口和数据结构;同时实际文件系统也提供 VFS 所期望的抽象接口和数据结构,将自身的诸如文件、目录等概念在形式上与VFS的定义保持一致。换句话说,一个实际的文件系统想要被 Linux 支持,就必须提供一个符合VFS标准的接口,才能与 VFS 协同工作。实际文件系统在统一的接口和数据结构下隐藏了具体的实现细节,所以在VFS 层和内核的其他部分看来,所有文件系统都是相同的。图3显示了VFS在内核中与实际的文件系统的协同关系。
我们已经知道,正是由于在内核中引入了VFS,跨文件系统的文件操作才能实现,“一切皆是文件” 的口号才能承诺。而为什么引入了VFS,就能实现这两个特性呢?在接下来,我们将以这样的一个思路来切入文章的正题:我们将先简要介绍下用以描述VFS模型的一些数据结构,总结出这些数据结构相互间的关系;
从本质上讲,文件系统是特殊的数据分层存储结构,它包含文件、目录和相关的控制信息。为了描述这个结构,Linux引入了一些基本概念:
文件 一组在逻辑上具有完整意义的信息项的系列。在Linux中,除了普通文件,其他诸如目录、设备、套接字等也以文件被对待。总之,“一切皆文件”。
目录 目录好比一个文件夹,用来容纳相关文件。因为目录可以包含子目录,所以目录是可以层层嵌套,形成文件路径。在Linux中,目录也是以一种特殊文件被对待的,所以用于文件的操作同样也可以用在目录上。
目录项 在一个文件路径中,路径中的每一部分都被称为目录项;如路径/home/source/helloworld.c中,目录 /, home, source和文件 helloworld.c都是一个目录项。
索引节点 用于存储文件的元数据的一个数据结构。文件的元数据,也就是文件的相关信息,和文件本身是两个不同的概念。它包含的是诸如文件的大小、拥有者、创建时间、磁盘位置等和文件相关的信息。
超级块 用于存储文件系统的控制信息的数据结构。描述文件系统的状态、文件系统类型、大小、区块数、索引节点数等,存放于磁盘的特定扇区中。
如上的几个概念在磁盘中的位置关系如图3所示。
关于文件系统的三个易混淆的概念:
创建 以某种方式格式化磁盘的过程就是在其之上建立一个文件系统的过程。创建文现系统时,会在磁盘的特定位置写入关于该文件系统的控制信息。
注册 向内核报到,声明自己能被内核支持。一般在编译内核的时侯注册;也可以加载模块的方式手动注册。注册过程实际上是将表示各实际文件系统的数据结构struct file_system_type 实例化。
安装 也就是我们熟悉的mount操作,将文件系统加入到Linux的根文件系统的目录树结构上;这样文件系统才能被访问。
VFS依靠四个主要的数据结构和一些辅助的数据结构来描述其结构信息,这些数据结构表现得就像是对象;每个主要对象中都包含由操作函数表构成的操作对象,这些操作对象描述了内核针对这几个主要的对象可以进行的操作。
存储一个已安装的文件系统的控制信息,代表一个已安装的文件系统;每次一个实际的文件系统被安装时,内核会从磁盘的特定位置读取一些控制信息来填充内存中的超级块对象。一个安装实例和一个超级块对象一一对应。超级块通过其结构中的一个域s_type记录它所属的文件系统类型。
根据第三部分追踪源代码的需要,以下是对该超级块结构的部分相关成员域的描述,(如下同):
struct super_block { //超级块数据结构 struct list_head s_list; /*指向超级块链表的指针*/ …… struct file_system_type *s_type; /*文件系统类型*/ struct super_operations *s_op; /*超级块方法*/ …… struct list_head s_instances; /*该类型文件系统*/ …… }; struct super_operations { //超级块方法 …… //该函数在给定的超级块下创建并初始化一个新的索引节点对象 struct inode *(*alloc_inode)(struct super_block *sb); …… //该函数从磁盘上读取索引节点,并动态填充内存中对应的索引节点对象的剩余部分 void (*read_inode) (struct inode *); …… }; |
索引节点对象存储了文件的相关信息,代表了存储设备上的一个实际的物理文件。当一个文件首次被访问时,内核会在内存中组装相应的索引节点对象,以便向内核提供对一个文件进行操作时所必需的全部信息;这些信息一部分存储在磁盘特定位置,另外一部分是在加载时动态填充的。
struct inode {//索引节点结构 …… struct inode_operations *i_op; /*索引节点操作表*/ struct file_operations *i_fop; /*该索引节点对应文件的文件操作集*/ struct super_block *i_sb; /*相关的超级块*/ …… }; struct inode_operations { //索引节点方法 …… //该函数为dentry对象所对应的文件创建一个新的索引节点,主要是由open()系统调用来调用 int (*create) (struct inode *,struct dentry *,int, struct nameidata *); //在特定目录中寻找dentry对象所对应的索引节点 struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *); …… }; |
引入目录项的概念主要是出于方便查找文件的目的。一个路径的各个组成部分,不管是目录还是普通的文件,都是一个目录项对象。如,在路径/home/source/test.c中,目录 /, home, source和文件 test.c都对应一个目录项对象。不同于前面的两个对象,目录项对象没有对应的磁盘数据结构,VFS在遍历路径名的过程中现场将它们逐个地解析成目录项对象。
struct dentry {//目录项结构 …… struct inode *d_inode; /*相关的索引节点*/ struct dentry *d_parent; /*父目录的目录项对象*/ struct qstr d_name; /*目录项的名字*/ …… struct list_head d_subdirs; /*子目录*/ …… struct dentry_operations *d_op; /*目录项操作表*/ struct super_block *d_sb; /*文件超级块*/ …… }; struct dentry_operations { //判断目录项是否有效; int (*d_revalidate)(struct dentry *, struct nameidata *); //为目录项生成散列值; int (*d_hash) (struct dentry *, struct qstr *); …… }; |
文件对象是已打开的文件在内存中的表示,主要用于建立进程和磁盘上的文件的对应关系。它由sys_open() 现场创建,由sys_close()销毁。文件对象和物理文件的关系有点像进程和程序的关系一样。当我们站在用户空间来看待VFS,我们像是只需与文件对象打交道,而无须关心超级块,索引节点或目录项。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已经打开的文件,它反过来指向目录项对象(反过来指向索引节点)。一个文件对应的文件对象可能不是惟一的,但是其对应的索引节点和目录项对象无疑是惟一的。
清单4. 文件对象
struct file {
……
struct list_head f_list; /*文件对象链表*/
struct dentry *f_dentry; /*相关目录项对象*/
struct vfsmount *f_vfsmnt; /*相关的安装文件系统*/
struct file_operations *f_op; /*文件操作表*/
……
};
struct file_operations {
……
//文件读操作
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
……
//文件写操作
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
……
int (*readdir) (struct file *, void *, filldir_t);
……
//文件打开操作
int (*open) (struct inode *, struct file *);
……
};
根据文件系统所在的物理介质和数据在物理介质上的组织方式来区分不同的文件系统类型的。 file_system_type结构用于描述具体的文件系统的类型信息。被Linux支持的文件系统,都有且仅有一个file_system_type结构而不管它有零个或多个实例被安装到系统中。
而与此对应的是每当一个文件系统被实际安装,就有一个vfsmount结构体被创建,这个结构体对应一个安装点。
struct file_system_type { const char *name; /*文件系统的名字*/ struct subsystem subsys; /*sysfs子系统对象*/ int fs_flags; /*文件系统类型标志*/ /*在文件系统被安装时,从磁盘中读取超级块,在内存中组装超级块对象*/ struct super_block *(*get_sb) (struct file_system_type*, int, const char*, void *); void (*kill_sb) (struct super_block *); /*终止访问超级块*/ struct module *owner; /*文件系统模块*/ struct file_system_type * next; /*链表中的下一个文件系统类型*/ struct list_head fs_supers; /*具有同一种文件系统类型的超级块对象链表*/ }; struct vfsmount { struct list_head mnt_hash; /*散列表*/ struct vfsmount *mnt_parent; /*父文件系统*/ struct dentry *mnt_mountpoint; /*安装点的目录项对象*/ struct dentry *mnt_root; /*该文件系统的根目录项对象*/ struct super_block *mnt_sb; /*该文件系统的超级块*/ struct list_head mnt_mounts; /*子文件系统链表*/ struct list_head mnt_child; /*子文件系统链表*/ atomic_t mnt_count; /*使用计数*/ int mnt_flags; /*安装标志*/ char *mnt_devname; /*设备文件名*/ struct list_head mnt_list; /*描述符链表*/ struct list_head mnt_fslink; /*具体文件系统的到期列表*/ struct namespace *mnt_namespace; /*相关的名字空间*/ }; |
struct files_struct {//打开的文件集 atomic_t count; /*结构的使用计数*/ …… int max_fds; /*文件对象数的上限*/ int max_fdset; /*文件描述符的上限*/ int next_fd; /*下一个文件描述符*/ struct file ** fd; /*全部文件对象数组*/ …… }; struct fs_struct {//建立进程与文件系统的关系 atomic_t count; /*结构的使用计数*/ rwlock_t lock; /*保护该结构体的锁*/ int umask; /*默认的文件访问权限*/ struct dentry * root; /*根目录的目录项对象*/ struct dentry * pwd; /*当前工作目录的目录项对象*/ struct dentry * altroot; /*可供选择的根目录的目录项对象*/ struct vfsmount * rootmnt; /*根目录的安装点对象*/ struct vfsmount * pwdmnt; /*pwd的安装点对象*/ struct vfsmount * altrootmnt;/*可供选择的根目录的安装点对象*/ }; |
struct nameidata { struct dentry *dentry; /*目录项对象的地址*/ struct vfsmount *mnt; /*安装点的数据*/ struct qstr last; /*路径中的最后一个component*/ unsigned int flags; /*查找标识*/ int last_type; /*路径中的最后一个component的类型*/ unsigned depth; /*当前symbolic link的嵌套深度,不能大于6*/ char *saved_names[MAX_NESTED_LINKS + 1];/ /*和嵌套symbolic link 相关的pathname*/ union { struct open_intent open; /*说明文件该如何访问*/ } intent; /*专用数据*/ }; |
如上的数据结构并不是孤立存在的。正是通过它们的有机联系,VFS才能正常工作。如下的几张图是对它们之间的联系的描述。
如图5所示,被Linux支持的文件系统,都有且仅有一个file_system_type结构而不管它有零个或多个实例被安装到系统中。每安装一个文件系统,就对应有一个超级块和安装点。超级块通过它的一个域s_type指向其对应的具体的文件系统类型。具体的文件系统通过file_system_type中的一个域fs_supers链接具有同一种文件类型的超级块。同一种文件系统类型的超级块通过域s_instances链接。
从图6可知:进程通过task_struct中的一个域files_struct files来了解它当前所打开的文件对象;而我们通常所说的文件描述符其实是进程打开的文件对象数组的索引值。文件对象通过域f_dentry找到它对应的dentry对象,再由dentry对象的域d_inode找到它对应的索引结点,这样就建立了文件对象与实际的物理文件的关联。最后,还有一点很重要的是, 文件对象所对应的文件操作函数列表是通过索引结点的域i_fop得到的。图6对第三部分源码的理解起到很大的作用。
struct file { …… struct list_head f_list; /*文件对象链表*/ struct dentry *f_dentry; /*相关目录项对象*/ struct vfsmount *f_vfsmnt; /*相关的安装文件系统*/ struct file_operations *f_op; /*文件操作表*/ …… }; struct file_operations { …… //文件读操作 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); …… //文件写操作 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); …… int (*readdir) (struct file *, void *, filldir_t); …… //文件打开操作 int (*open) (struct inode *, struct file *); …… }; |