十二、虚拟文件系统

虚拟文件系统所隐含的思想是把表示很多不同种类文件系统的共同信息放入内核;其中有一个字段或函数来支持linux所支持的所有文件系统所提供的任何操作。对所调用的每个读、写或其他函数,内核都能把他们替换成支持本地linux文件系统、NTFS文件系统或者文件所在的其他任何文件系统的实际函数。

12.1 虚拟文件系统的作用

VFS支持的文件系统可以划分为三种主要类型:

1、磁盘文件系统  

  这些文件系统管理在本地磁盘分区中可用的存储空间或者其他可以起到磁盘作用的设备(比如USB闪存)。

2、网络文件系统

  这些文件系统允许轻易的访问属于其他网络计算机的文件系统所包含的文件。

3、特殊文件系统

  这些文件系统不管理本地货远程磁盘空间。

基于磁盘的文件系统通常存放在硬件块设备中,如硬盘;虚拟块设备也可以用来安装普通文件所在的文件系统;用户可以保护自己的私有文件系统,这可以通过把自己文件系统的加密版本存放在一个普通文件中来实现。

12.1.1 通用文件模型

要实现每个具体的文件系统,必须将其物理组织结构转换为虚拟文件系统的通用文件模型。例如,对基于FAT(File Allocation Table)的文件系统,linux必须在必要时能够快速建立对应于目录的文件。这样的文件只作为内核内存的对象而存在,不对应实际的磁盘空间。

linux内核不能对一个特定的函数进行硬编码(硬编码指将一个可变变量用一个固定的值来代替的方法)来执行诸如read()或ioctl()这样的操作,而是对每个操作都必须使用一个指针,指向要访问的具体文件系统的适当函数。

通用文件模型有下列对象类型组成:

1、超级块对象

存放已安装文件系统的有关信息。对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件系统控制块。

2、索引节点对象

存放关于具体文件的一般信息。对于基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件控制块。每个索引节点对象都有一个索引节点号,这个节点号唯一的标识文件系统中的文件。

3、文件对象

存放打开文件与进程之间进行交互的有关信息。这类信息仅当进程访问文件期间存在于内核中。

4、目录项对象

存放目录项与对应文件进行链接的有关信息。每个磁盘文件系统都以自己特有的方式将该类信息存放在磁盘上。

VFS一方面为所有文件系统的实现提供了一个通用的接口,另一方面使用目录项高速缓存机制,加速了从文件路径名到最后一个路径分量的索引节点的转换过程,从而提高了系统性能。

12.1.2 VFS所处理的系统调用

在某些情况下,一个文件操作可能有VFS本身去执行,无需调用低层函数。例如,当进程关闭打开的文件时,并不需要设计磁盘上的相应文件,因此VFS只需要释放对应的文件对象;当系统调用lseek()修改一个文件的指针,而这个文件指针是打开文件与进程交互所设计的一个属性时,VFS只需要修改对应的文件对象,而不必访问磁盘上的文件,因此无需调用具体文件系统的函数。可以把VFS看成“通用”文件系统,在必要时以来某种具体文件系统。

12.2 VFS的数据结构

每个VFS对象都存放在一个适当的数据结构中,其中包括对象的属性和指向对象方法表的指针。内核可以动态的修改这些对象的方法,因此可以为对象建立专用的行为。

12.2.1 超级块对象

所有的超级块对象都以双向循环链表的形式链接在一起。链表中第一个元素用super_blocks变量表示。超级块对象中的s_list字段指向链表相邻元素的指针。

s_fs_info字段指向属于具体文件系统的超级块信息。为了效率起见,由s_fs_info字段所指向的数据被复制到内存。任何基于磁盘的文件系统都需要访问和更改自己的磁盘分配位图(磁盘分配位图指存在于磁盘中用来标识磁盘每个块是否空闲的一段存储空间),以便分配或释放块。VFS允许这些文件系统直接对内存超级块的s_fs_info字段进行操作,而无需访问磁盘。有可能VFS超级块最终不再与磁盘上相应的超级块同步。因此,super_block中引入一个s_dirt标志来表示是否需要更新磁盘上的数据。

与超级块关联的方法就是超级块操作,由super_operations来描述,该结构的起始地址存放在超级块的s_op字段中。

12.2.2 索引节点对象

文件系统处理文件所需要的所有信息都放在一个名为索引节点的数据结构中。文件名可以随时更改,但是索引节点对文件是唯一的,并且随文件的存在而存在。内存中索引节点由inode数据结构组成。

每个索引节点对象都会复制磁盘索引节点包含的一些数据,比如分配给文件的磁盘块数。

i_state字段的意义:

I_DIRTY_SYNC\IDIRY_DATASYNC\I_DIRTY_PAGES 该索引节点是脏的,对应的磁盘索引必须更新。I_DIRTY宏可以用来检查三个标志的值
I_LOCK  涉及的索引节点对象处于IO传送中 
I_FREEING  索引节点正在被释放
I_CLEAR 索引节点对应的内容不再有意义
I_NEW 索引节点已经分配但是还没有用从磁盘索引节点读取的数据来填充

 

 

 

 

索引节点链表i_list:

 
有效未使用的索引节点链表 不为脏,i_count=0,变量inode_unused的next和prev字段分别指向链表首元素和尾元素,该链表用于磁盘高速缓存,如镜像有效的磁盘索引节点
正在使用的索引节点链表 不为脏,i_count>0,变量inode_in_use的next和prev指向链表首尾,其中索引节点正被某些进程使用
脏索引节点链表 为脏,super_block中的s_dirty字段指向首尾

 

 

 

 

super_block的s_inodes字段指向当前文件系统包含的所有索引节点链表头。inode中的i_sb_list字段存放相邻元素。

inode_hashtable散列表。每个索引节点存放其中,通过索引节点号以及super_block地址可以在散列表中找到包含与目标索引节点同样散列值的链表。inode中i_hash字段用来指向该链表中相邻的索引节点元素。

索引节点操作方法由inode_operations结构来描述,存放在inode的i_ops字段中。

12.2.3 文件对象

文件对象描述进程怎样与一个打开的文件进行交互。文件对象是文件被打开时创建的,由一个file结构组成。文件对象在磁盘上没有对应的映像,因此file解雇中没有设置脏字段来表示文件对象是否已被修改。

存放在文件对象中的主要信息是文件指针,即文件中当前的位置,下一个操作将在该位置上发生。由于几个进程可能同时访问同一文件,因此文件指针必须存放在文件对象而不是索引节点中。文件对象通过名为filp的slab高速缓存分配,filp描述符地址存放在filp_cachep变量中。files_stat变量在其max_files字段中指定了可分配文件对象的最大数目,也就是系统可同时访问的最大文件数。

每个super_block对象把文件对象链表的头存放在s_files中,属于不同文件系统的文件对象包含在不同的链表中;文件对象的f_list字段指向链表的相邻元素;files_lock自旋锁保护超级块的s_files链表免受多处理器上的同时访问。

f_count字段记录使用文件对象的进程数(以CLONE_FILES标志创建的轻量级进程共享打开文件表,他们可以使用相同的文件对象)。当内核本身使用该文件对象时也要增加计数器的值。

当VFS代表进程必须打开一个文件时,它调用get_empty_filp()函数分配一个新的文件对象。函数调用kmem_cache_alloc()从filp高速缓存中获得一个空闲的文件对象,然后初始化这个对象的字段,将文件对象的f_count字段设为1.

当内核将一个索引节点从磁盘装入内存时,inode中的i_fop字段被赋值。当进程打开文件时,VFS就用inode中的i_fop初始化文件对象的f_op字段,使得对文件操作的后续调用能够使用这些函数。如果需要,VFS随后也可以通过在f_op字段存放一个新值而修改文件操作的集合。

12.2.4 目录项对象

对进程查找的路径名中的每个分量,内核都为其创建一个目录项对象;目录项对象将每个分量与其对应的索引节点相联系。例如,在查找/tmp/test时,内核为根目录"/"创建一个目录项对象,为根目录下的透明片想创建一个第二级目录项对象,为/tmp目录下的test项创建一个第三极目录项对象。目录项对象在磁盘上并没有对应的映像,因此dentry不含‘脏’字段。目录项对象存放在名为dentry_cache的slab分配器高速缓存中。

目录项状态:

空闲状态 目录项不包括有效信息,且还没有被VFS使用。对应的内存由slab分配器进行处理
未使用状态 当前没有被内核使用。d_count=0,d_inode字段指向关联的索引节点。该目录项包含有效信息,但为了在必要时回收内存,内容可能被丢弃
正在使用状态 当前正被内核使用,d_count>0,d_inode指向关联inode,包含有效信息,不能被丢弃
负状态 关联索引节点不存在,因为磁盘索引节点已被删除或者目录项因为解析一个不存在的文件路径名创建的。d_inode=NULL。该对象仍然保存在高速缓存中,以便后续对同一文件目录名的查找操作能够快速完成

 

 

 

 

 

目录项操作相关方法存放在d_op字段中,为dentry_operations结构。

12.2.5 目录项高速缓存

linux使用目录项高速缓存,它由两种类型的数据结构组成:

1、一个处于正在使用、未使用或负状态的目录项的集合

2、一个散列表,从中能够快速获取与给定的文件名和目录名对应的目录项对象。如果访问的对象不在目录项高速缓存中,则散列函数返回一个空值。

目录项高速缓存的作用还相当于索引节点高速缓存的控制器。在内核内存中,并不丢弃与未用目录项相关的索引节点,这是由于目录项高速缓存仍在使用它们。

所有“未使用”目录项对象都存放在一个“最近最少使用”的双向链表中,链表按插入时间排序。一旦目录项高速缓存的空间开始变小,需要删除元素,则从链表的尾部删除元素,使得最近最常使用的对象得以保存。LRU链表的首元素和尾元素的地址存放在list_head类型的dentry_unused变量的next和prev字段中。目录项对象的d_lru字段包含指向链表中相邻目录的指针。

每个“正在使用”的目录项对象都被插入一个双向链表中个,该链表由相应索引节点对象的list_head类型的i_dentry字段指向(由于每个索引节点可能与若干硬链接关联,所以需要一个链表表示与同一inode关联的所有目录项)。目录项对象的d_alias字段指向链表中相邻元素。

当指向相应文件的最后一个硬链接被删除后,一个“正在使用”的目录项可能变成“负”状态。这时,该目录项对象被移动到LRU的链表首。每当往LRU中添加新元素时,该对象则不断向尾部移动。当内核开始缩减目录项高速缓存时,从尾部开始释放元素,该对象则可能会被释放。

散列表是由dentry_hashtable数组实现的。数组中的每个元素是一个指向链表的指针,这种链表把具有相同散列值的目录项进行散列而形成。该数组的长度取决于系统已安装的RAM的数量;缺省值是每兆字节RAM含256个元素。目录项对象的d_hash字段包含指向具有相同散列值的链表中的相邻元素。散列函数产生的值是由目录的目录项对象以及文件名计算出来的。

dcache_lock自旋锁保护目录项高速缓存数据。d_lookup()函数在散列表中查找给定的父目录项对象和文件名。

12.2.6 与进程相关的文件

fs_struct结构包含当前工作目录、根目录、文件系统对象等信息;files_struct表示打开的文件。task_struct中fs和files字段分别为fs_struct何files_struct结构。

files_struct中分段字段指向文件对象的指针数组。该数组长度存放在max_fds字段中。通常fd字段指向fd_array字段,该字段包含32个文件对象指针。如果进程打开的文件数目多于32个,内核就分配一个新的、更大的文件指针数组,并将其地址存放在fd字段中,内核同时更新max_fds的值。

files_struct中open_fds字段最初包含open_fds_init字段的地址,open_fds_init字段表示当前已经打开文件的文件描述符的位图。max_fdset字段存放位图中的位数。由于fd_set数据结构有1024位,通常不需要扩大位图的大小。

当内核开始使用一个文件对象时,内核提供fget()函数以供调用。这个函数接收文件描述符fd作为参数,返回current->files->fd[]中的地址,增加f_count;当内核控制路径完成对文件对象的使用时,调f_put()函数。将文件对象地址作为参数,并减少文件对象引用计数器f_count,如果f_count为0,调用release(),减少索引节点对象i_write count字段的值,将文件从超级块链表中移走,释放文件给slab分配器,最后减少相关文件系统描述符的目录项对象的引用计数器的值。

12.3 文件系统类型

12.3.1 特殊文件系统

网络和磁盘文件系统能够使用户处理存放在内核之外的信息;特殊文件系统可以为系统程序员和管理员提供一种容易的方式来操作内核的数据结构并实现操作系统的特殊特征。

最常用的特殊文件系统:

名字 安装点 说明
bdev 块设备
binfmt_misc 任意 其他可执行格式
devpts /dev/pts 伪终端支持
eventpollfs 由有效事件轮询机制使用
futexfs 由futex(快速用户空间枷锁)机制使用
pipefs 管道
proc /proc 对内核数据结构的常规访问点
rootfs 为启动阶段提供一个空的根目录
shm IPC共享线性区
mqueue 任意 实现POSIX消息队列时使用
sockfs 套接字
sysfs /sys 对系统数据的常规访问点
tmpfs 任意 临时文件(如果不被交换出去就保持在RAM)
usbfs /proc/bus/usb USB设备

 

 

 

 

 

 

 

 

 

 

 

 

特殊文件系统不限于物理块设备。内核给每个安装的特殊文件系统分配一个虚拟的块设备,让其主设备号为0而次设备号任意。set_anon_super()函数用于初始化特殊文件系统的超级块;该函数本质上获得一个未使用的次设备号dev,然后用主设备号和dev设置新超级块的s_dev字段。kill_anon_super()函数移走特殊文件系统的超级块。虚拟块设备标示符有助于内核以统一的方式处理特殊文件系统和普通文件系统。

12.3.2 文件系统类型注册

VFS必须对代码目前已在内核中的所有文件系统进行跟踪,这可以通过进行文件系统的注册来实现。

每个注册的文件系统都用一个类型为file_system_type的对象来表示。

所有文件系统类型的对象都插入到一个单向链表中。变量file_systems指向第一个file_system_type元素,元素内部的next字段指向下一个。file_systems_lock读写自旋锁保护链表。

fs_super字段表示给定类型的已安装文件系统所对应的超级块链表的头。super_block中的s_instances指向链表相邻元素。

get_sb字段指向依赖于文件系统类型的函数,该函数分配一个新的super_block对象并进行初始化。kill_sb字段指向删除超级块的函数。

fs_flags存放文件系统类型标志。FS_REQUIRES_DEV(文件系统必须位于物理磁盘设备上)FS_BINARY_MOUNTDATA(文件系统使用的二进制安装数据)FS_REVAL_DOT(使.和..路径重新生效,针对网络文件系统)FS_ODD_RENAME(重命名就是移动操作,针对网络文件系统)

在系统初始化期间,调用register_filesystem()函数注册编译时指定的每个文件系统;该函数把相应的file_system_type插入到文件系统类型的链表中。

当实现了文件系统的模块被装入时,也要调用register_filesystem()函数。

get_fs_type()函数接收文件系统名称参数,扫描已注册的文件系统链表返回对应的file_system_type对象指针。

12.4 文件系统处理

linux使用系统的根文件系统:它由内核在引导阶段直接安装,并拥有系统初始化脚本以及最基本的系统程序。

其他文件系统要么由初始化脚本安装,要么由用户直接安装在已安装文件系统的目录上。作为一个目录树,每个文件系统都拥有自己的根目录。安装文件系统的这个目录叫做安装点(mount point)。已安装文件系统属于安装点目录的一个子文件系统。例如/proc虚拟文件系统就是系统跟文件系统的孩子。已安装文件系统的根目录隐藏了父文件系统的安装点目录原来的内容,而起父文件系统的整个子树位于安装点之下。

12.4.1 命名空间

每个进程可以拥有自己的已安装文件系统树——叫做进程的命名空间。

通常大多数进程共享同一命名空间,即位于系统根文件系统且被init进程使用的已安装文件系统树。不过,如果clone()系统调用以CLONE_NEWS标志建立一个新进程,那么进程将获取一个新的命名空间。当进程安装或卸载一个文件系统时,仅修改它的命名空间。因此,所做的修改堆共享同一命名空间的所有进程是可见的,并且只对他们可见。

进程的命名空间由进程描述符的namespace字段指向的namespace结构描述。字段如下:

atomic_t  count ;//引用计数器

struct vfsmount * root;//命名空间根目录的已安装文件系统描述符

struct list_head list;//属于命名空间的所有已安装文件系统描述符链表的头

struct rw_semphore sem;

12.4.2 文件系统的安装

linux下同一文件系统可以被安装多次。如果一个文件系统被安装了n次,那么它的根目录就可通过n个安装点来访问。尽管同一文件系统可以通过不同的安装点来访问,但是文件系统是唯一的。因此不管一个文件系统被安装了多少次,都仅有一个超级块对象。

安装的文件系统形成一个层次:一个文件系统的安装点可能成为第二个文件系统的目录,而第二个文件系统又安装在第三个文件系统之上。

把多个安装堆叠在一个单独的安装点上也可以。在同一安装点上的新安装隐藏前一个安装的文件系统。当最顶层的安装被删除时,下一层的安装再一次变为可见。

内核必须在内存中保存安装点和安装标志,以及要安装文件系统与其他已安装文件系统之间的关系。这样的信息保存在已安装文件系统描述符中;每个描述符是一个具有vfsmount类型的数据结构。

12.4.3 安装普通文件系统

mount()系统调用被用来安装一个普通文件系统;它的服务例程sys_mount()作用于以下参数:

1、文件系统所在的设备文件的路径名,或者不需要的话就为NULL(例如,当安装的文件系统是基于网络时)

2、文件系统被安装其上的某个目录的目录路径名(安装点)

3、文件系统类型,必须已注册

4、安装标志

5、指向一个与文件系统相关的数据结构的指针。

sys_mount()函数把参数拷贝到临时内核缓冲区,获取大内核锁,并调用do_mount()函数。一旦do_mount()返回,释放大内核锁并释放临时缓冲区。

do_mount()步骤:

1、检查安装标志MS_NOSUID,MS_NODEV或MS_NOEXEC,并在vfsmount对象中设置相应标志

2、调用path_lookup()查找安装点路径名

3、检查安装标志:

  a.如果MS_REMOUNT,do_remount();

  b.否则检查MS_BIND,用户要求在系统目录树的另一个安装点上的文件或目录能够可见

  c. 否则检查MS_MOVE,用户要求改变安装点,do_move_mount()

  d.否则,do_new_mount()->>do_kernel_mount()->>do_add_mount()

    do_add_mount()步骤: 

     1)获得namespace->sem

     2)验证在安装点上最近安装的文件系统是否仍指向当前的namespace,不是释放sem,返回错误码

       3)如果要安装的文件系统已经被安装在由系统调用的参数所指定的安装点上,或该安装点是一个符号链接,则释放sem,返回错误码

     4)初始化有do_kernel_mount()分配的新的安装文件系统对象的mnt_flags字段的标志

       5)调用graft_tree()把新安装的文件系统对象插入到namespace链表、散列表以及父文件系统的子链表中

       6)释放sem,返回。

4、调用path_release()终止安装点路径名查找,并返回0.

 do_kern_mount()函数:

参数:

1、fstype:要安装文件系统类型名

2、flags:安装版标志

3、name:存放文件系统的块设备路径名

4、data:指向传递给文件系统read_super方法的附加数据指针

步骤:

1、get_fs_type()找到对应file_system_type类型的type

2、alloc_vfsmnt()分配vfsmount描述符,并给mnt局部变量赋值

3、type->get_sb(),初始化一个新的超级块

4、用新的超级块给mnt->mnt-sb赋值

5、将mnt->mnt_root字段初始化为与文件系统根目录对应的目录项对象的地址存在于super_block中的s_root字段中,并增加该目录项对象引用计数器值。

6、用mnt中的值初始化mnt->mnt_parent字段

7、用current->namespace初始化mnt->mnt_namespace

8、释放超级块信号量s_unmont

9、返回mnt

分配超级块对象:

get_sb_bdev()分配并初始化一个新的适合于磁盘文件系统的超级块;get_sb_pseudo()针对没有安装点的特殊文件系统如pipefs;get_sb_single()针对具有唯一安装点的特殊文件系统如sysfs;get_sb_nodev()针对可以安装多次的特殊文件系统如tmpfs

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、调用以来文件系统的函数访问磁盘上超级块的信息,并填充新超级块对象的其他字段

6、返回新超级块对象的地址

12.4.4 安装根文件系统

安装根文件系统分为两个阶段:

1、内核安装特殊rootfs文件系统,该文件系统仅提供一个作为初始安装点的空目录。

2、内核在空目录上安装实际的根文件系统。

阶段1:安装rootfs文件系统

第一阶段由init_rootfs()和init_mount_tree()函数完成,他们在系统初始化过程中执行。

init_rootfs()函数注册特殊文件系统类型rootfs:

struct file_system_type rootfs_fs_type={

  .name = "rootfs"

  .get_sb =rootfs_get_sb;

  .kill_sb = kill_litter_super;

};

  register_filesystem(&rootfs_fs_type);

init_mount_tree()步骤:

1、调用do_kern_mount()函数返回vfsmount,调用rootfs_get_sb()方法,rootfs_get_sb()调用get_sb_nodev()方法:

  a.调用sget()函数分配super_block

  b.将flags参数的值拷贝到super_block的s_flags中。

  c.调用ramfs_fill_super()分配inode对象和dentry对象,填充super_block字段值

  d.返回super_block地址

2、为进程0的命名空间分配一个namespace对象,并将它插入到vfsmount中

3、将系统中其他每个进程namespace字段赋值为namespace。

4、将0进程的根目录和当前工作目录设置为根文件系统

阶段2:安装实际根文件系统

prepare_namespace()函数执行:

1、把root_device_name变量值为从启动参数root中获取的设备文件名。把ROOT_DEV变量置为同一设备文件的主设备号和次设备号。

2、mount_root():

  a.sys_mknod()在rootfs文件系统中创建设备文件/dev/root,设备号与ROOT_DEV中相同

  b.分配一个缓冲区并用文件系统类型名链表填充。该链表要么通过启动参数rootfstype传送给内核,要么通过扫描文件系统类型单向链表中的元素建立

  c.扫描链表。对每一项调用sys_mount()试图在根设备上安装给定的文件系统类型。

  d.调用sys_chdir("/root")改变进程的当前目录

3、移动rootfs文件系统根目录上已安装文件系统的安装点。

rootfs特殊文件系统没有被卸载:它只是隐藏在基于磁盘的根文件系统下了。

12.4.5 卸载文件系统

unmount()系统调用用来卸载一个文件系统。相应的sys_unmount()服务例程使用两个参数:文件名(多是安装点目录或是块设备文件名)和一组标志:

1、path_lookup()查找安装点路径名;查询结果存放在nameidata类型的局部变量nd中

2、检查查询的结果是不是文件系统安装点,不是则设置返回码-EINVAL

3、检查要卸载的文件系统是否已安装在命名空间中,不是则设置返回码-EINVAL

4、检查用户是否有权限

5、调用do_unmount(),传递nd.mnt和flags

6、减少相应文件系统根目录的目录项对象和已安装文件系统描述符的引用计数器值;这些计数器由path_lookup()增加。

7.返回retval的值。

12.5 路径名查找

12.6 VFS系统调用的实现

12.6.1 open()系统调用

12.6.2 read()和write()系统调用

12.6.3 close()系统调用

12.7 文件加锁

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2013-05-08 22:36  shuying1234  阅读(463)  评论(0编辑  收藏  举报