本章描述Linux如何维护它支持的文件系统中的文件。描述了虚拟文件系统(Virtual File System VFS)并解释了Linux核心中真实的文件系统如何被支持
Linux的一个最重要的特点之一使它可以支持许多不同的文件系统。这让它非常灵活,可以和许多其他操作系统共存。在写作本章的时候,Linux可一直支持15种文件系统:ext、ext2、xia、minix、umsdos、msdos、vfat、proc、smb、ncp、iso9660、sysv、hpfs、affs和ufs,而且不容置疑,随着时间流逝,会加入更多的文件系统。
在Linux中,象Unix一样,系统可以使用的不同的文件系统不是通过设备标识符(例如驱动器编号或设备名称)访问,而是连接成一个单一的树型的结构,用一个统一的单个实体表示文件系统。Linux在文件系统安装的时候把它加到这个单一的文件系统树上。所有的文件系统,不管什么类型,都安装在一个目录,安装的文件系统的文件掩盖了这个目录原来存在的内容。这个目录叫做安装目录或安装点。当这个文件系统卸载的时候,安装目录自己的文件又可以显现出来。
当磁盘初始化的时候(比如用fdisk),利用一个分区结构把物理磁盘划分成一组逻辑分区。每一个分区可以放一个文件系统,例如一个EXT2文件系统。文件系统在物理设备的块上通过目录、软链接等把文件组织成逻辑的树型结构。可以包括文件系统的设备是块设备。系统中的第一个IDE磁盘驱动器的第一个分区,IDE磁盘分区/dev/hda1,是一个块设备。Linux文件系统把这些块设备看成简单的线性的块的组合,不知道也不去关心底层的物理磁盘的尺寸。把对设备的特定的块的读的请求映射到对于设备有意义的术语:这个块保存在硬盘上的磁道、扇区和柱面,这是每一个块设备驱动程序的任务。一个文件系统不管它保存在什么设备上,都应该用同样的方式工作,有同样的观感。另外,使用Linux的文件系统,是否这些不同的文件系统在不同的硬件控制器的控制下的不同的物理介质上都是无关紧要的(至少对于系统用户是这样)。文件系统甚至可能不在本地系统上,它可能是通过网络连接远程安装的。考虑以下的例子,一个Linux系统的根文件系统在一个SCSI磁盘上。
A E boot etc lib opt tmp usr
C F cdrom fd proc root var sbin
D bin dev home mnt lost+found
不管是操作这些文件的用户还是程序都不需要知道/C实际上是在系统的第一个IDE磁盘上的一个安装的VFAT文件系统。本例中(实际是我家中的Linux系统),/E是次IDE控制器上的master IDE磁盘。第一个IDE控制器是PCI控制器,而第二个是ISA控制器,也控制着IDE CDROM,这些也都无关紧要。我可以用一个modem和PPP网络协议拨号到我工作的网络,这时,我可以远程安装我的Alpha AXP Linux系统的文件系统到/mnt/remote。
文件系统中的文件包含了数据的集合:包含本章源的文件是一个ASCII文件,叫做filesystems.tex。一个文件系统不仅保存它包括的文件的数据,也保存文件系统的结构。它保存了Linux用户和进程看到的所有的信息,例如文件、目录、软链接、文件保护信息等等。另外,它必须安全地保存这些信息,操作系统的基本的一致性依赖于它的文件系统。没有人可以使用一个随机丢失数据和文件的操作系统(不知道是否有,虽然我曾经被拥有的律师比Linux开发者还多的操作系统伤害过)。
Minix是Linux的第一个文件系统,有相当的局限,性能比较差。它的文件名不能长于14个字符(这仍然比8.3文件名要好),最大的文集大小是64M字节。第一眼看去,64M字节好像足够大,但是设置中等的数据库需要更大的文件大小。第一个专为Linux设计的文件系统,扩展文件系统或EXT(Extend File System),在1992年4月引入,解决了许多问题,但是仍然感到性能低。所以,1993年,增加了扩展文件系统第二版,或EXT2。这种文件系统在本章稍后详细描述。
当EXT文件系统增加到Linux的时候进行了一个重要的开发。真实的文件系统通过一个接口层从操作系统和系统服务中分离出来,这个接口叫做虚拟文件系统或VFS。VFS允许Linux支持许多(通常是不同的)文件系统,每一个都向VFS表现一个通用的软件接口。Linux文件系统的所有细节都通过软件进行转换,所以所有的文件系统对于Linux核心的其余部分和系统中运行的程序显得一样。Linux的虚拟文件系统层允许你同时透明地安装许多不同的文件系统。
Linux虚拟文件系统的实现使得对于它的文件的访问尽可能的快速和有效。它也必须保证文件和文件数据正确地存放。这两个要求相互可能不平等。Linux VFS在安装和使用每一个文件系统的时候都在内存中高速缓存信息。在文件和目录创建、写和删除的时候这些高速缓存的数据被改动,必须非常小心才能正确地更新文件系统。如果你能看到运行的核心中的文件系统的数据结构,你就能够看到文件系统读写数据块,描述正在访问的文件和目录的数据结构会被创建和破坏,同时设备驱动程序会不停地运转,获取和保存数据。这些高速缓存中最重要的是Buffer Cache,在文件系统访问它们底层的块设备的时候结合进来。当块被访问的时候它们被放到Buffer Cache,根据它们的状态放在不同的队列中。Buffer Cache不仅缓存数据缓冲区,它也帮助管理块设备驱动程序的异步接口。
9.1 The Second Extended File System (EXT2)
EXT2被发明(Remy Card)作为Linux一个可扩展和强大的文件系统。它至少在Linux社区中是最成功的文件系统,是所有当前的Linux发布版的基础。EXT2文件系统,象所有多数文件系统一样,建立在文件的数据存放在数据块中的前提下。这些数据块都是相同长度,虽然不同的EXT2文件系统的块长度可以不同,但是对于一个特定的EXT2文件系统,它的块长度在创建的时候就确定了(使用mke2fs)。每一个文件的长度都按照块取整。如果块大小是1024字节,一个1025字节的文件会占用两个1024字节的块。不幸的是这一意味着平均你每一个文件浪费半个块。通常计算中你会用磁盘利用来交换CPU对于内存的使用,这种情况下,Linux象大多数操作系统一样,为了较少CPU的负载,使用相对低效率的磁盘利用率来交换。不是文件系统中所有的块都包含数据,一些块必须用于放置描述文件系统结构的信息。EXT2用一个inode数据结构描述系统中的每一个文件,定义了系统的拓扑结构。一个inode描述了一个文件中的数据占用了哪些块以及文件的访问权限、文件的修改时间和文件的类型。EXT2文件系统中的每一个文件都用一个inode描述,而每一个inode都用一个独一无二的数字标识。文件系统的inode都放在一起,在inode表中。EXT2的目录是简单的特殊文件(它们也使用inode描述),包括它们目录条目的inode的指针。
图9.1显示了一个EXT2文件系统占用了一个块结构的设备上一系列的块。只要提到文件系统,块设备都可以看作一系列能够读写的块。文件系统不需要关心自身要放在物理介质的哪一个块上,这是设备驱动程序的工作。当一个文件系统需要从包括它的块设备上读取信息或数据的时候,它请求对它支撑的设备驱动程序读取整数数目的块。EXT2文件系统把它占用的逻辑分区划分成块组(Block Group)。每一个组除了当作信息和数据块来存放真实的文件和目录之外,还复制对于文件系统一致性至关重要的信息。这种复制的信息对于发生灾难,文件系统需要恢复的时候是必要的。下面对于每一个块组的内容进行了详细的描述。
9.1.1 The EXT2 Inode(EXT2 I节点)
在EXT2文件系统中,I节点是建设的基石:文件系统中的每一个文件和目录都用一个且只用一个inode描述。每一个块组的EXT2的inode都放在inode表中,还有一个位图,让系统跟踪分配和未分配的I节点。图9.2显示了一个EXT2 inode的格式,在其他信息中,它包括一些域:
参见include/linux/ext2_fs_i.h
mode 包括两组信息:这个inode描述了什么和用户对于它的权限。对于EXT2,一个inode可以描述一个文件、目录、符号链接、块设备、字符设备或FIFO。
Owner Information 这个文件或目录的数据的用户和组标识符。这允许文件系统正确地进行文件访问权限控制
Size 文件的大小(字节)
Timestamps 这个inode创建的时间和它上次被修改的时间。
Datablocks 指向这个inode描述的数据的块的指针。最初的12个是指向这个inode描述的数据的物理块,最后的3个指针包括更多级的间接的数据块。例如,两级的间接块指针指向一个指向数据块的块指针的块指针。的这意味着小于或等于12数据块大小的文件比更大的文件的访问更快。
你应该注意EXT2 inode可以描述特殊设备文件。这些不是真正的文件,程序可以用于访问设备。/dev下所有的设备文件都是为了允许程序访问Linux的设备。例如mount程序用它希望安装的设备文件作为参数。
9.1.2 The EXT2 Superblock(EXT2超级块)
超级块包括这个文件系统基本大小和形状的描述。它里面的信息允许文件系统管理程序用于维护文件系统。通常文件系统安装时只有块组0中的超级块被读取,但是每一个块组中都包含一个复制的拷贝,用于系统崩溃的时候。除了其他一些信息,它包括:
参见include/linux/ext2_fs_sb.h
Magic Number 允许安装软件检查这是否是一个EXT2文件系统的超级块。对于当前版本的EXT2是0xEF53。
Revision Level major和minor修订级别允许安装代码确定这个文件系统是否支持只有在这种文件系统特定修订下才有的特性。这也是特性兼容域,帮助安装代码确定哪些新的特征可以安全地使用在这个文件系统上。
Mount Count and Maximum Mount Count 这些一起允许系统确定这个文件系统是否需要完全检查。每一次文件系统安装的时候mount count增加,当它等于maximum mount count的时候,会显示告警信息“maximal mount count reached,running e2fsck is recommended”。
Block Group Number 存放这个超级块拷贝的块组编号。
Block Size 这个文件系统的块的字节大小,例如1024字节。
Blocks per Group 组中的块数目。象块大小一样,这是文件系统创建的时候确定的。
Free Blocks 文件系统中空闲块的数目
Free Inodes 文件系统中空闲的inode
First Inode 这是系统中第一个inode的编号。一个EXT2根文件系统中的第一个inode是‘/’目录的目录条目
9.1.3 The EXT2 Group Descriptor(EXT2组描述符)
每一个块组都有一个数据结构描述。象超级块,所有得亏组的组描述符在每一块组都进行复制。每一个组描述符包括以下信息:
参见include/linux/ext2_fs.h ext2_group_desc
Blocks Bitmap 这个块组的块分配位图的块编号,用在块的分配和回收过程中
Inode Bitmap 这个块组的inode位图的块编号。用在inode的分配和回收过程中。
Inode Table 这个块组的inode table的起始块的块编号。每一个EXT2 inode数据结构表示的inode在下面描述
Free blocks count,Free Inodes count,Used directory count
组描述符依次排列,它们一起组成了组描述符表(group descriptor table)。每一个块组包括块组描述符表和它的超级块的完整拷贝。只有第一个拷贝(在块组0)实际被EXT2文件系统使用。其他拷贝,象超级块的其他拷贝一样,只有在主拷贝损坏的时候才使用。
9.1.4 EXT2 Directories(EXT2目录)
在EXT2文件系统中,目录是特殊文件,用来创建和存放对于文件系统中的文件的访问路径。图9.3显示了内存中一个目录条目的布局。一个目录文件,是一个目录条目的列表,每一个目录条目包括以下信息:
参见include/linux/ext2_fs.h ext2_dir_entry
inode 这个目录条目的inode。这是个放在块组的inode表中的inode数组的索引。图9.3叫做file的文件的目录条目引用的inode 是 i1。
Name length 这个目录条目的字节长度
Name 这个目录条目的名字
每一个目录中的前两个条目总是标准的“.”和“..”,分别表示“本目录”和“父目录”。
9.1.5 Finding a File in a EXT2 File System(在一个EXT2文件系统中查找一个文件)
Linux的文件名和所有的Unix文件名的格式一样。它是一系列目录名,用“/”分隔,以文件名结尾。一个文件名称的例子是/home/rusling/.cshrc,其中/home和/rusling是目录名,文件名是.cshrc。象其它Unix系统一样,Linux不关心文件名本身的格式:它可以任意长度,由可打印字符组成。为了在EXT2文件系统中找到代表这个文件的inode,系统必须逐个解析目录中的文件名直到得到这个文件。
我们需要的第一个inode是这个文件系统的根的inode。我们通过文件系统的超级块找到它的编号。为了读取一个EXT2 inode我们必须在适当的块组中的inode表中查找。举例,如果根的inode编号是42,那么我们需要块组0中的inode表中的第42个inode。Root inode是一个EXT2目录,换句话说root inode的模式描述它是一个目录,它的数据块包括EXT2目录条目。
Home是这些目录条目之一,这个目录条目给了我们描述/home目录的inode编号。我们必须读取这个目录(首先读取它的inode,然后读取从这个inode描述的数据块读取目录条目),查找rusling条目,给出描述/home/rusling目录的inode编号。最后,我们读取描述/home/rusling目录的inode指向的目录条目,找到.cshrc文件的inode编号,这样,我们得到了包括文件里信息的数据块。
9.1.6 Changing the size of a File in an EXT2 File System(在EXT2文件系统中改变一个文件的大小)
文件系统的一个常见问题是它趋于更多碎片。包含文件数据的块分布在整个文件系统,数据块越分散,对于文件数据块的顺序访问越没有效率。EXT2文件系统试图克服这种情况,它分配给一个文件的新块物理上和它的当前数据块接近或者至少和它的当前数据块在一个块组里面。只有这个失败了它才分配其它块组中的数据块。
无论何时一个进程试图象一个文件写入数据,Linux文件系统检查数据是否会超出文件最后分配块的结尾。如果是,它必须为这个文件分配一个新的数据块。直到这个分配完成,该进程无法运行,它必须等待文件系统分配新的数据块并把剩下的数据写入,然后才能继续。EXT2块分配例程所要做的第一个事情是锁定这个文件系统的EXT2超级块。分配和释放块需要改变超级块中的域,Linux文件系统不能允许多于一个进程同一时间都进行改变。如果另一个进程需要分配更多的数据块,它必须等待,直到这个进程完成。等待超级块的进程被挂起,不能运行,直到超级块的控制权被它的当前用户释放。对于超级块的访问的授权基于一个先来先服务的基础(first come first serve),一旦一个进程拥有了超级块的控制,它一直维持控制权到它完成。锁定超级块之后,进程检查文件系统是否有足够的空闲块。如果没有足够的空闲块,分配更多的尝试会失败,进程交出这个文件系统超级块的控制权。
如果文件系统中有足够的空闲块,进程会试图分配一块。如果这个EXT2文件系统已经建立了预分配的数据块,我们就可以取用。预分配的块实际上并不存在,它们只是分配块的位图中的保留块。VFS inode用两个EXT2特有的域表示我们试图分配新数据块的文件:prealloc_block and prealloc_count,分别是预分配块中第一块的编号和预分配块的数目。如果没有预分配块或者预分配被禁止,EXT2文件系统必须分配一个新的数据块。EXT2文件系统首先查看文件最后一个数据块之后数据块是否空闲。逻辑上,这是可分配的效率最高的块,因为可以让顺序访问更快。如果这个块不是空闲,继续查找,在随后的64块中找理想的数据块。这个块,虽然不是最理想,但是至少和文件的其它数据块相当接近,在一个块组中。
参见fs/ext2/balloc.c ext2_new_block()
如果这些块都没有空闲的,进程开始顺序查看所有其它块组直到它找到空闲的块。块分配代码在这些块组中查找8个空闲数据块的簇。如果无法一次找到8个,它会降低要求。如果希望进行块预分配,并允许,它会相应地更新prealloc_block和prealloc_count。
不管在哪里找到了空闲的数据块,块分配代码会更新块组的块位图,并从buffer cache中分配一个数据缓冲区。这个数据缓冲区使用支撑文件系统的设备标识符和分配块的块编号来唯一标识。缓冲区中的数据被置为0,缓冲区标记为“dirty”表示它的内容还没有写到物理磁盘上。最后,超级块本身也标记位“dirty”,显示它进行了改动,然后它的锁被释放。如果有进程在等待超级块,那么队列中第一个进程就允许运行,得到超级块的排它控制权,进行它的文件操作。进程的数据写到新的数据块,如果数据块填满,整个过程重复进行,再分配其它数据块
9.2 The Virtual File System(虚拟文件系统VFS)
图9.4显示了Linux核心的虚拟文件系统和它的真实的文件系统之间的关系。虚拟文件系统必须管理任何时间安装的所有的不同的文件系统。为此它管理描述整个文件系统(虚拟)和各个真实的、安装的文件系统的数据结构。
相当混乱的是,VFS也使用术语超级块和inode来描述系统的文件,和EXT2文件系统使用的超级块和inode的方式非常相似。象EXT2的inode,VFS的inode描述系统中的文件和目录:虚拟文件系统的内容和拓扑结构。从现在开始,为了避免混淆,我会用VFS inode和VFS超级块以便同EXT2的inode和超级块区分开来。
参见fs/*
当每一个文件系统初始化的时候,它自身向VFS登记。这发生在系统启动操作系统初始化自身的时候。真实的文件系统自身建立在内核中或者是作为可加载的模块。文件系统模块在系统需要的时候加载,所以,如果VFAT文件系统用核心模块的方式实现,那么它只有在一个VFAT文件系统安装的时候才加载。当一个块设备文件系统安装的时候,(包括root文件系统),VFS必须读取它的超级块。每一个文件系统类型的超级块的读取例程必须找出这个文件系统的拓扑结构,并把这些信息映射到一个VFS超级块的数据结构上。VFS保存系统中安装的文件系统的列表和它们的VFS超级块列表。每一个VFS超级块包括了文件系统的信息和完成特定功能的例程的指针。例如,表示一个安装的EXT2文件系统的超级块包括一个EXT2相关的inode的读取例程的指针。这个EXT2 inode读取例程,象所有的和文件系统相关的inode读取例程一样,填充VFS inode的域。每一个VFS超级块包括文件系统中的一个VFS inode的指针。对于root文件系统,这是表示“/”目录的inode。这种信息映射对于EXT2文件系统相当高效,但是对于其他文件系统相对效率较低。
当系统的进程访问目录和文件的时候,调用系统例程,游历系统中的VFS inode。例如再一个目录中输入ls或者cat一个文件,让VFS查找代表这个文件系统的VFS inode。映为系统中的每一个文件和目录都用一个VFS inode代表,所以一些inode会被重复访问。这些inode保存在inode cache,这让对它们的访问更快。如果一个inode不在inode cache中,那么必须调用一个和文件系统相关的例程来读取适当的inode。读取这个inode的动作让它被放到了inode cache,以后对这个inode的访问会让它保留在cache中。较少使用的VFS inode 会从这个高速缓存中删除。
参见fs/inode.c
所有的Linux文件系统使用一个共同的buffer cache来缓存底层的设备的数据缓冲区,这样可以加速对于存放文件系统的物理设备的访问,从而加快对文件系统的访问。这个buffer cache独立于文件系统,集成在Linux核心分配、读和写数据缓冲区的机制中。让Linux文件系统独立于底层的介质和支撑的设备驱动程序有特殊的好处。所有的块结构的设备向Linux核心登记,并表现为一个统一的,以块为基础的,通常是异步的接口。甚至相对复杂的块设备比如SCSI设备也是这样。当真实的文件系统从底层的物理磁盘读取数据的,引起块设备驱动程序从它们控制的设备上读取物理块。在这个块设备接口中集成了buffer cache。当文件系统读取了块的时候,它们被存放到了所有的文件系统和Linux核心共享的全局的buffer cache中。其中的buffer(缓冲区)用它们的块编号和被读取设备的一个唯一的标识符来标记。所以,如果相同的数据经常需要,它会从buffer cache中读取,而不是从磁盘读取(会花费更多时间)。一些设备支持超前读(read ahead),数据块会预先读取,以备以后可能的读取。
参见fs/buffer.c
VFS也保存了一个目录查找的缓存,所以一个常用的目录的inode可以快速找到。作为一个试验,试着对于你最近没有列表的目录列表。第一次你列表的时候,你会注意到短暂的停顿,当时第二次你列表的时候,结果会立即出来。目录缓存本身不存储目录里的inode,这是inode cache负责的,目录缓存只是存储目录项目全称和它们的inode编号。
参见fs/dcache.c
9.2.1 The VFS Superblock(VFS超级块)
每一个安装的文件系统都用VFS超级块表示。除了其它信息,VFS超级块包括:
参见include/linux/fs.h
Device 这是包含文件系统的块设备的设备标识符。例如,/dev/hda1,系统中的第一个IDE磁盘,设备标识符是0x301
Inode pointers 其中的mounted inode指针指向该文件系统的第一个inode。Covered inode指针指向文件系统安装到的目录的inode。对于root文件系统,它的VFS超级块中没有covered指针。
Blocksize 文件系统块的字节大小,例如1024字节。
Superblock operations 指向一组本文件系统超级块例程的指针。除了其他类型之外,VFS使用这些例程读写inode和超级块
File System type 指向这个安装的文件系统的file_system_type数据结构的一个指针
File System Specific 指向这个文件系统需要的信息的一个指针
9.2.2 The VFS Inode
象EXT2文件系统,VFS中每一个文件、目录等等都用一个且只用一个VFS inode代表。每一个VFS inode中的信息使用文件系统相关的例程从底层的文件系统中获取。VFS inode只在核心的内存中存在,只要对系统有用,就一直保存在VFS inode cache中。除了其它信息,VFS inode包括一些域:
参见include/linux/fs.h
device 存放这个文件(或这个VFS inode代表的其它实体)的设备的设备标识符。
Inode nunber 这个inode的编号,在这个文件系统中唯一。Device 和inode number的组合在整个虚拟文件系统中是唯一的。
Mode 象EXT2一样,这个域描述这个VFS inode代表的东西和对它的访问权限。
User ids 属主标识符
Times 创建、修改和写的时间
Block size 这个文件的块的字节大小,例如1024字节
Inode operations 指向一组例程地址的指针。这些例程和文件系统相关,执行对于这个inode的操作,例如truncate这个inode代表的文件
Count 系统组件当前使用这个VFS inode的数目。Count 0意味着这个inode是空闲,可以废弃或者重用。
Lock 这个域用于锁定这个VFS inode。例如当从文件系统读取它的时候
Dirty 显示这个VFS inode是否被写过,如果这样,底层的文件系统需要更新。
File system specific information
9.2.3 Registering the File Systems(登记文件系统)
当你建立Linux核心的时候,你会被提问是否需要每一个支持的文件系统。当核心建立的时候,文件系统初始化代码包括对于所有内建的文件系统的初始化例程的调用。Linux文件系统也可以建立成为模块,这种情况下,它们可以在需要的时候加载或者手工使用insmod加载。当家在一个文件系统模块的时候,它自身向核心登记,当卸载的时候,它就注销。每一个文件系统的初始化例程都向虚拟文件系统注册自身,并用一个file_system_type数据结构代表,这里面包括文件系统的名称和一个指向它的VFS超级块的读取例程的指针。图9.5显示file_system_type数据结构被放到了由file_systems指针指向的一个列表中。每一个file_system_type数据结构包括以下信息:
参见fs/filesystems.c sys_setup()
参见 include/linux/fs.h file_system_type
Superblock read routine 在这个文件系统的一个实例安装的时候,由VFS调用这个例程
File Systme name 文件系统的名称,例如ext2
Device needed 是否这个文件系统需要一个设备支持?并非所有的文件系统需要一个设备来存放。例如/proc文件系统,不需要一个块设备
你可以检查/proc/filesystems来查看登记了哪些文件系统,例如:
ext2
nodev proc
iso9660
9.2.4 Mounting a File System(安装一个文件系统)
当超级用户试图安装一个文件系统的时候,Linux核心必须首先验证系统调用中传递的参数。虽然mount可以执行一些基本的检查,但是它不知道这个核心建立是可以支持的文件系统或者提议的安装点是否存在。考虑以下的mount 命令:
$ mount –t iso9660 –o ro /dev/cdrom /mnt/cdrom
这个mount命令会传递给核心三部分信息:文件系统的名称、包括这个文件系统的物理块设备和这个新的文件系统要安装在现存的文件系统拓扑结构中的哪一个地方。
虚拟文件系统要做的第一件事情是找到这个文件系统。它首先查看file_systems指向的列表中的每一个file_system_type数据结构,查看所有已知的文件系统。如果它找到了一个匹配的名称,它就直到这个核心支持这个文件系统类型,并得到了文件系统相关例程的地址,去读取这个文件系统的超级块。如果它不能找到一个匹配的文件系统名称,如果核心内建了可以支持核心模块按需加载(参见第12章),就可以继续。这种情况下,在继续之前,核心会请求核心守护进程加载适当的文件系统模块。
参见fs/super.c do_mount()
参见fs/super.c get_fs_type()
第二步,如果mount传递的物理设备还没有安装,必须找到即将成为新的文件系统的安装点的目录的VFS inode。这个VFS inode可能在inode cache或者必须从支撑这个安装点的文件系统的块设备上读取。一旦找到了这个inode,就检查它是否是一个目录,而且没有其他文件系统安装在那里。同一个目录不能用作多于一个文件系统的安装点。
这时,这个VFS安装代码必须分配以一个VFS超级块并传递安装信息给这个文件系统的超级块读取例程。系统所有的VFS超级块都保存在super_block数据结构组成的super_blocks向量表中,必须为这次安装分配一个结构。超级块读取例程必须根据它从物理设备读取得信息填充VFS超级块的域。对于EXT2文件系统而言,这种信息的映射或者转换相当容易,它只需要读取EXT2的超级块并填到VFS超级块。对于其他文件系统,例如MS DOS文件系统,并不是这么简单的任务。不管是什么文件系统,填充VFS超级块意味着必须从支持它的块设备读取描述该文件系统的信息。如果块设备不能读取或者它不包含这种类型的文件系统,mount命令会失败。
每一个安装的文件系统用一个vfsmount数据结构描述,参见图9.6。它们在vfsmntlist指向的一个列表中排队。另一个指针, vfsmnttail指向列表中最后一个条目,而mru_vfsmnt指针指向最近使用的文件系统。每一个vfsmount结构包括存放这个文件系统的块设备的设备编号,文件系统安装的目录和一个指向这个文件系统安装时所分配的VFS超级块的指针。VFS超级块指向这一类型的文件系统的file_system_type数据结构和这个文件系统的root inode。这个inode在这个文件系统加载过程中一直驻留在VFS inode cache中。
参见fs/super.c add_vfsmnt()
9.2.5 Finding a File in the Virtual File System(在虚拟文件系统中查找一个文件)
为了找到一个文件在虚拟文件系统中的VFS inode,VFS必须依次名称,一次一个目录,找到中间的每一个的目录的VFS inode。每一个目录查找都要调用和文件系统相关的查找例程(地址放在代表父目录的VFS inode中)。因为在文件系统的VFS超级块中总是有文件系统的root inode,并在超级块中用指针指示,所以整个过程可以继续。每一次查找真实文件系统中的inode的时候,都要检查这个目录的目录缓存。如果目录缓存中没有这个条目,真实文件系统要么从底层的文件系统要么从inode cache中获得VFS inode。
9.2.6 Creating a File in the Virtual File System(在虚拟文件系统中创建一个文件)
9.2.7 Unmounting a File System(卸载一个文件系统)
我的工作手册通常把装配描述成为拆卸的反过程,但是对于卸载文件系统有些不同。如果系统有东西在使用文件系统的一个文件,那么这个文件系统就不能被卸载。例如,如果一个进程在使用/mnt/cdrom目录或它的子目录,你就不能卸载/mnt/cdrom。如果有东西在使用要卸载的文件系统,那么它的VFS inode会在VFS inode cache中。卸载代码检查整个inode列表,查找属于这个文件系统所占用的设备的inode。如果这个安装的文件系统的VFS超级块是dirty,就是它被修改过了,那么它必须被写回到磁盘上的文件系统。一旦它写了磁盘,这个VFS超级块占用的内存就被返回到核心的空闲内存池中。最后,这个安装的vmsmount数据结构也从vfsmntlist中删除并释放。
参见fs/super.c do_umount()
参见fs/super.c remove_vfsmnt()
The VFS Inode Cache
当游历安装的文件系统的时候,它们的VFS inode不断地被读取,有时是写入。虚拟文件系统维护一个inode cache,用于加速对于所有安装的文件系统的访问。每一次从inode cache读出一个VFS inode,系统就可以省去对于物理设备的访问。
参见fs/inode.c
VFS inode cache用散列表(hash table)的方式实现,条目是指针,指向既有同样hash value的VFS inode 列表。一个inode的hash value从它的inode 编号和包括这个文件系统的底层的物理设备的设备编号中计算出来。不论何时虚拟文件系统需要访问一个inode,它首先查看VFS inode cache。为了在inode hash table中查找一个inode,系统首先计算它的hash value,然后用它作为inode hash table的索引。这样得到了具有相同hash value的inode的列表的指针。然后它一次读取所有的inode直到它找到和它要找的inode具有相同的inode编号和相同的设备标识符的inode为止。
如果可以在cache中找到这个inode,它的count就增加,表示它有了另一个用户,文件系统的访问继续进行。否则必须找到一个空闲的VFS inode让文件系统把inode读入到内存。如何得到一个空闲的inode,VFS有一系列选择。如果系统可以分配更多的VFS inode,它就这样做:它分配核心页并把它们分成新的、空闲的inode,放到inode列表中。系统中所有的VFS inode除了在inode hash table中之外也在一个由first_inode指向的列表。如果系统已经拥有了它允许有的所有的inode,它必须找到一个可以重用的inode。好的候选是哪些使用量(count)是0的inode:这表示系统当前没有使用它们。真正重要的VFS inode,例如文件系统的root inode,已经有了一个大于0的使用量,所以永远不会选做重用。一旦定位到一个重用的候选,它就被清除。这个VFS inode可能是脏的,这时系统必须等待它被解锁然后才能继续。在重用之前这个VFS inode的候选必须被清除。
虽然找到了一个新的VFS inode,还必须调用一个和文件系统相关的例程,用从底层的真正的文件系统中毒取得信息填充这个inode。当它填充的时候,这个新的VFS inode的使用量为1,并被锁定,所以在它填入了有效的信息之前除了它没有其它进程可以访问。
为了得到它实际需要的VFS inode,文件系统可能需要访问其它一些inode。这发生在你读取一个目录的时候:只有最终目录的inode是需要的,但是中间目录的inode也必须读取。当VFS inode cache使用过程并填满时,较少使用的inode会被废弃,较多使用的inode会保留在高速缓存中。
The Directory Cache(目录缓存)
为了加速对于常用目录的访问,VFS维护了目录条目的一个高速缓存。当真正的文件系统查找目录的时候,这些目录的细节就被增加到了目录缓存中。下一次查找同一个目录的时候,例如列表或打开里边的文件,就可以在目录缓存中找到。只有短的目录条目(最多15字符)被缓存,不过这是合理的,因为较短的目录名称是最常用的。例如:当X服务器启动的时候,/usr/X11R6/bin非常频繁地被访问。
参见fs/dcache.c
目录缓存中包含一个hash table,每一个条目都指向一个具有相同的hash value的目录缓存条目的列表。Hash 函数使用存放这个文件系统的设备的设备编号和目录的名称来计算在hash table中的偏移量或索引。它允许快速找到缓存的目录条目。如果一个缓存在查找的时候花费时间太长,或根本找不到,这样的缓存是没有用的。
为了保持这些cache有效和最新,VFS保存了一个最近最少使用(LRU)目录缓存条目的列表。当一个目录条目第一次被放到了缓存,就是当它第一次被查找的时候,它被加到了第一级LRU列表的最后。对于充满的cache,这会移去LRU列表前面存在的条目。当这个目录条目再一次被访问的时候,它被移到了第二个LRU cache列表的最后。同样,这一次它移去了第二级LRU cache列表前面的二级缓存目录条目。这样从一级和二级LRU列表中移去目录条目是没有问题的。这些条目之所以在列表的前面只是因为它们最近没有被访问。如果被访问,它们会在列表的最后。在二级LRU缓存列表中的条目比在一级LRU缓存列表中的条目更加安全。因为这些条目不仅被查找而且曾经重复引用。
The Buffer Cache
当使用安装的文件系统的时候,它们会对块设备产生大量的读写数据块的请求。所有的块数据读写的请求都通过标准的核心例程调用,以buffer_head数据结构的形式传递给设备驱动程序。这些数据结构给出了设备驱动程序需要的所有信息:设备标识符唯一标识了设备,块编号告诉了驱动程序读去哪一块。所有的块设备被看成同样大小的块的线性组合。为了加速对于物理块设备的访问,Linux维护了一个块缓冲区的缓存。系统中所有的块缓冲区动保存在这个buffer cache,甚至包括那些新的、未使用的缓冲区。这个缓存区被所有的物理块设备共享:任何时候缓存区中都有许多块缓冲区,可以属于任何一个系统块设备,通常具有不同的状态。如果在buffer cache中有有效的数据,这就可以节省系统对于物理设备的访问。任何用于从/向块设备读取/写入数据的块缓冲区都进入这个buffer cache。随着时间的推移,它可能从这个缓存区中删除,为其它更需要的缓冲区让出空间,或者如果它经常被访问,就可能一直留在缓存区中。
这个缓存区中的块缓冲区用这个缓冲区所属的设备标识符和块编号唯一标识。这个buffer cache由两个功能部分组成。第一部分是空闲的块缓冲区列表。每一个同样大小的缓冲区(系统可以支持的)一个列表。系统的空闲的块缓冲区当第一次创建或者被废弃的时候就在这些列表中排队。当前支持的缓冲区大小是512、1024、2048、4096和8192字节。第二个功能部分是缓存区(cache)本身。这是一个hash table,是一个指针的向量表,用于链接具有相同hash index的buffer。Hash index从数据块所属的设备标识符和块编号产生出来。图9.7显示了这个hash table和一些条目。块缓冲区要么在空闲列表之一,要么在buffer cache中。当它们在buffer cache的时候,它们也在LRU列表中排队。每一个缓冲区类型一个LRU列表,系统使用这些类型在一种类型的缓冲区上执行操作。例如,把有新数据的缓冲区写到磁盘上。缓冲区的类型反映了它的状态,Linux当前支持以下类型:
clean 未使用,新的缓冲区(buffer)
locked 锁定的缓冲区,等待被写入
dirty 脏的缓冲区。包含新的有效的数据,将被写到磁盘,但是直到现在还没有调度到写
shared 共享的缓冲区
unshared 曾经共享的缓冲区,但是现在没有共享
不论何时文件系统需要从它的底层的物理设备读取一个缓冲区的时候,它都试图从buffer cache中得到一个块。如果它不能从buffer cache中得到一个缓冲区,它就从适当大小的空闲列表中取出一个干净的缓冲区,这个新的缓冲区会进入到buffer cache中。如果它需要的缓冲区已经在buffer cache中,那么它可能是也可能不是最新。如果它不是最新,或者它是一个新的块缓冲区,文件系统必须请求设备驱动程序从磁盘上读取适当的数据块。
象所有的高速缓存一样,buffer cache必须被维护,这样它才能有效地运行,并在使用buffer cache的块设备之间公平地分配缓存条目。Linux使用核心守护进程bdflush在这个缓存区上执行大量的整理工作,不过另一些是在使用缓存区的过程中自动进行的。
9.3.1 The bdflush Kernel Daemon(核心守护进程bdflsuh)
核心守护进程bdflush是一个简单的核心守护进程,对于有许多脏的缓冲区(包含必须同时写到磁盘的数据的缓冲区)的系统提供了动态的响应。它在系统启动的时候作为一个核心线程启动,相当容易混淆,它叫自己位“kflushd”,而这是你用ps显示系统中的进程的时候你会看得的名字。这个进程大多数时间在睡眠,等待系统中脏的缓冲区的数目增加直到太巨大。当缓冲区分配和释放的时候,就检查系统中脏的缓冲区的数目,然后唤醒bdflush。缺省的阈值是60%,但是如果系统非常需要缓冲区,bdflush也会被唤醒。这个值可以用updage命令检查和设置:
#update –d
bdflush version 1.4
0: 60 Max fraction of LRU list to examine for dirty blocks
1: 500 Max number of dirty blocks to write each time bdflush activated
2: 64 Num of clean buffers to be loaded onto free list by refill_freelist
3: 256 Dirty block threshold for activating bdflush in refill_freelist
4: 15 Percentage of cache to scan for free clusters
5: 3000 Time for data buffers to age before flushing
6: 500 Time for non-data (dir, bitmap, etc) buffers to age before flushing
7: 1884 Time buffer cache load average constant
8: 2 LAV ratio (used to determine threshold for buffer fratricide).
不管什么时候写入了数据,成为了脏的缓冲区,所有的脏的缓冲区都链接在BUF_DIRTY LRU列表中,bdflush会尝试把合理数目的缓冲区写到它们的磁盘中。这个数目也可以用update命令检查和设置,缺省是500(见上例)。
9.3.2 The update Process(update 进程)
update 命令不仅仅是一个命令,它也是一个守护进程。当以超级用户身份(系统初始化)运行的时候,它会定期把所有旧的脏缓冲区写到磁盘上。它通过调用系统服务例程执行这些任务,或多或少和bdflush的任务相同。当生成了一个脏缓冲区的时候,都标记上它应该被写到它自己的磁盘上的系统时间。每一次update运行的时候,它都查看系统中所有的脏的缓冲区,查找具有过期的写时间的缓冲区。每一个过期的缓冲区都被写到磁盘上。
参见fs/buffer.c sys_bdflush()
The /proc File System
/proc文件系统真实地体现了Linux虚拟文件系统的能力。它实际上并不存在(这也是Linux另外一个技巧),/proc和它的子目录以及其中的文件都不存在。但是为什么你可以cat /proc/devices? /proc文件系统,象一个真正的文件系统一样,也向虚拟文件系统登记自己,但是当它的文件和目录被打开,VFS执行调用请求它的inode的时候,/proc文件系统才利用核心中的信息创建这些文件和目录。例如,核心的/proc/devices文件是从核心描述它的设备的数据结构中产生出来的。
/proc文件系统代表了一个用户可读的窗口,进入核心的内部工作空间。一些Linux子系统,例如第12章描述的Linux核心模块,都在/proc文件系统中创建条目。
Device Special Files
Linux,象所有版本的Unix一样,把它的硬件设备表示成为特殊文件。例如,/dev/null是空设备。设备文件不在文件系统中占用任何数据空间,它只是设备驱动程序的一个访问点。EXT2文件系统和Linux的VFS都把设备文件作为特殊类型的inode。有两种类型的设备文件:字符和块特殊文件。在核心内部本身,设备驱动程序都实现文件的基本操作:你可以打开、关闭等等。字符设备允许字符模式的I/O操作,而块设备要求所有的I/O通过buffer cache。当对于一个设备文件执行一个I/O请求的时候,它被转到系统中适当的设备驱动程序。通常这不是一个真正的设备驱动程序,而是一个用于子系统的伪设备驱动程序(pseudo-device driver)例如SCSI设备驱动程序层。设备文件用主设备编号(标识设备类型)和次类型来引用(用于标识单元,或者主类型的实例)。例如,对于系统中的第一个IDE控制器上的IDE磁盘,主设备编号是3,IDE磁盘的第一个分区的次设备编号应该是1,所以,ls –l /dev/hda1输出
$ brw-rw---- 1 root disk 3, 1 Nov 24 15:09 /dev/hda1
参见/include/linux/major.h中所有Linux的主设备编号
在核心中,每一个设备用一个kdev_t数据类型唯一描述。这个类型有两个字节长,第一个包括设备的次设备编号,第二个包括主设备编号。上面的IDE设备在核心中保存为0x0301。代表一个块或者字符设备的EXT2 inode把设备的主和次设备号放在它的第一个直接块指针那里。当它被VFS读取的时候,代表它的VFS inode数据结构的I_rdev域被设成正确的设备标识符。
参见include/linux/kdev_t.h