深入传统文件系统
——摘自how linux works,《精通linux》中文第二版4.5章。
传统的Unix文件系统有两个基础组件:一个用来存储数据的数据块池,和一个用来管理数据池的数据库系统。这个数据库是inode数据结构的核心。inode是一组描述文件的数据,包括文件类型,权限,最重要的是文件数据所在的数据池。inodes在inode表中以数字的形式表示。
文件名和目录也是通过inodes来实现的。目录inode包含一个文件名列表以及对应的指向其他inodes的链接。
为了方便举例,我们来创建一个新的文件系统,挂载它,并切换到挂载点目录。然后加入一些文件和目录(你可以在一个闪存盘上来做实验):
$ mkdir dir_1
$ mkdir dir_2
$ echo a > dir_1/file_1
$ echo b > dir_1/file_2
$ echo c > dir_1/file_3
$ echo d > dir_2/file_4
$ ln dir_1/file_3 dir_2/file_5
这里我们创建了一个硬链接(hard link)dir_2/file_5,指向dir_1/file_3,它们实际上代表的是同一个文件(稍后详述)。
从用户角度而言,该文件系统的目录结构如图4-4所示。而图4-5更为复杂,显示的是真实的文件系统结构。
我们如何来理解这个图呢?对ext2/3/4文件系统来说,编号为2的inode是根(root inode),它是一个目录inode(dir),如果跟随箭头到数据池,我们可以看到跟目录的内容:dir_1和dir_2两个条目分别对应inode12和7633。我们也可以回到inode表查看这两个inode的详细内容。
内核采取以下步骤来对dir_1/file_2做检查:
- 检查路径部分,即目录dir_1和后面的file_2。
- 通过根inode找到它的目录信息。
- 在inode 2的目录信息中找到dir_1,它指向inode 12。
- 在inode表中查找inode 12,验证它是一个目录。
- 找到inode 12的目录信息(在数据池中)。
- 在inode 12的目录信息中找到file_2,它指向inode 14。
- 在目录表中查找inode 14,它是一个文件inode。
至此内核了解了该文件的属性,可以通过inode 14的数据链接打开它了。通过这种方式,inode指向目录数据结构,目录数据结构也指向inode,这样你可以根据自己的习惯创建文件系统结构。另外请注意目录inode中包含了.和..两个条目(除根目录以外),让你能够轻松地在目录结构中浏览。
4.5.1 查看inode细节
我们可以使用命令ls -i来查看目录的inode编号。例如上例中的目录inode编号如下(可以使用stat命令来查看更详细的信息):
$ ls -i
12 dir_1 7633 dir_2
你可能在ls -l命令的结果中见过但忽略了链接计数(link count)这个信息,图4-5 中文件的链接计数是多少呢,特别是硬链接file_5?链接计数是指向同一个inode的所有目录条目的总数。大多数文件的链接计数是1,因为它们大多在目录条目里只出现一次。这不奇怪,因为通常当你创建一个新文件的时候,你只为其创建一个新的目录条目和一个新的inode。然而inode 15出现了两次:一次是dir_1/file_3,另一次是dir_2/file_5。硬链接是手工创建的、指向一个已有的inode的目录条目。使用ln命令(不带-s选项)可以创建新链接。
这就是为什么我们有时候将删除文件称为取消链接(unlinking)。如果你运行rm dir_1/file_2,内核会在inode 12的目录条目中搜索名为file_2的条目。当发现file_2对应inode 14的时候,内核删除目录条目,同时将inode 14的链接计数减1。这导致inode 14的链接计数为0,内核发现该inode没有任何链接的时候,会将其和与之相关的所有数据删除。
但是如果你运行rm dir_1/file_3,inode 15的链接计数会由2变为1(dir_2/file_5仍然与之链接),这时内核不会删除此inode。
链接计数对于目录来说也是同理。inode 12的链接计数为2,一个是目录条目中的dir_1(inode 2),另一个是它自己的目录条目中的自引用(.)。如果你创建一个新目录dir_1/dir_3,inode 12的链接计数会变为3,因为新目录包含其上级目录(..)条目,链接到inode 12。类似inode 12指向其上级目录inode 2。
有一种情况比较特殊,根目录(root)的inode 2的链接计数为4。而图4-5中只显示了3个目录条目链接。另外一个其实是文件系统的超级块(superblock),它知道如何找到根inode。
你完全可以自己做一些尝试,使用ls -i创建文件系统,使用stat来遍历,这些操作都很安全。你也不需要使用root权限(除非你需要挂载和创建新的文件系统)。
有一个地方我们还没有讲到,就是在为新文件分配数据池块的时候,文件系统如何知道哪些块可用哪些已被占用?方法之一是使用块位图(block bitmap)来管理块信息。文件系统保留了一些字节空间,每一位(bit)代表一个数据池中的一个块。0代表块可用,1代表块已经被占用,释放和分配块就变成了0和1之间的切换。
当inode表中的数据和块分配数据不匹配,或者由于你没有正确关闭系统导致链接数目不正确,这些都会导致文件系统出错。所以你在检查文件系统的时候,如4.1.11 检查和修复文件系统一节介绍的那样,fsck会遍历inode表和目录结构来生成链接数目和块分配信息,并且会根据磁盘上的文件系统来检查新数据,如果发现数据不匹配的情况,fsck会修复链接数错误,以及inode和其他一些目录结构数据错误。大部分fsck程序会将新创建的文件放到lost+found目录。
4.5.2 在用户空间中使用文件系统
在用户空间中使用文件和目录时,你不需要太关心底层实现的细节。你只需要能够通过内核系统调用来访问文件和目录的内容。其实你也能够看到一些看似超出用户空间范围的文件系统信息,特别是stat()这个系统调用能够告诉你inode数目和链接计数。
除非你需要维护文件系统,否则你不需要知道inode数目和链接计数,用户模式中的程序之所以能够访问这些信息,主要是因为一些向后兼容的考虑。并且也不是所有Linux的文件系统都提供这些信息。虚拟文件系统(Virtual File System,VFS)接口层能够确保系统调用总是返回inode数目和链接计数,不过这些数据可能没有太大意义。
在传统文件系统上你有可能无法执行一些传统的Unix文件系统的操作。比如,你无法使用ln命令在VFAT文件系统上创建硬链接,因为其目录条目数据结构根本不同。
幸运的是Unix/Linux提供给用户空间的系统调用为用户访问文件提供了足够的抽象和便利,用户不需要关心任何底层的细节。文件命名很灵活,支持大小写混合,并且能很容易地支持其他文件系统结构。
请记住,内核只是作为系统调用的通道,而不会包含对某个特定的文件系统的支持。
4.5.3 文件系统的演进
你已经看到了,就算一个很简单的文件系统也包括各种各样不同的组件,需要去维护。同时随着新需求、新技术、和存储容量的不断发展,对文件系统的要求也越来越高。如今,性能、数据完整性、安全性等方面的需求大大超出了老式文件系统的功能范围,因而文件系统方面的技术也在日新月异地发展。一个例子就是我们在4.2.1 文件系统类型中提到的Btrfs。
文件系统演进的另一个例子是新的文件系统使用不同的数据结构来表示文件和目录,它们使用数据块的方式各不相同,而不是使用本章介绍的inode。此外针对SSD优化的文件系统也在不断演进,无论怎么变化,归根结底它们的最终目的都是一样的。