深入理解Linux内核--文件系统(阅读笔记)(原创)
由 王宇 原创并发布 :
第十二章虚拟文件系统
虚拟文件系统所隐含的思想是把表示很多不同种类文件系统的共同信息放入内核
1、虚拟文件系统(VFS)的作用
虚拟文件系统(VirtualFilesystem)也可以称之为虚拟文件系统转换(VirtualFilesystemSwitch,VFS),是一个内核软件层,用来处理与Unix标准文件系统相关的所有系统调用,其健壮性表现在能为各种文件系统提供一个通用的接口。 图:12-1
VFS支持的文件系统可以划分为三种主要类型 :
(1)磁盘文件系统
(2)网络文件系统
(3)特殊文件系统
[1]通用文件模型
VFS所隐含的主要思想在于引入了一个通用的文件模型,这个模型能够表示所有支持的文件系统
**通用文件模型对象类型组成:
(1)超级块对象(superblockobject)
存放已安装文件系统的有关信息。对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件系统控制块
(2)索引点对象(inodeobject)
存放关于具体文件的一般信息。对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件控制块。每个索引点对象都有一个索引节点号,这个节点好唯一地标识文件系统中的文件。
(3)文件对象(fileobject)
存放打开文件与进程之间进行交互的有关信息。这类信息仅当进程访问文件期间存在于内核中。
(4)目录项对象(dentryobject)
存放目录项(也就是文件的特殊名称)与文件进行链接的有关信息。每个磁盘文件系统都以自己特有的方式将该类信息存在磁盘上。
图12-2
说明进程怎样与文件进行交互。三个不同进程已经打开同一个文件,其中两个进程使用同一个硬链接。在这种情况下,其中的每个进程都是用自己的文件对象,但只需要两个目录项对象,每个硬链接对应一个目录项对象。这两个目录项对象指向同一个索引节点对象,该索引节点对象标识超级快对象,以及随后的普通磁盘文件。VFS除了能为所有文件系统的实现提供一个通用接口外,还具有另一个与系统性能相关的重要作用。最近最常使用的目录项对象被放在所谓目录项高速缓存的磁盘高速缓存中,以加速从文件路径名到最后一个路径分量的索引节点的转换过程。
[2]VFS所处理的系统调用
表12-1***
VFS是应用程序和具体文件系统之间的一层。不过在某些情况下,一个文件操作可能由VFS本身去执行,无需调用低层函数。例如,当某个金城关闭一个打开的文件时,并不需要涉及磁盘上的相应文件,因此VFS只需释放对应的文件对象。类似地,当系统调用lseek()修改一个文件指针,而这个文件指针是打开文件与进程交互所涉及的一个属性时,VFS就只需修改对应的文件对象,而不必访问磁盘上的文件,因此,无需调用具体文件系统的函数。从某种意义上说,可以把VFS看成"通用"文件系统,它在必要时依赖某种具体文件系统。
2、VFS的数据结构
每个VFS对象都存放在一个适当的数据结构中,其中包含对象的属性和指向对象方法表的指针。
[1]超级块对象:
super_block数据结构,参考表12-2
所有超级块对象都以双向循环链表的形式链接在一起。链表中第一个元素用super_blocks变量 来表示,而超级块对象的s_list字段存放指向链表相邻元素的指针。
s_fs_info字段指向属于具体文件系统的超级快信息,假如超级块对象指的是Ext2文件系统,该字段就指向ext2_sb_info数据结构,该结构包括磁盘分配位掩码和其他与VFS的通用文件模型无关的数据。通常,为了效率起见,由s_fs_info字段所指向的数据被复制到内存。任何基于磁盘的文件系统都需要访问和更改自己的磁盘分配位图,以便分配或释放磁盘块。VFS允许这些文件系统直接对内存超级块的s_fs_info字段进行操作,而无需访问磁盘。
但是,这种方法带来一个新问题:有可能VFS超级块最终不再与磁盘上相应的超级块同步。因此,有必要引入一个s_dirt标志来表示该超级块是否是脏的--那磁盘上的数据是否必须要更新。缺乏同步还会导致产生我们熟悉的一个问题:当一台机器的电源突然断开而用户来不及正常关闭系统时,就会出现文件系统崩溃。我们将会在第十五章的“把脏页写入磁盘”一节中看到,Linux是通过周期性地将所有“脏”的超级块写回磁盘来减少该问题带来的危害。
与超级块关联的方法就是所谓的超级块操作:由数据结构:super_operations来描述,参考p463-465的函数列表。
[2]索引节点对象
文件系统处理文件所需要的所有信息都放在一个名为索引节点的数据结构中,文件名可以随时更改,但是索引节点对文件时唯一的,并且随文件的存在而存在
inode数据结构:参考表12-3
与索引节点对象关联的方法也叫索引节点操作:由inode_operations结构来描述,参考p468-469的函数列表
[3]文件对象
文件对象描述进程怎么与一个打开的文件进行交互。文件对象是在文件被打开时创建的,由一个file结构组成。注意,文件对象在磁盘上没有对应的映像,因此file结构中没有设置"脏"字段来表示文件对象是否已被修改。
存放在文件对象中的主要信息是文件指针,即文件中当前的位置,下一个操作将在该位置发生。由于几个进程可能同时访问同一文件,因此文件指针必须存放在文件对象而不是索引节点对象中。
file数据结构:参考表12-4
file_operations结构:参考p472-474的函数列表
[4]目录项对象
VFS把每个目录看作由若干子目录和文件组成的一个普通文件。然而一旦目录项被读入内存,VFS就把它转换成基于dentry结构(参考表12-5) 的一个目录项对象,对于进程查找的路径名中的每个分量,内核都为其创建一个目录项对象,内核为跟目录“/”创建一个目录项对象,为根目录下的tmp项创建一个第二级目录项对象,为/tmp目录下的test项创建一个第三极目录项对象。
注意,目录项对象在磁盘上并没有对应的映像,因此在pentry结构中不包含指出该对象已被修改的字段。目录项对象存放在名为dentry_cache的slab分配器高速缓存中。因此,目录项对象的创建和删除是通过调用kmem_checha_alloc()和kmem_cahce_free()实现的
每个目录项对象可以处于以下四种状态之一:
(1)空闲状态
(2)未使用状态
(3)正在使用状态
(4)负状态
dentry_operation结构:参考p476的函数列表
[5]目录项高速缓存
由于从磁盘读入一个目录项并构造相应的目录项对象需要花费大量的时间,所以,在完成对目录项对象的操作后,可能后面还要使用它,因此仍在内存中保留它有重要的意义
为了最大限度地提高处理这些目录项对象的效率,Linux使用目录项高速缓存,它有两种类型的数据结构组成:
(1)一个处理正在使用、未使用或负状态的目录项对象的集合
(2)一个散列表,从中能够快速获取与给定的文件名和目录名对应的目录项对象。同样,如果访问的对象不在目录项高速缓存中,则散列函数返回一个空值。
[6]与进程相关的文件
每个进程都有它自己当前的工作目录和它自己的根目录。这仅仅是内核用来表示进程与文件系统相互作用所必须维护的数据中的两个例子。类型为fs_stuct的整个数据结构就是用于此目的。参考表12-6p478
files_struct结构表示进程当前打开的文件。参考表12-7p478-479
3、文件系统类型
文件系统注册--也就是通常在系统初始化期间并且在使用文件系统类型之前必须执行的基本操作。一旦文件系统被注册,其特定的函数对内核就是可用的,因此文件系统类型可以安装在系统的目录树上
[1]特殊文件系统
当网络和磁盘文件系统能够使用户处理存放在内核之外的信息时,特殊文件系统可以为系统程序员和管理员提供一种容易的方式来操作内核的数据结构并实现操作系统的特殊特征
特殊文件系统不限于物理块设备。然而,内核给每个安装的特殊文件系统分配一个虚拟的块设备,让其主设备号为0而次设备号具有任意值
[2]文件系统类型注册
VFS必须对代码目前已在内核中的所有文件系统的类型进行跟踪。这就是通过进行文件系统类型注册来实现的
每个注册的文件系统都用一个类型为file_system_type的对象来表示,参考表12-9
所有文件系统类型的对象都插入到一个单向链表中
4、文件系统处理
Linux也使用系统的根文件系统(system'srootfilesystem):它由内核在引导阶段直接安装,并拥有系统初始化脚本以及最基本的系统程序。
其他文件系统要么由初始化脚本安装,要么由用户直接安装在已安装文件系的目录上。作为一个目录树,每个文件系统都拥有自己的根目录(rootdirectory)。安装文件系统的这个目录称之为安装点(mountpoint)。已安装文件系统属于安装点目录的一个子文件系统。
[1]命名空间
在传统的Unix系统中,只有一个已安装文件系统树:从系统的根文件系统开始,每个进程通过制定合适的路径名可以访问已安装文件系统中的任何文件。从这个方面考虑,Linux2.6更加的精确:每个进程可拥有自己的已安装文件系统树---叫做进程的命名空间(namespace)
通常大多数进程共享一个命名空间,即位于系统的根文件系统且被init进程使用的已安装文件系统树
单进程安装或卸载一个文件系统时,仅修改它的命名空间。因此,所做的修改对共享统一命名空间的所有进程都是可见的,并且也只对它们可见。进程甚至可通过使用Linux特有的pivot_root()系统调用来改变它的命名空间的根文件系统。
namespace结构:参考表12-11
[2]文件系统安装
大多数传统的类Unix内核中,每个文件系统只能安装一次。然而,Linux有所不同:同一个文件系统被安装多次是可能的。不管一个文件系统被安装了多少次,都仅有一个超级块对象。
每个描述符是一个具有vfsmount类型的数据结构:参考表12-12p485
下列函数处理已安装文件系统描述符:
alloc_vfsmnt(name):分配和初始化一个已安装文件系统描述符
free_vfsmnt(mnt):释放由mnt指向的已安装文件系统描述符
lookup_mnt(mnt,dentry)在散列表中查找一个描述符并返回它的地址
[3]安装普通文件系统
mount()系统调用被用来安装一个普通文件系统;
do_mount()函数操作过程:参考p488
[4]分配超级快对象
get_sb_bdev()操作过程:参考P490-p491
[5]安装根文件系统
安装根文件系统是系统初始化的关键部分
(1)阶段1:安装rootfs文件系统
由init_rootfs()和init_mount_tree()函数完成,这两个函数的操作过程:参考p492-493
(2)阶段2:安装实际根文件系统
prepare_namespace()操作过程:参考p493
[6]卸载文件系统
umount()操作过程:参考p494
5、路径名查找
VFS如何实现路径名查找,也就是说如何从文件路径名导出相应的索引节点。
执行这一任务的标准过程就是分析路径名并把它拆分成一个文件名序列。除了最后一个文件名以外,所有的文件名都必定是目录
如果路径名的第一个字符是“/”,那么这个路径名是绝对路径,因此从current->fs->root(进程的根目录)所标识的目录开始搜索。否则,路径名是相对路径,因此从current->fs->pwd(进程的当前目录)所标识的开始搜索。
在对初始化目录的索引节点进行处理的过程中,代码要检查与第一个名字匹配的目录项,以获得相应的索引节点。然后,从磁盘读出包含那个索引节点的目录文件,并检查与第二个名字匹配的目录项,以获得相应的索引节点。对于包含在路径中的每个名字,这个过程反复执行。
目录项高速缓存极大地加速了这一过程,因为它把最近最常使用的目录项对象保留在内存中。正如我们以前看到的,每个这样的对象是特定目录中的一个文件名与它相应的索引节点相联系。因此在很多情况下,路径名的分析可以避免从磁盘读取中间目录。
但是,事情并不像看起来那么简单,因为必须考虑如下的UNIX和VFS文件系统的特点:
对每个目录的访问权必须进行检查,以验证是否允许进程读取这一目录的内容。
文件名可能是任意一个路径名对应的符号链接;在这种情况下,分析必须扩展到那个路径名的所有分量
符号链接可能导致循环引用;内核必须考虑这个可能性,并能在出现这种情况时将循环终止
文件名可能是一个已安装文件系统的安装点。这种情况必须检测到,这样,查找操作必须延伸到新的文件系统
路径名查找应该在发出系统调用的进程的命名空间中完成。由具有不同命名空间的两个进程使用的相同路径名,可能制定了不同的文件。
path_lookup()函数步骤:参考:p497
[1]标准路径名查找
link_path_walk()函数步骤:参考p498-502
[2]父路径名查找
在很多情况下,查找操作的真正目的并不是路径名的最后一个分量的前一个分量。例如,当文件被创建时,最后一个分量表示还不存在的文件的文件名,而路径名中的其余路径指定新链接必须插入的目录。因此,查找操作应当取回最后分量的前一个分量的目录项对象。
当查找操作必须解析的是包含路径名最后一个分量的目录而不是最后一个分量本身时,使用LOOKUP_PARENT标志
[3]符号链接的查找
符号链接是一个普通文件,其中存放的是另一个文件的路径名。路径名可以包含符号链接,且必须由内核来解析。
当前进程的描述符中的link_count字段用来避免一个符号链接指向自己而导致的无休止的递归,每次递归执行前增加这个字段的值,执行之后减少其值。如果该字段的值达到6,整个循环操作就以错误码结束。因此符号链接嵌套的层数不超过5
一旦link_path_walk()函数检索到与路径名分量相关的目录项对象,就检查相应的索引节点是否自定义的follow_link方法。如果是,索引节点就是一个符号链接,在原路径名的查找操作进行之前就必须先对这个符号链接进行解释。
do_follow_link()函数步骤:参考p504
6、VFS系统调用的实现
[1]Open()系统调用
open()系统调用的服务例程为sys_open函数,该函数接收的参数为:要打开文件的路径名filename、访问模式的一些标志flags,以及如何该文件被创建所需要的许可权限位掩码mode.如果该系统调用成功,就返回一个文件描述符,也就是指向文件对象的指针数组current->file->fd中分配给新文件的索引:否则,返回-1参考表12-8(open()系统调用的标志)
sys_open()函数的操作:参考:p506-507
[2]read()和write()系统调用
sys_read()和sys_write()步骤:参考:p508-509
[3]close()系统调用
操作:参考p509
7、文件加锁
当一个文件可以被多个进程访问时,就会出现同步问题。
Unix系统提供了一种允许进程对一个文件区进行加锁的机制,以使同时访问可以很容易地被避免。
POSIX标准规定了基于fcntl()系统调用的文件加锁机制。这样就有可能对文件的任意一部分(甚至一个字节)加锁或对整个文件(包含以后要追加的数据)加锁。因为进程可以选择仅仅对文件的一部分加锁,因此,它也可以在文件的不同部分保持对个锁。
这种锁并不把不知道加锁的其他进程关在外面。与用于保护代码中临界区的信号类似,可以认为这种锁起“劝告”的作用。因此,POSIX的锁被称为劝告锁(advisorylock);内核只是提供加减锁以及检测是否加锁的操作,但是不提供锁的控制与协调工作。也就是说,如果应用程序对某个文件进行操作时,没有检测是否加锁或者无视加锁而直接向文件写数据,内核是不会加以阻拦控制的,因此,劝告锁,不能阻止进程对文件的操作。而只能依赖于大家自觉的去检测是否加锁然后约束自己的行为,若是不遵守规则也不会报错。
传统的BSD变体通过flock()系统调用来实现劝告锁。这个调用不允许进程对文件的一个区字段进行加锁,而只能对整个文件进行加锁。传统的SystemV变体提供了lockf()库函数,它仅仅是fcntl()的一个接口。
更重要的是,SystemVRelease3引入了强制加锁(mandatorylocking):内核检查open()、read()和write()系统调用的每次调用都不违背在所访问文件上的强制锁。因此,强制锁甚至在非合作的进程之间也被强制加上
不管进程是使用劝告锁还是强制锁,它们都可以使用共享读锁和独占写锁。在文件的某个区字段上,可以有任意多个进程进行读,但在同一时刻只能有一个进程进行写。此外,当其他进程对同一个文件都拥有自己的读锁时,就不可以获得一个写锁。
[1]Linux文件加锁
Linux支持所有的文件加锁方式:劝告锁和强制锁,以及fcntl()、flock()和lockf()系统调用
flock()系统调用不管MS_MANDLOCK安装标志如何设置,只产生劝告锁。这是任何类Unix操作系统所期望的系统调用行为。在Linu中,增加了一种特殊的flock()强制锁,以允许对专有的网络文件系统的实现提供适当的支持。这就是所谓的共享模式强制锁;当这个锁被设置时,其他任何进程都不能打开与锁访问模式冲突的文件。不鼓励本地Unix应用程序中使用这个特征,因为这样加锁的源代码是不可移植的
在linux中还引入了另一种基于fcntl()的强制锁,叫做租借锁 (lease).当一个进程试图打开由租借锁保护的文件时,它照样被阻塞。然而,拥有锁得进程接收到一个信号。一旦该进程得到通知,它应当首先更新文件,以使文件的内容保持一致,然后释放锁。如果拥有者不在预定的时间间隔内这么做,则租借锁由内核自动删除,且允许阻塞的进程继续执行
进程可以采用以下两种方式获得或释放一个文件劝告锁:
(1)发出flock()系统调用。 传递给它的两个参数为文件描述符fd和指定锁操作的命令。该锁应用于整个文件。
(2)使用fcntl()系统调用。 传递给它的三个参数为文件描述符fd、指定锁操作的命令以及指向flock结构的指针。flock结构中的几个字段允许进程指定要加锁得文件部分。因此进程可以在同一文件的不同部分保持几个锁。
fcntl()和flock()系统调用可以在同一文件上同时使用,但是通过fcntl()加锁的文件看起来与通过flock()加锁的文件不一样,反之亦然。这样当应用程序使用一种依赖于某个库的锁,而该库同时使用另一种类型的锁时,可以避免发生死锁。
处理强制文件锁要更复杂些。步骤如下:
(1)安装文件系统时强制锁时是必需的,可使用mount命令的-omand选项在mount()系统调用中设置MS_MANDLOCK标志。缺省操作是不使用强制锁。
(2)通过设置文件的set-group位(SGID)和清除group-execute许可权位将它们标记为强制锁得候选者。因此当group-execute位为0时,set-group位页没有任何意义,因此内核将这种合并解释成使用强制锁而不是劝告锁
(3)使用fcntl()系统调用获得或释放一个文件锁。
处理租借锁比处理强制锁要容易得多:调用具有F_SETLEASE或F_GETLEASE命令的系统调用fcntl()就足够了。使用另一个带有F_SETSIG命令的fcntl()系统调用可以改变传送给租借锁进程拥有者的信号类型。
[2]文件锁得数据结构
file_lock数据结构:参考表12-19
[3]FL_FLOCK锁
FL_LOCK锁总是与一个文件对象相关联,因此有一个打开该文件的进程(或共享同一个打开文件的子进程)来维护。当一个锁被请求或允许时,内核就把进程保持在同一文件对象上的任何其他锁都替换掉。这只发生在进程想把一个已经拥有的读锁改变为一个写锁,或把一个写锁改变为一个读锁时。
sys_flock()步骤:参考P513-514
flock_lock_file_wait()函数操作:参考P514
[4]FL_POSIX锁
FL_POSIX锁总是与一个进程和一个索引节点相关联。当进程死亡或一个文件描述符被关闭时(即使该进程对同一文件打开了两次或复制了一个文件描述符),这种锁会被自动地释放。此外,FL_POSIX锁绝不会被子进程通过fork()继承
fcntl_setlk()函数的操作:参考p516
第十六章访问文件