虚拟文件系统所隐含的思想是把表示很多不同种类文件系统的共同信息放入内核:其中有一个字段或函数来支持Linux所支持的所有实际文件系统所提供的任何操作。对所调用的每个读、写或其他函数,内核都能把它们替换成支持本地Linux文件系统、NTFS文件系统,或者文件所在的任何其他文件系统的实际函数。
一、虚拟文件系统(VFS)的作用
虚拟文件系统(Virtual Filesystem)也可以称之为虚拟文件系统转换(Virtual Filesystem Switch,VFS),是一个内核软件层,用来处理与Unix标准文件系统相关的所有系统调用。其健壮性表现在能为各种文件系统提供一个通用的接口。
VFS支持的文件系统可以划分为三种主要类型:
磁盘文件系统:这些文件系统管理在本地磁盘分区中可用的存储空间或者其他可以起到磁盘作用的设备(比如一个USB闪存)。VFS支持的基于磁盘的某些著名文件系统还有:
* Linux使用的文件系统,如广泛使用的第二扩展文件系统(Ext2),新近的第三扩展文件系统(Third Extended Filesystem, Ext3)及Reiser文件系统(ReiserFS)。
* Unix家族的文件系统。
* 微软公司的文件系统,如MS-DOS、VFAT及NTFS。
* ISO9660 CD-ROM文件系统(以前的High Sierra文件系统)和通用磁盘格式(UDF)的DVD文件系统。
* 其他有专利权的文件系统。
* 起源于非Linux系统的其他日志文件系统,如IBM的JFS和SGI的XFS。
网络文件系统:这些文件系统允许轻易地访问属于其他网络计算机的文件系统所包含的文件。虚拟文件系统所支持的一些著名的网络文件系统有:NFS、Coda、AFS(Andrew文件系统)、CIFS以及NCP。
特殊文件系统:这些文件系统不管理本地或者远程磁盘空间。/proc文件系统是特殊文件系统的一个典型范例。
Unix的目录建立了一颗根目录为“/”的树。根目录包含在跟文件系统(root filesystem)中,在Linux中这个根文件系统通常就是Ext2或Ext3类型。其他所有的文件系统都可以被“安装”在根文件系统的子目录中。
1.1、通用文件模型
通用文件模型由下列对象类型组成:
超级块对象(superblock object):存放已安装文件系统有关信息。对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件系统控制块(filesystem control block)。
索引节点对象(inode object):存放关于具体文件的一般信息。对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件控制块(file control block)。每个索引节点对象都有一个索引节点号,这个节点号唯一地标识文件系统中的文件。
文件对象(file object):存放打开文件与进程之间进行交互的有关信息。这类信息仅当进程访问文件期间存在于内核内存中。
目录项对象(dentry object):存放目录项(也就是文件的特定名称)与对应文件进行链接的有关信息。每个磁盘文件系统都以自己特有的方式将该类信息存在磁盘上。
磁盘高速缓存(disk cache)属于软件机制,它允许内核将原本存在磁盘上的某些信息保存在RAM中,以便对这些数据的进一步访问能快速进行,而不必慢慢访问磁盘本身。
注意,磁盘高速缓存不同于硬件高速缓存或内存高速缓存,后两者都与磁盘或其他设备无关。硬件高速缓存是一个快速静态RAM,它加快了直接对慢速动态RAM的请求。内存高速缓存是一种软件机制,引入它是为了绕过内核内存分配器。
除了目录项高速缓存和索引节点高速缓存之外,Linxu还使用其他磁盘高速缓存。其中最重要的一种就是所谓的页高速缓存。
1.2、VFS所处理的系统调用
二、VFS的数据结构
每个VFS对象都存放在一个适当的数据结构中,其中包括对象的属性和指向对象方法表的指针。内核可以动态地修改对象的方法,因此可以为对象建立专用的行为。
2.1、超级块对象
所有超级块对象都以双向循环链表的形式链接在一起。链表中第一个元素用super_blocks变量来表示,而超级块对象的s_list字段存放指向链表相邻元素的指针。sb_lock自旋锁保护链表免受多处理器系统上的同时访问。
与超级块关联的方法就是所谓的超级块操作。这些操作是由数据结构super_operations来描述的,该结构的启示地址存放在超级块的s_op字段中。
2.2、索引节点对象
文件系统处理文件所需要的所有信息都放在一个名为索引节点的数据结构中。文件名可以随时更改,但是索引节点对文件是唯一的,并且随文件的存在而存在。内存中的索引节点对象由一个inode数据结构组成。
每个索引节点对象总是出现在下列双向循环链表的某个链表中(所有情况下,指向相邻元素的指针存放在i_list字段中):
* 有效未使用的索引节点链表,典型的如那些镜像有效的磁盘索引节点,且当前未被任何进程使用。这些索引节点不为脏,且它们的i_count字段置为0。链表中的首元素和尾元素是由变量inode_unused的next字段和prev字段分别指向的。这个链表用作磁盘高速缓存。
* 正在使用的索引节点链表,也就是那些镜像有效的磁盘索引节点,且当前被某些进程使用。这些索引节点不为脏,但它们的i_count字段为正数。链表中的首元素和尾元素是由变量inode_in_use引用的。
* 脏索引节点的链表。链表中的首元素和尾元素是由相应超级块对象的s_dirty字段引用的。
与索引节点对象关联的方法也叫索引节点操作。它们由inode_operation结构来描述,该结构的地址存放在i_op字段中。
2.3、文件对象
文件对象描述进程怎样与一个打开的文件进行交互。交互对象是在文件被打开时创建的,由一个file结构组成。注意,文件对象在磁盘上没有对应的映像,因此file结构中没有设置“脏”字段来表示文件对象是否已被修改。
存放在文件对象中的主要信息是文件指针,即文件中当前的位置,下一个操作将在该位置发生。由于几个进程可能同时访问同一文件,因此文件指针必须存放在文件对象而不是索引节点对象中。
文件对象通过一个名为filp的slab高速缓存分配,filp描述符地址存放在filp_cachep变量中。由于分配的文件对象数目是有限的,因此files_stat变量在其max_files字段中指定了可分配文件对象的最大数目,也就是系统可同时访问的最大文件数。
文件对象的f_count字段是一个引用计数器:它记录使用文件对象的进程数(记住,以CLONE_FILES标志创建的轻量级进程共享打开文件表,因此它们可以使用相同的文件对象)。当内核本身使用该文件对象时也要增加计数器的值--例如,把对象插入链表中或发出dup()系统调用时。
2.4、目录项对象
一旦目录项被读入内存,VFS就把它转换成基于dentry结构的一个目录项对象。对于进程查找的路径名中的每个分量,内核都为其创建一个目录项对象;目录项对象将每个分量与其对应的索引节点相联系。例如,在查找路径名/tmp/test时,内核为根目录“/”创建一个目录项对象,为根目录下的tmp项创建一个第二级目录项对象,为/tmp目录下的test项创建一个第三级目录项对象。
每个目录项对象可以处于以下四种状态之一:
空闲状态(free):处于该状态的目录项对象不包括有效的信息,且还没有被VFS使用。对应的内存区由slab分配器进行处理。
未使用状态(unused):处于该状态的目录项对应当前还没有被内核使用。该对象的引用计数器d_count的值为0,但其d_inode字段仍然指向关联的索引节点。该目录项对象包含有效的信息,但为了在必要时回收内存,它的内容可能被丢弃。
正在使用状态(in use):处于该状态的目录项对象当前正在被内核使用。该对象的引用计数器d_count的值为正数,其d_inode字段指向关联的索引节点对象。该目录项对象包含有效的信息,并且不能被丢弃。
负状态(negative):与目录项关联的索引节点不复存在,那是因为相应的磁盘索引节点已被删除,或者因为目录项对象是通过解析一个不存在的路径名创建的。目录项对象的d_inode字段被置为NULL,但该对象仍然被保存在目录项高速缓存中,以便后续对同意文件目录名的查找操作能够快速完成。
2.5、目录项高速缓存
为了最大限度地提高处理同一个文件需要被反复访问的这些目录项对象的效率,Linux使用目录项高速缓存,它由两种类型的数据结构组成:
* 一个处于正在使用、未使用或负状态的目录项对象的集合。
* 一个散列表,从中能够快速获取与给定的文件名和目录名对应的目录项对象。同样,如果访问的对象不在目录项高速缓存中,则散列表函数返回一个空值。
目录项高速缓存的作用还相当于索引节点高速缓存(inode cache)的控制器。在内核内存中,并不丢弃与未用目录项相关的索引节点,这是由于目录项高速缓存仍在使用它们。因此,这些索引节点对象保存在RAM中,并能够借助相应的目录项快速引用它们。
所有“未使用”目录项对象都存放在一个“最近最少使用(Least Recently used, LRU)”的双向链表中,该链表按照插入的时间顺序。换句话说,最后释放的目录项对象放在链表的首部,所以最近最少使用的目录项对象总是靠近链表的尾部。一旦目录项高速缓存的空间开始变小,内核就从链表的尾部删除元素,使得最近最常使用的对象得以保留。
2.6、与进程相关的文件
每个进程都有它自己当前的工作目录和它自己的根目录。这仅仅是内核用来表示进程与文件系统相互作用所必须维护的数据中的两个例子。类型为fs_struct的整个数据结构就用于此目的,且每个进程描述符的fs字段就指向进程的fs_struct结构。
fd字段指向文件对象的指针数组。该数组的长度存放在max_fds字段中。通常,fd字段指向files_struct结构的fd_array字段,该字段包括32个文件对象指针。如果进程打开的文件数目多余32,内核就分配一个新的、更大的文件指针数组,并将其地址存放在fd字段中,内核同时也更新max_fds字段的值。
对于在fd数组中有元素的每个文件来说,数组的索引就是文件描述符(file descriptor)。通常,数组的第一个元素(索引为0)是进程的标准输入文件,数组的第二个元素(索引为1)是进程的标准输出文件,数组的第三个元素(索引为2)是进程的标准错误文件。
三、文件系统类型
文件系统注册----也就是通常在系统初始化期间并且在使用文件系统类型之前必须执行的基本操作。一旦文件系统被注册,其特定的函数对内核就是可用的,因此文件系统类型可以安装在系统的目录树上。
3.1、特殊文件系统
3.2、文件系统类型注册
文件系统的源代码实际上要么包含在内核映像中,要么作为一个模块被动态装入。
所有文件系统类型的对象都插入到一个单向链表中。由变量file_systems指向链表的第一个元素,而结构中的next字段指向链表的下一个元素。file_systems_lock读/写自旋锁保护整个链表免受同时访问。
在系统初始化期间,调用register_filesystem()函数来注册编译时指定的每个文件系统;该函数把相应的file_system_type对象插入到文件系统类型的链表中。
当实现了文件系统的模块被装入时,也要调用register_filesystem()函数。在这种情况下,当该模块被卸载时,对应的文件系统也可以被注销(调用unregister_filesystem()函数)。
四、文件系统处理
就像每个传统的Unix系统一样,Linux也使用系统的根文件系统(system's root filesystem):它由内核在引导阶段直接安装,并拥有系统初始化脚本以及最基本的系统程序。
其他文件系统要么由初始化脚本安装,要么由用户直接安装在已安装文件系统的目录上。作为一个目录树,每个文件系统都拥有自己的根目录(root directory)。安装文件系统的这个目录称之为安装点(mount point)。已安装文件系统属于安装点目录的一个子文件系统。例如,/proc虚拟文件系统是系统的根文件系统的孩子(且系统的根文件系统是/proc的父亲)。已安装文件系统的根目录隐藏了父文件系统的安装点目录原来的内容,而且父文件系统的整个子树位于安装点之下。
4.1、命名空间
在传统的Unix系统中,只有一个已安装文件系统树:从系统的根文件系统开始,每个进程通过指定合适的路径名可以访问已安装文件系统中的任何文件。从这个方面考虑,Linux2.6更加的精确:每个进程可拥有自己的已安装文件系统树----叫做进程的命令空间(namespace)。
通常大多数进程共享同一个命名空间,即位于系统的根文件系统且被init进程使用的已安装文件系统树。不过,如果clone()系统调用以CLONE_NEWNS标志创建一个新进程,那么进程将获取一个新的命名空间。这个新的命名空间随后由子进程继承(如果父进程没有以CLONE_NEWNS标志创建这些子进程)。
当进程安装或卸载一个文件系统时,仅修改它的命名空间。因此,所做的修改对共享同一命名空间的所有进程都是可见的,并且也只对它们可见。进程甚至可通过使用Linux特有的pivot_root()系统调用来改变它的命名空间的根文件系统。
4.2、文件系统安装
不管一个文件系统被安装了多少次,都仅有一个超级块对象。
安装的文件系统形成一个层次:一个文件系统的安装点可能成为第二个文件系统的目录,而第二个文件系统又安装在第三个文件系统之上。
把多个安装堆叠在一个单独的安装上也是可能的。尽管已经使用先前安装下的文件和目录的进程可以继续使用,但在同一安装点上的新安装隐藏前一个安装的文件系统。当最顶层的安装被删除时,下一层的安装再一次变为可见的。
4.3、安装普通文件系统
mount()系统调用被用来安装一个普通文件系统;它的服务历程sys_mount()作用于以下参数:
* 文件系统所在的设备文件的路径名,或者如果不需要的话就为NULL。
* 文件系统被安装其上的某个目录的目录路径名(安装点)。
* 文件系统的类型,必须是已注册文件系统的名字。
* 安装标志。
* 指向一个与文件系统相关的数据结构的指针(也许为NULL)。
4.3.1、do_kern_mount()函数
4.3.2、分配超级块对象
get_sb_bdev()执行的最重要的操作如下:
1、调用open_bdev_excl()打开设备文件名为dev_name的块设备。
2、调用sget()搜索文件系统的超级块对象链表(type->fs_supers)。如果找到一个与块设备相关的超级块,则返回它的地址。否则,分配并初始化一个新的超级块对象,把它插入到文件系统链表和超级块全局链表中,并返回其地址。
3、如果不是新的超级块,则跳到第6步。
4、把参数flags中的值拷贝到超级块的s_flags字段,并将s_id、s_old_blocksize以及s_blocksize字段设置为块设备的合适值。
5、调用依赖文件系统的函数(该函数作为传递给get_sb_bdev()的最后一个参数)访问磁盘上的超级块信息,并填充新超级块对象的其他字段。
6、返回新超级块对象的地址。
4.4、安装根文件系统
安装根文件系统分两个阶段,如下所示:
1、内核安装特殊rootfs文件系统,该文件系统仅提供一个作为初始安装点的空目录。
2、内核在空目录上安装实际根文件系统。
4.4.1、阶段1:安装rootfs文件系统
第一阶段是由init_rootfs()和Init_mount_tree()函数完成的,它们在系统初始化过程中执行。
4.4.2、阶段2:安装实际根文件系统
4.5、卸载文件系统
umount()系统调用用来卸载一个文件系统。相应的sys_umount()服务例程作用于两个参数:文件名(多是安装点目录或是块设备文件名)和一组标志。
五、路径名查找
当进程必须识别一个文件时,就把它的文件路径名传递给某个VFS系统调用,如open()、mkdir()、rename()或stat()。
执行这一任务的标准过程就是分析路径名并把它拆分成一个文件名序列除了最后一个文件名以外,所有的文件名都必定是目录。
如果路径名的第一个字符是“/”,那么这个路径名是绝对路径,因此从current->fs->root(进程的根目录)所标识的目录开始搜索。否则,路径名是相对路径,因此从current->fs->pwd(进程的当前目录)所标识的目录开始搜索。
在对初始目录的索引节点进行处理的过程中,代码要检查与第一个名字匹配的目录项,以获得相应的索引节点。然后,从磁盘读出包含那个索引节点的目录文件,并检查与第二个名字匹配的目录项,以获得相应的索引节点。对于包含在路径中的每个名字,这个过程反复执行。
目录项高速缓存极大地加速了这一过程,因为它把最近最常使用的目录项对象保留在内存中。正如我们以前看到的,每个这样的对象使特定目录中的一个文件名与它相应的索引节点相联系,因此在很多情况下,路径名的分析可以避免从磁盘读取中间目录。
但是,事情并不像看起来那么简单,因为必须考虑如下的Unix和VFS文件系统的特点:
* 对每个目录的访问权必须进行检查,以验证是否允许进程读取这一目录的内容。
* 文件名可能是与任意一个路径名对应的符号链接;在这种情况下,分析必须扩展到那个路径名的所有分量。
* 符号链接可能导致循环引用;内核必须考虑这个可能性,并能在出现这种情况时将循环终止。
* 文件名可能是一个已安装文件系统的安装点。这种情况必须检测到,这样,查找操作必须延伸到新的文件系统。
* 路径名查找应该在发出系统调用的进程的命名空间中完成。由具有不同命名空间的两个进程使用的相同路径名,可能指定了不同的文件。
路径名查找是由path_lookup()函数执行的,它接收三个参数:
name:指向要解析的文件路径名的指针。
flags:标志的值,表示将会怎样访问查找的文件。
nd:nameidata数据结构的地址,这个结构存放了查找操作的结果。
5.1、标准路径名查找
5.2、父路径名查找
5.3、符号链接的查找
六、VFS系统调用的实现
6.1、open()系统调用
open()系统调用的服务例程为sys_open()函数,该函数接收的参数为:要打开文件的路径名filename、访问模式的一些标志flags,以及如果该文件被创建所需要的许可权位掩码mode。如果该系统调用成功,就返回一个文件描述符,也就是指向文件对象的指针数组current->files->fd中分配给新文件的索引;否则,返回-1。
6.2、read()和write()系统调用
read()和write()系统调用非常相似。它们都需要三个参数:一个文件描述符fd、一个内存区的地址buf(该缓冲区包含要传送的数据),以及一个数count(指定应该传送多少字节)。当然,read()把数据从文件传送到缓冲区,而write()执行相反的操作。两个系统调用都返回所成功传送的字节数,或者发送一个错误条件的信号并返回-1。
返回值小于count并不意味着发生了错误。即使请求的字节没有被传送,也总是允许内核终止系统调用,因此用户应用程序必须检查返回值并重新发出系统调用(如果必要)。在以下几种典型情况下返回小的值:当从管道或终端设备读取时,当读到文件的末尾时,或者当系统调用被信号中断时。文件结束条件(EOF)很容易从read()的空返回值中判断出来。这个条件不会与因信号引起的异常终止混淆在一起,因为如果读取数据之前read()被一个信号中断,则发生一个错误。
读或写操作总是发生在由当前文件指针所指定的文件偏移处(文件对象的f_pos字段)。两个系统调用都通过把所传送的字节数加到文件指针上而更新文件指针。
sys_read()(read()的服务例程)和sys_write()(write()的服务例程)几乎都执行相同的步骤:
1、调用fget_light()从fd获取相应文件对象的地址file。
2、如果file->f_mode中的标志不允许所请求的访问(读或写操作),则返回一个错误码-EBADF。
3、如果文件对象没有read()或aio_read()(write()或aio_write())文件操作,则返回一个错误码-EINVAL。
4、调用access_ok()粗略地检查buf和count参数。
5、调用rw_verify_area()对要访问的文件部分检查是否有冲突的强制锁。如果有,则返回一个错误码,如果该锁已经被F_SETLKW命令请求,那么就挂起当前进程。
6、调用file->f_op->read或file->f_oop->write方法(如果已定义)来传送数据;否则,调用file-?f_op->aio_read或file->f_op->aio_write方法。所有这些方法都返回实际传送的字节数。另一方面的作用是,文件指针被适当地更新。
7、调用fput_light()释放文件对象。
8、返回实际传送的字节数。
6.3、close()系统调用
close()系统调用接收的参数为要关闭文件的描述符fd。sys_close()服务例程执行下列操作:
1、获得存放在current->files->fd[fd]中的文件对象的地址;如果它为NULL,则返回一个出错码。
2、把current->files->fd[fd]置为NULL。释放文件描述符fd,这是通过清除current->files中的open_fds和close_on_exec字段的相应位来进行的。
3、调用filp_close(),该函数执行下列操作:
a、调用文件操作的flush方法(如果已定义)。
b、释放文件上的任何强制锁。
c、调用fput()释放文件对象。
4、返回0或一个出错码。出错码可由flush方法或文件中的前一个写操作错误产生。
七、文件加锁
不管进程是使用劝告锁还是强制锁,它们都可以使用共享读锁和独占写锁。在文件的某个区字段上,可以有任意多个进程进行读,但在同一时刻只能有一个进程进行写。此外,当其他进程对同一个文件都拥有自己的读锁时,就不可能获得一个写锁,反之亦然。
7.1、Linux文件加锁
进程可以采用以下两种方式获得或释放一个文件劝告锁:
* 发出flock()系统调用。传递给它的两个参数为文件描述符fd和指定锁操作的命令。该锁应用于整个文件。
* 使用fcntl()系统调用。传递给它的三个参数为文件描述符fd、指定锁操作的命令以及指向flock结构的指针。flock结构中的几个字段允许进程指定要加锁的文件部分。因此进程可以在同一文件的不同部分保持几个锁。
7.2、文件锁的数据结构
7.3、FL_FLOCK锁
7.4、FL_POSIX锁
FL_POSIX锁总是与一个进程和一个索引节点相关联。当进程死亡或一个文件描述符被关闭时(即使该进程对同一文件打开了两次或复制了一个文件描述符),这种锁会被自动地释放。此外,FL_POSIX锁绝不会被子进程通过fork()继承。
当使用fcntl()系统调用对文件加锁时,该系统调用作用于三个参数:要加锁文件的文件描述符fd、指向锁操作的参数cmd,以及指向存放在用户态进程地址空间中的flock数据结构的指针f1。
阅读(447) | 评论(0) | 转发(0) |