CXD Linux Engineer

Linux文件系统

2017-02-05

前言

本篇文章是翻译于:

The File system

Linux文件系统

本章介绍Linux内核是怎样管理它所支持的各种文件系统的。我们将会讨论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磁盘上的/dev/hdal分区是系统中的第一个IDE磁盘驱动器的第一个分区,它是一个块设备。 Linux文件系统将这些块设备看作一个简单的线性排列的块集合,它并不需要关心底层物理磁盘到底是怎样实现的。这是块设备驱动的任务, 驱动需要将一个读特定块的请求映射到物理设备中对应的保存这个块的磁道、扇区和柱面上。不管物理设备是什么样的,对于文件系统来说都是一样的。 而且不管什么样的文件系统,使用什么样的磁盘驱动器,用什么介质来存储数据,对于Linux系统来说都是一样的。 文件系统不仅可以存放在本地而且可以通过网络连接挂载远程磁盘。考虑下面这种情况,位于SCSI磁盘上的Linux根文件系统:

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驱动器的主磁盘分区。 并且第一个IDE驱动器是一个PCI驱动器而第二个是ISA驱动器它还用于控制IDE CDROM。 还可以将我的远程Alpha AXP Linux系统上的文件系统挂载到本地/mnt/remote目录下。

文件系统中的文件用于保存数据,例如保存本篇文章内容的文件是一个被称为filesystems.tex的ASCII文件。 一个文件系统不仅保存着文件中的数据而且还需要保存Linux用户和程序能够看到的用于表示这个文件的所有信息。 这些信息包括文件的使用权限,所有指向这个文件软连接等等。并且这些信息必须是安全可靠的,因为操作系统的基本功能依赖于文件系统。 没有人会使用一个经常丢失数据和文件的操作系统。

Minix是Linux使用的第一个文件系统,它有很多功能缺陷并且性能低下。文件名不能超过14个字符,而且文件的最大大小只有64MBytes64MBytes在当时看来是非常大的但是有可能需要更大的文件来存储中型数据库文件。扩展文件系统(Extended File system)或者称为EXT是第一个专门为Linux系统设计的文件系统。 在1992年4月被Linux内核使用,它解决了很多问题但是仍然缺乏性能。

所以1993年第二个扩展文件系统(Second Extended File system)被引入简写为EXT2,本文稍后将会详细它的实现细节。 当EXT文件系统被加入到Linux后内核发生了一个重大的变化。被称为虚拟文件系统或者VFS的接口层被引入, 它将真实的文件系统从操作系统和系统服务中分离。虚拟文件系统使得Linux支持很多差异巨大的文件系统,它们向VFS提供一个公共的软件接口。 文件系统的所有细节被VFS屏蔽,所以对于Linux内核的其它部分和运行在内核上面的程序来说所有的文件系统都是一样的。 Linux的虚拟文件系统允许同时挂载多个不同的文件系统。

Linux虚拟文件系统的实现需要尽可能的高效快速,并且需要保证文件和文件中的数据不会出现错误。 这两个要求是相互矛盾的,VFS会将每个文件系统在使用时产生的信息缓存到内存中。当这些缓存的数据被修改或者创建了新的目录、写入了新数据和被删除时, 需要格外小心的将正确的信息更新到文件系统中去。如果你可以看到内核运行时文件系统中的数据结构,你可能会看到数据块被文件系统读出和写入的过程。 数据结构描述了被访问的文件和目录从创建到删除的整个过程,并且设备驱动程序会一直使用它来读取和存储数据。其中最重要的缓冲区是被集成到各个文件系统中 用于访问底层块设备的缓冲区。当数据块被访问时他们被缓存到缓冲区并根据他们的状态被加入到不同的队列中去,缓冲区不仅用于缓存数据, 它还可以帮助管理文件系统与块设备驱动之间的异步接口。

Second Extended File system (EXT2)

1_ext2

图 9.1: EXT2文件系统在磁盘上的组织方式

EXT2是专门为Linux设计的一个强大的可扩展的文件系统。到不前为止它是Linux社区里最为成功的文件系统并且它是目前所有Linux发行版的基础文件系统。

EXT2文件系统和其他文件系统一样,都是使用数据块来保存文件的。这些数据块都是相同大小的, 但是可以使用mke2fs等工具在文件格式化的时候来设置数据块的大小。每个文件都是由整数个数据块来存储。 如果数据块的大小为1024字节,而某个文件的大小是1025字节,则这个文件需要占用两个数据块。 这意味着几乎浪费了一半的空间来存放这个文件。这是CPU负载和内存跟磁盘利用率之间的权衡, 通常这种情况下Linux和其他大多数操作系统都会选择降低磁盘利用率而减轻CPU的负载。不是所有数据块都是用来存放数据的, 有些需要用来存放描述文件系统结构的信息。EXT2文件系统通过使用inode数据结构描述每个文件来定义文件系统拓扑结构。 一个inode节点描述了该文件占用了哪些数据块、文件的最后修改时间和这个文件的类型。 EXT2中的每个文件都拥有一个单独的inode节点,每个节点使用唯一的一个数字来标记。 文件系统中的inode节点集中存放在inode表中。EXT2中的目录是一个简单的特殊文件(它本身也是使用inode来描述)他保存着指向此目录下面文件的inode节点的指针。

图9.1展示了EXT2文件系统在块设备中存放文件的布局。就文件系统而言块设备只是一系列可读可写的数据块。文件系统不需要关心数据块存放在物理媒介中的哪个地方, 这是设备驱动的工作。当文件系统需要从块设备中读取信息或者数据时,都是要求设备驱动读取整数个数据块的。 EXT2文件系统将它管理的逻辑分区分成多个块组。

每个块组都保存着对文件系统完整性至关重要的信息备份,以及存放真实文件和目录的数据和信息。 这些备份是用于当出现错误时恢复文件系统的。下面的章节将会详细介绍每个块组包含的信息。

EXT2文件系统的inode节点

ext2_inode

图 9.2: EXT2文件系统的inode节点

在EXT2文件系统中,inode节点是最基本的元素;每个文件和目录都使用一个且唯一一个inode节点来描述。 每个块组使用一个位图形式的inode表来集中存放inode节点,用于文件系统跟踪节点的分配和释放。图 9.2 展示了一个EXT2节点的详细信息:

  • mode 它保存了两个属性:该inode节点描述的是什么文件,这个用户对这个文件拥有那些权限。对于EXT2文件系统来说 一个inode节点可以描述一个文件、目录、符号链接、块设备文件、字符设备文件或者FIFO

  • Owner Information 用于标记这个文件或目录属于哪个用户和组的。它可以使文件系统正确的允许对应的用户访问文件。

  • Size 这个文件的大小,单位是字节。

  • Timestamps 保存这个文件的创建时间和最后修改时间。

  • Datablocks 保存这个文件的数据所存放的数据块地址。前十二个地址指向保存这个文件数据的物理数据块, 最后三个指向间接数据块,这些间接数据块里面保存的是存放文件数据的数据块地址。例如双重间接块中存放的是地址, 这些地址指向存放文件数据的数据块。这意味着文件体积小于或等于12个数据块时比那些大文件的访问速度要快一些。

你需要知道EXT2的节点还可以用于描述特殊的设备文件,他们不是实际的文件但可以使用它来访问设备。这些允许程序访问Linux的物理设备的设备文件都存放在/dev目录下, 例如mount程序使用一个参数来存放他希望挂载的设备的设备文件名。

EXT2文件系统的超级块

超级块保存着描述这个文件系统的基本信息。文件系统管理器使用这些信息来维护文件系统。 通常只有文件系统挂载时需要读取块组0上的超级块,但是每个块组上都有一个超级块备份。超级块包含下面这些信息:

  • Magic Number 它用于挂载程序区分不同文件系统的,当前版本的EXT2文件系统的魔数是0xEF53

  • Revision Level 保存主次版本号用于挂载程序通过版本号来决定使用该文件系统的哪些功能特性。 还有功能兼容性字段用于帮助挂载程序确定此文件系统中的哪些新特性可以安全使用。

  • Mount Count and Maximum Mount Count 用于帮助系统确定是否需要全面检查此文件系统。文件系统的每次挂载都会将此值加一, 当达到最大值时会打印警告信息:“已经达到最大挂载数量,建议运行e2fsck工具”

  • Block Group Number 保存这个超级块的块组编号。

  • Block Size 单个数据块的大小,单位是字节,例如1024字节。

  • Blocks per Group 一个块组中的数据块数量,和数据块大小一样当文件系统创建后此值是固定的。

  • Free Blocks 文件系统中可用的数据块数量

  • Free Inodes 文件系统中可用的inode节点数量

  • First Inode 文件系统中的第一个inode节点,EXT2根文件系统中第一个inode节点描述的是根目录/

EXT2文件系统的组描述符

每个块组都有一个数据结构体来描述。和超级块一样,每个块组中都有一个包含所有组描述符的备份以防止文件系统损坏。

每个组描述符包含如下信息:

  • Blocks Bitmap 存放块组中已经被分配的块位图,在数据块分配和释放时使用

  • Inode Bitmap 存放块组中已经被分配的inode节点位图,在inode节点分配和释放时使用

  • Inode Table 存放块组中的所有inode节点,所有inode节点组成一个Inode

  • Free blocks count, Free Inodes count, Used directory count 组描述符被一个接着一个的存放在一起,他们共同组成了一个组描述符表。每个块组中都存放着所有的组描述符。 但只有块组0中的备份会被EXT2文件系统使用,其他的组描述符备份和超级块备份一样用于防止块组0上的备份被损坏。

EXT2文件系统的目录

ext2_dir

图 9.3: EXT2文件系统的目录

在EXT2文件系统中,目录是一个被用于创建和保存文件访问路径的特殊文件。图9.3展示了内存中目录的存放结构。

每个目录文件是一个目录列表,包含如下信息:

  • inode 此目录的inode节点。它是存放在块组中的Inode表的一个索引。 图9.3中,文件名为file的文件的目录项中有一个索引指向inode号为i1inode节点。

  • name length 这个目录项的长度,单位是字节。

  • name 这个目录项的名字。

每个目录中的前两项总是...,分别代表此目录和父目录(上一级目录)。

在EXT2文件系统中查找文件

Linux文件名的格式和所有Unix系统的一样。由一系列被/分开的目录名加上最后的文件名组成。 一个文件名实例/home/rusling/.cshrc其中/home/rusling是目录名,.cshrc是文件名。 和所有其他Unix系统一样,Linux对文件名的格式没有限制,文件名可以为任意长度,任意可显示的字符。 为了在EXT2文件系统中找到一个文件的inode节点,系统需要循环解析文件路径中的目录直到找到此文件。 文件系统中的第一个inode节点是根节点,它存放在文件系统的超级块中。 为了得到一个inode节点我们需要在对应块组中的inode表中查找它。例如,如果根节点的inode号为42, 则此根节点存放在块组0的inode表中的第42个位置。 EXT2文件系统中的根节点是一个目录,换句话说根节点描述的是一个目录,它的数据块中存放的是子目录项。

home目录只是很多目录中的一个,这个目录项存放着描述/home这个目录的inode节点号。 为了找到rusling目录项,我们必须首先读取home目录,得到描述/home/rusling这个目录的inode节点号。 然后我们读取/home/rusling这个目录的节点来找到.cshrc这个文件的inode节点号。 最后我们通过这个节点号来得到.cshrc文件中的数据。

在EXT2文件系统中改变文件大小

文件系统有一个通病就是趋向于碎片化。保存单个文件数据的数据块分散在整个文件系统的不同地方,数据块越分散就会导致顺利访问数据的效率越低。 EXT2文件系统通过将新的数据块分配在接近当前数据块的位置,或者至少分配在同一个块组中来试图克服这个问题。只有当新的数据块分配失败时才会考虑其他块组。 当进程试图向文件中写数据时,文件系统会检查数据是否到达文件的最后一个数据块的末尾。如果是,文件系统才会分配一个新的数据块给这个文件。 在数据块分配完成之前,进程不能运行;进程必须等待文件系统分配一个新的数据块并将剩余的数据写入数据块后才能继续运行。 EXT2的数据块分配函数做的第一件事是给这个文件系统的超级块加锁。数据块的分配和释放都需要改变超级块里面的内容,并且Linux文件系统不允许多个进程同时操作超级块。 如果其他进程需要分配新的数据块,它必须等到这个进程分配完成。 进程在等待超级块时会被挂起,不在运行,直到当前使用者放弃超级块的使用权限。

超级块的使用是先到先得的,先得的标志是进程得到超级块的控制权,进程会总是保持控制权直到任务完成。 锁定超级块之后,进程会检查文件系统中是否有足够的空闲块。如果没有足够多的数据块,则这个数据块分配请求会失败,进程会主动放弃这个文件系统超级块的控制权。 如果文件系统中有足够多的数据块,进程会尝试分配数据块。

如果EXT2文件系统有预分配的功能则我们会得到一个预分配的数据块。这个预分配的数据块不是真实存在的,他只是在已分配数据块的位图中标记过。 描述这个文件的VFS虚拟文件系统的inode节点中有两个EXT2文件系统特定的字段prealloc_blockprealloc_count, 分别代表第一个预分配数据块的块号和预分配数据块的数量。如果没有预分配数据块或者预分配功能没有打开,EXT2文件系统必须分配一个新的数据块。 EXT2文件系统首先查看文件中最后一个数据块之后的数据块是否空闲,逻辑上这是使得顺序访问效率最高的数据块。 如果这个数据块不是空闲的,则会在这个数据块附近的64个数据块的范围内查找空闲数据块。 虽然查找到的数据块不是最有效的,但是至少是非常接近这个文件的其他数据块并且在一个块组内。

如果这样还找不到空闲数据块,则进程开始在其他块组中查找,直到找到空闲数据块。 数据块分配代码在其他块组中查找时会首先查找8个连续的空闲数据块组成的簇,如果没有找到则会降低要求,例如7个连续的空闲数据块。 当数据块预分配功能被启用并且需要预分配时分配代码还需要更新prealloc_blockprealloc_count这两个字段。

无论在哪找到空闲块,块分配代码都要更新块组中的块位图并在缓冲区中分配一个数据缓冲区。 数据缓冲区被文件系统所支持的设备标识符和所分配的块号唯一标记。 缓冲区首先被清零,写入新的数据后被标记为dirty,表示缓冲区中的数据还没有写入到物理磁盘中。 最后超级块本身也被标记为dirty,表示它被更改过并且没有被加锁。 如果此时有其他进程在等待超级块,则允许等待队列中的第一个进程再次运行并获得超级块的唯一控制权来操作文件。 此时进程的数据可以被写入到新的数据块中,如果这个数据块被写满则上面的整个过程将会再次运行一遍再次分配新的数据块。

虚拟文件系统(VFS)

vfs

图 9.4:虚拟文件系统的整体框架

图 9.4展示了Linux内核的虚拟文件系统和实际文件系统之间的关系。虚拟文件系统需要管理任何时间挂载的任何文件系统。 为此它需要维护描述虚拟文件系统和被挂载的文件系统的所有数据结构。

非常迷惑的是,VFS描述的超级块和inode节点和EXT2文件系统的非常相似。和EXT2一样,VFS的inode也可以表示系统中的文件和目录,虚拟文件系统的内容和拓扑结构。 从现在开始为了避免冲突,我会使用VFS superblocksVFS inodes来区分EXT2的inodessuperblocks

当某个文件系统在初始化时,它会将自己注册到VFS中。这发生在系统启动时操作系统初始化阶段。 真正的文件系统被编译进内核中或者是被编译为一个可加载的模块。 当系统需要的时候才会加载文件系统模块,例如当VFAT文件系统被编译为内核模块时,只有VFAT文件系统被挂载的时候才会加载进内核。 当一个基于块设备的文件系统被挂载时(也包括根文件系统),VFS必须读取它的超级块。 每个文件系统的超级块读取函数都必须得到文件系统的拓扑结构并将这些信息映射到VFS的超级块数据结构中。 VFS将所有被挂在的文件系统和它们的VFS超级块组成一个列表,每个VFS超级块包含执行特定功能函数的指针和信息。 例如,表示EXT2文件系统的超级块包含读取EXT2特定的inode节点的函数指针。 这个EXT2的inode节点读取函数和其他文件系统特定的读取函数一样,需要填充VFS的inode相关字段。 每个VFS超级块都保存着第一个VFS inodes的指针,对于根文件系统这个inode表示的是/根目录。 对于EXT2文件系统,这种信息映射非常有效,但对于其他文件系统,这种映射的适用性要低一些。

当系统的进程需要访问目录或文件时,系统函数会被调用来遍历VFS节点。

例如,在一个目录中执行ls或者cat命令会使虚拟文件系统搜索代表该文件系统的VFS inode节点。 由于每个文件和目录都使用VFS inode表示,一个节点号可能被多次访问。 为了加快节点访问速度,这些节点被保存在缓存中。如果缓存中没有该节点则文件系统特定的函数会被执行来读取对应的节点。 读取节点的动作会将该节点放到缓存中,以便下次直接在缓存中访问。使用次数最少的VFS inode会被移出缓存。

所有Linux的文件系统共用一个数据缓存,来缓存物理设备上的数据以加快访问速度。

这个数据缓冲区与文件系统无关,它集成在Linux内核中用于分配、读、写缓存数据。 使Linux的文件系统独立于底层设备和设备驱动有明显的好处。所有块结构的设备将他们注册进Linux内核时都表示为统一的,基于块的,同常为异步的接口。即使是相对复杂的块设备例如SCSI设备。 当实际的文件系统读取物理磁盘的数据时,会请求对应的块设备驱动来读取物理数据块。 缓冲区集成在块设备的接口中,当数据块被文件系统读取时他们保存在全局缓冲区中,这个缓冲区被所有文件系统和内核共享。 缓冲区中的数据块使用他们的块号和此设备的唯一标识符来区分。 因此如果相同的数据经常被读取,它会从缓冲区中再次获取数据而不是需要更长的时间读取物理磁盘。 有些设备支持预读取,就是预测他可能需要的数据提前放到缓冲区中。

VFS也维护着一个目录缓冲区,以便加快多级目录中节点的访问速度。

做一个测试,尝试ls列出你没有访问过的目录。你会发现第一次列出时会有轻微的停顿,第二次则会立即显示出来。 目录缓冲区不会存放目录本身的节点,他们应该在节点缓冲区中,目录缓冲区只是简单的存放此目录名和他们的节点号之间的映射关系。

VFS的超级块

每一个挂载的文件系统都用一个VFS超级块来表示;VFS超级块还包含下面信息:

  • Device 这是文件系统中保存的块设备的设备标识符。例如/dev/hda1系统中的第一个IDE磁盘的设备标识符为0x301

  • Inode pointers mounted节点指针,指向此文件系统的第一个节点。covered节点指针指向此文件系统被挂载在某个目录下时此目录的节点。 根文件系统的VFS超级块没有covered指针。

  • Blocksize 此文件系统的单个块大小,单位是字节,例如1024字节。

  • Superblock operations 指向此文件系统的一组超级块操作函数的指针。这些函数用于VFS读、写节点个超级块。

  • File System type 指向被挂载文件系统的file_system_type数据结构体的指针。

  • File System specific 指向此文件系统所需要的信息的指针。

VFS inode

和EXT2文件系统一样,每个文件、目录等等在VFS中都使用一个且唯一一个VFS inode来表示。

每个VFS inode中的信息是通过底层实际文件系统特定的函数创建的。VFS inode只在系统需要的时候存在于内核的内存和VFS inode缓冲区中。 VFS inode还包含下面信息:

  • device 保存这个文件的设备标识符或者此VFS inode表示的任何东西。

  • inode number 实际文件系统中唯一的节点号。__device__和__inode number__结合成为虚拟文件系统中的唯一标志。

  • node 和EXT2一样,该字段描述了此VFS inode表示的是什么以及它的访问权限。

  • user ids 此文件所有者的标识符

  • times 创建、修改和写入的时间

  • block size 保存此文件的块大小,单位是字节,例如1024字节。

  • inode operations 指向块操作函数的指针,这些函数是文件系统特定的用于操作此节点。例如,追踪此节点所表示的文件。

  • count 当前VFS inode的引用计数,如果为0则说明此节点没有人使用可以被丢弃或者回收利用。

  • lock 这个字段用于给VFS inode加锁,例如当从文件系统中读取时需要加锁。

  • dirty 标记这个VFS inode是否被写过,如果是则需要更新到底层实际的文件系统。

  • file system specific information 文件系统特定的信息。

文件系统的注册

file-systems

图 9.5:文件系统的注册

当编译Linux内核的时候会询问你是否支持每个文件系统,当内核编译完成,文件系统的启动代码中包含被编译进去的文件系统的初始化函数调用。

Linux的文件系统也可以被编译为模块。在这种情况下,他们会在需要的时候被加载或者使用insmod命令手动加载。 当文件系统的模块加载时会将自己注册进内核,卸载时再注销。每个文件系统的初始化函数都会将自己注册到虚拟文件系统中, 这表现在file_system_type数据结构中保存着文件系统的名字和一个指向VFS超级块读函数的指针。 图 9.5展示的是file_system_type数据结构组成的file_systems链表。每个file_system_type数据结构包含下面信息:

  • Superblock read routine 当实际文件系统被挂载时VFS会调用此函数。

  • File System name 此文件系统的名字,例如ext2

  • Device needed 此文件系统是否需要设备的支持?不是所有的文件系统都需要硬件设备来保存它,例如/proc文件系统就不需要实际的块设备。

你可以通过/proc/filesystems来查看内核中注册的文件系统。例如:

      ext2
nodev proc
      iso9660

文件系统的挂载

当超级用户试图挂载文件系统时,Linux内核必须首先检查传递给系统调用的参数的正确性。虽然mount命令会做一些基本的检查, 但是它不知道Linux内核支持哪些文件系统或者指定的挂载点是否真实存在。考虑下面的mount命令:

$ mount -t iso9660 -o ro /dev/cdrom /mnt/cdrom

mount命令将传递三种信息给内核;文件系统的名字、保存此文件系统的物理块设备、挂载点。

虚拟文件系统必须首先找到需要挂载的文件系统,为此它会在file_systems链表中遍历每个file_system_type数据结构。 如果找到对应的文件系统名字他就知道此文件系统是内核所支持的,并且会调用此文件系统特定的函数来读取该文件系统的超级块。 如果没有找到匹配的文件系统名字,但是内核被编译为按需加载内核模块。在这种情况下内核在挂载之前会首先加载对应的文件系统模块。

接下来,如果mount命令传递的物理设备还没有被挂载,必须先找到挂载点的VFS inode。 这个VFS inode可能在节点缓冲区中或者需要从被挂载文件系统的块设备中读取。 一旦找到它会首先检查此节点是否是一个目录并且没有其他的文件系统挂载在这里。同一个目录不能同时挂载多个文件系统。

此时VFS挂载代码必须分配一个VFS超级块并将挂载信息传递给该文件系统特定的超级块读取函数。 系统中的所有VFS超级块都保存在super_block数据结构的super_blocks向量中,并且每次挂载都必须分配一个。 超级块读取函数必须根据从物理设备中读取的信息来填充VFS超级块的相应字段。 对于EXT2文件系统来说这种信息映射或者翻译非常简单,只需要读取EXT2文件系统的超级块然后填充到VFS超级块中去。 对于其他文件系统,例如MS DOS文件系统就没有这么简单了。无论什么文件系统,填充VFS超级块意味着文件系统需要读取块设备的所有信息。 如果该块设备不能被读取或者它保存的文件系统类型不对则mount命令执行失败。

mounted

图 9.6:被挂载的文件系统

每一个被挂载的文件系统都使用vfsmount数据结构来描述;如图9.6,vfsmntlist链表中的一个节点。

另外一个指针vfsmnttail指向链表中的最后一项,mru_vfsmnt指针指向最近使用过的文件系统。 每个vfsmount数据结构都包含块设备的设备号、此文件系统的挂载点和指向分配给此文件系统的VFS超级块的指针。 相反的VFS超级块指向此文件系统的file_system_type数据结构和根节点。这个根节点会从此文件系统被加载开始一直常驻VFS节点缓冲区中。

在虚拟文件系统中查找文件

为了在虚拟文件系统中查找文件的VFS inode,VFS必须解析目录名,查找代表每一级目录的VFS inode。 每一个目录的查找都需要到对应的文件系统中查找代表当前目录的VFS inode。 之所以可以这样做是因为我们始终拥有每个文件系统根节点的VFS inode并且VFS超级块中的指针指向它。 每次在实际文件系统中查找节点时首先检查目录缓冲区中是否存在。如果不存在则实际文件系统在底层文件系统或者节点缓冲区中得到VFS inode

卸载文件系统

如果有文件正在被使用则此文件系统不能被卸载。例如当一个进程正在使用/mnt/cdrom中的目录或者它的任何子目录则此时你不能卸载它。 如果卸载的文件系统正在被使用则节点缓冲区中应该有该文件系统的VFS inode,检查代码通过遍历所有节点并查看节点所属设备是否是该文件系统的。 如果被挂载文件系统的VFS超级块被标记为dirty,说明它被更改过,卸载时必须将缓存写回磁盘。当写回磁盘后,VFS超级块所占用的内存将会返回到内核的内存池中。 最后该文件系统的vfsmount数据结构将会从vfsmntlist链表上去除并释放所占用的内存。

VFS的节点缓冲区

当被挂载的文件系统被索引时,他们的VFS inode可以被连续读取或者写入。虚拟文件系统维护了一个节点缓冲区来加速所有被挂载文件系统的访问。 每次从节点缓冲区中读取VFS inode时,系统都会保存对物理设备的访问。

VFS的节点缓冲区通过哈希表来实现,其条目是指向具有相同哈希值的VFS inode链表的指针。节点的哈希值是通过节点号和底层物理设备的设备号计算出来的。 无论何时虚拟文件系统需要访问节点时首先在缓冲区中查找。为了在缓冲区中找到节点,系统首先计算它的哈希值然后使用哈希值在节点哈希表中索引。 节点哈希表会给出一个指针,指向具有相同哈希值的节点链表。然后再遍历节点链表直到找到节点号和设备标识符都相同的节点,这个节点就是我们要找的。

如果能够在缓冲区中找到节点,它的访问计数会加一,表明有另外一个用户在使用它,并且该文件系统可以继续访问。 否则必须找到一个空的VFS inode以便文件系统可以从内存中读取。VFS有几种方式得到一个空的VFS inode。 系统可能分配更多的VFS inode这是一种方式,分配的内存页被划分为新的和空的节点然后将他们放到节点链表中。 系统中所有的VFS inode都在first_inode链表和节点哈希表中。如果系统中的节点数已经达到最大值, 他必须找到一个最合适的候选节点来重复利用。好的候选节点是访问计数为零的,这表明目前没有被使用。 非常重要的VFS inode例如文件系统的根节点的访问计数总是大于0,所以始终不会被替换。 一旦被选为候选节点则此节点会被清除。这个候选节点可能被标记为dirty此时需要写回到文件系统, 也有可能被锁定,则系统需要等待解锁才能继续操作。候选节点必须被清除才能重复利用。

找到新的VFS inode后,必须调用文件系统特定的函数来读取底层实际文件系统的信息填充到VFS inode中。 在填充过程中这个新的VFS inode的访问计数加一并且被加锁以防止信息填充没有完成就有其他进程来访问。

要得到实际需要的VFS inode,文件系统可能需要访问多个其他节点。 这发生在当你读取一个目录时,只有最后一个目录是你所需要的但是中间的节点也必须读取。 VFS inode缓冲区会丢弃那些使用较少的节点,经常使用的节点会长时间保存在缓冲区中。

目录缓冲区

为了加速常用目录的访问,VFS维护了一个目录项缓冲区。

当实际文件系统查找目录时它们的数据被添加到目录缓冲区中去了。 当下一次查找相同目录,例如当需要列出目录中的文件或者打开文件时可以在目录缓冲区中查找。 目录缓冲区只会缓存较短的目录项(小于15个字符),这些目录项正好是被经常使用的。例如当X服务运行时/usr/X11R6/bin会被大量访问。

目录缓冲区由哈希表构成,每个表项指向目录缓冲区中的一项。哈希函数使用设备号和目录名来计算偏移值或者索引,使目录缓冲区中的目录可以快速访问。

为了维护缓冲区中目录的有效性并且保证都是最近使用过的(Least Recently Used – LRU)目录。当第一次访问目录时会将其加入到缓冲区中,它被放置在第一级LRU链表的末尾。 如果缓冲区已满则会替换掉LRU链表中的第一项。当目录项再次被访问则会将目录升级进入第二级LRU链表的末尾。 这又会将第二级LRU链表中的第一项替换掉。目录项排在链表前面的唯一原因是它最近没有被访问过。如果被访问过应该排在链表尾端。 第二级LRU链表中的表项比第一级的安全。这样做的目的是将最近被多次访问的目录保存更长时间。

数据缓冲区

buffer-cache

图9.7:数据缓冲区

当文件系统被使用时通常会向块设备发送大量读写数据块的请求。所有数据块的读写请求都是组装成buffer_head数据结构通过内核标准系统调用传递给设备驱动。 buffer_head数据结构中包含所有块设备驱动所需要的信息,使用设备标识符来确定要操作的设备,使用块号告诉驱动应该读取哪个数据块。 所有块设备被视为具有相同大小的数据块的线性集合。为了加速物理块设备的访问,Linux维护了一个数据块缓冲区。 系统中的所有数据块都存放在块缓冲区中,包括新的、未使用的数据块。这个缓冲区被所有物理块设备共享,在任何时候缓冲区中总是有很多数据块, 这些数据块可以属于系统中的任何块设备并且通常处于各种不同状态。当数据块可以从缓冲区中获得,系统会保存对物理设备的访问。 所有数据块都是从块设备中读取出来的,向块设备写入的数据也是来自缓冲区。 随着时间的推移数据块可能会被移除缓冲区以腾出内存空间或者保留在缓冲区中以便频繁访问。

缓冲区中的数据块通过设备标识符和块号唯一标记。缓冲区由两个功能区组成,一部分存放空闲数据块, 缓冲区支持的数据块大小分为512, 1024, 2048, 4096 和 8192 字节。 每个支持的块大小都有一个队列来存放系统中的空闲数据块,数据块在第一次创建或者被移除的时候加入到队列中。 第二部分是缓冲区本身,它们被组织成一个哈希表。哈希表的索引是由设备号和块号生成。图9.7展示了哈希表中数据的组织方式。 当数据块在缓冲区中,他们也被组织为LRU链表。每个块类型都有一个LRU链表,这些链表被系统用于管理缓冲区,例如将缓冲区数据写入磁盘。 数据块的类型反映了它的状态,Linux目前支持下面这些类型:

  • clean 没有被使用的块区;

  • locked 数据块被加锁,正在等待数据的写入;

  • dirty 数据块中有新的、有用的数据,将要被写入到磁盘但是还没有被调度执行;

  • unshared 曾经被共享过的数据块,现在取消共享了;

当文件系统需要从底层设备中读取数据时,它会试图从缓冲区中得到一个空闲数据块。如果不能从缓冲区中得到一个数据块, 则它会从空闲数据块列表中得到一个干净的数据块,这个新的数据块将加入到缓冲区中。如果文件系统所需要的数据块在已经在缓冲区中, 这个数据块可能不是最新的。如果数据块不是最新的或者需要新的数据块,文件系统必须请求设备驱动来读取块设备中相应的数据块。

和所有缓冲区一样,数据块缓冲区必须需要管理才能被高效利用。Linux使用bdflush内核守护进程来管理缓冲区。

bdflush内核守护进程

bdflush是一个简单的内核守护进程,当系统中有太多标记为dirty的数据块时bdflush负责将数据写回磁盘。 它在系统启动时内核线程起来后开始运行。大多数时间这个进程在睡眠等待系统中出现足够多的dirty数据块。 当数据块在分配或者去除的时候会检查dirty数据块的数量。如果系统中的dirty数据块相对于总的数据块数量占比太大则会唤醒bdflush。 默认的百分比为60%,但是如果系统紧缺数据块则bdflush会被立即唤醒。这个值可以使用update命令查看和更改:

# 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).

所有dirty数据块都存放在BUF_DIRTYLRU链表中,无论数据块何时被标记为dirtybdflush会试图将大量dirty数据块写入磁盘。这个数量默认为500,也可以使用update命令查看和更改。

update命令

update不仅仅是一个命令,它也是一个内核守护进程。当在超级用户下运行时它会定期清理所有旧的dirty数据块。 他通过系统服务函数来实现这个操作。

update或多或少的和bdflush有功能上的重复。dirty数据块会被打上一个系统时间的标签,标记何时被写入磁盘。 update每次运行的时候都会检查每个dirty数据块的时间标签,如果时间到了就将此数据块写入磁盘。

proc文件系统

/proc展示了Linux虚拟文件系统的强大之处。它不是实际存在的,就连/proc目录和其中的子目录都是虚拟的。 因此cat /proc/devices命令到底是怎样实现的了? /proc文件系统和实际文件系统一样将它自己注册到虚拟文件系统中去。当VFS请求打开其中的目录和文件时, /proc文件系统在内核中实时创建目录和文件信息。例如,/proc/devices文件产生于描述此设备的内核数据结构。

/proc文件系统提供了一个窗口,给用户查看内核内部的工作情况。Linux的其他子系统例如内核模块会在/proc中创建目录。

设备文件

Linux和所有类Unix系统一样将硬件设备抽象为一个特殊文件。例如/dev/null是null设备。设备文件不占用文件系统的任何空间, 它只是访问设备驱动的一个接口。EXT2文件系统和VFS都将设备文件作为一类特殊类型的节点。 设备文件有两种类型,字符和块设备文件。内核将设备驱动加上文件属性:你可以打开或者关闭它。 字符设备可以使用字符模式操作I/O,块设备通过缓冲区请求I/O。当设备文件产生一个I/O请求,会在系统内部传递给对应的设备驱动。 通常它不是实际的设备驱动而是某个子系统的伪设备驱动,例如SCSI设备驱动层。 设备文件通过主设备号区分设备类型,用次设备号标记主设备类型的一个实例或者一个单元。 例如,IDE控制器中的第一个IDE磁盘的主设备号是3,磁盘中第一个分区的次设备号为1。所以执行命令:ls -l of /dev/hda1

$ brw-rw----   1 root    disk       3,    1  Nov 24  15:09 /dev/hda1

在内核内部每个设备都使用kdev_t数据类型唯一标记,这个数据类型占两个字节,第一个字节保存主设备号,第二个字节保存次设备号。

所以上面的IDE设备在内核中保存的值为0x0301。EXT2节点在第一个块指针中保存设备的主次设备号来表示一个块或者字符设备。 当VFS读取此节点时,VFS数据结构中的i_rdev字段被设置为正确的设备标识符。

参考

ext2文件系统解构探析

自己实现的一个简单的ramfs文件系统


下一篇 HTTP协议学习

Comments

Content