本文目录:
4.1 文件系统的组成部分
4.2 文件系统的完整结构
4.3 Data Block
4.4 inode基础知识
4.5 inode深入
4.6 单文件系统中文件操作的原理
4.7 多文件系统关联
4.8 ext3文件系统的日志功能
4.9 ext4文件系统
4.10 ext类文件系统的缺点
4.11 虚拟文件系统VFS
将磁盘进行分区,分区是将磁盘按柱面进行物理上的划分。划分好分区后还要进行格式化,然后再挂载才能使用(不考虑其他方法)。格式化分区的过程其实就是创建文件系统。
文件系统的类型有很多种,如CentOS 5和CentOS 6上默认使用的ext2/ext3/ext4,CentOS 7上默认使用的xfs,windows上的NTFS,光盘类的文件系统ISO9660,MAC上的混合文件系统HFS,网络文件系统NFS,Oracle研发的btrfs,还有老式的FAT/FAT32等。
本文将非常全面且详细地对ext家族的文件系统进行介绍。有ext2/ext3/ext4,ext3是有日志的ext2改进版,ext4对相比ext3做了非常多的改进。虽然xfs/btrfs等文件系统有所不同,但它们只是在实现方式上不太同,再加上属于自己的特性而已。
4.1 文件系统的组成部分
4.1.1 block的出现
硬盘的读写IO一次是一个扇区512字节,如果要读写大量文件,以扇区为单位肯定很慢很消耗性能,所以Linux中通过文件系统控制使用”块”为读写单元。现在的文件系统上,块的大小一般为1024bytes(1K)或2048bytes(2K)或4096bytes(4K)。比如需要读一个或多个块时,文件系统的IO管理器通知磁盘控制器要读取哪些块的数据,硬盘控制器将这些块按扇区读取出来,再通过硬盘控制器将这些扇区数据重组返回给计算机。
block的出现使得在文件系统层面上读写性能大大提高,也大量减少了碎片。但是它的副作用是可能造成空间浪费。由于文件系统以block为读写单元,即使存储的文件只有1K大小也将占用一个block,剩余的空间完全是浪费的。在某些业务需求下可能大量存储小文件,这会浪费大量的空间。
尽管有缺点,但是其优点足够明显,在当下硬盘容量廉价且追求性能的时代,使用block是一定的。
4.1.2 inode的出现
如果存储的1个文件占用了大量的block读取时会如何?假如block大小为1KB,仅仅存储一个10M的文件就需要10240个block,而且这些blocks很可能在位置上是不连续在一起的(不相邻),读取该文件时难道要从前向后扫描整个文件系统的块,然后找出属于该文件的块吗?显然是不应该这么做的,因为太慢太傻瓜式了。再考虑一下,读取一个只占用1个block的文件,难道只读取一个block就结束了吗?并不是,仍然是扫描整个文件系统的所有block,因为它不知道什么时候扫描到,扫描到了它也不知道这个文件是不是已经完整而不需要再扫描其他的block。
另外,每个文件都有属性(如权限、大小、时间戳等),这些属性类的元数据存储在哪里呢?难道也和文件的数据部分存储在块中吗?如果一个文件占用多个block那是不是每个属于该文件的block都要存储一份文件元数据?但是如果不在每个block中存储元数据文件系统又怎么知道某一个block是不是属于该文件呢?但是显然,每个数据block中都存储一份元数据太浪费空间。
文件系统设计者当然知道这样的存储方式很不理想,所以需要优化存储方式。如何优化?对于这种类似的问题的解决方法是使用索引,通过扫描索引找到对应的数据,而且索引可以存储部分数据。
在文件系统上索引技术具体化为索引节点(index node),在索引节点上存储的部分数据即为文件的属性元数据及其他少量信息。一般来说索引占用的空间相比其索引的文件数据而言占用的空间就小得多,扫描它比扫描整个数据要快得多,否则索引就没有存在的意义。这样一来就解决了前面所有的问题。
在文件系统上的术语中,索引节点称为inode。在inode中存储了inode号、文件类型、权限、文件所有者、大小、时间戳等元数据信息,最重要的是还存储了指向属于该文件block的指针,这样读取inode就可以找到属于该文件的block,进而读取这些block并获得该文件的数据。由于后面还会介绍一种指针,为了方便称呼和区分,暂且将这个inode记录中指向文件data block的指针称之为block指针,。
一般inode大小为128字节或256字节,相比那些MB或GB计算的文件数据而言小得多的多,但也要知道可能一个文件大小小于inode大小,例如只占用1个字节的文件。
4.1.3 bmap出现
在向硬盘存储数据时,文件系统需要知道哪些块是空闲的,哪些块是已经占用了的。最笨的方法当然是从前向后扫描,遇到空闲块就存储一部分,继续扫描直到存储完所有数据。
优化的方法当然也可以考虑使用索引,但是仅仅1G的文件系统就有1KB的block共1024*1024=1048576个,这仅仅只是1G,如果是100G、500G甚至更大呢,仅仅使用索引索引的数量和空间占用也将极大,这时就出现更高一级的优化方法:使用块位图(bitmap简称bmap)。
位图只使用0和1标识对应block是空闲还是被占用,0和1在位图中的位置和block的位置一一对应,第一位标识第一个块,第二个位标识第二个块,依次下去直到标记完所有的block。
考虑下为什么块位图更优化。在位图中1个字节8个位,可以标识8个block。对于一个block大小为1KB、容量为1G的文件系统而言,block数量有1024*1024个,所以在位图中使用1024*1024个位共1024*1024/8=131072字节=128K,即1G的文件只需要128个block做位图就能完成一一对应。通过扫描这100多个block就能知道哪些block是空闲的,速度提高了非常多。
但是要注意,bmap的优化针对的是写优化,因为只有写才需要找到空闲block并分配空闲block。对于读而言,只要通过inode找到了block的位置,cpu就能迅速计算出block在物理磁盘上的地址,cpu的计算速度是极快的,计算block地址的时间几乎可以忽略,那么读速度基本认为是受硬盘本身性能的影响而与文件系统无关了。
虽然bmap已经极大的优化了扫描,但是仍有其瓶颈:如果文件系统是100G呢?100G的文件系统要使用128*100=12800个1KB大小的block,这就占用了12.5M的空间了。试想完全扫描12800个很可能不连续的block这也是需要占用一些时间的,虽然快但是扛不住每次存储文件都要扫描带来的巨大开销。
所以需要再次优化,如何优化?简而言之就是将文件系统划分开形成块组,至于块组的介绍放在后文。
4.1.4 inode表的出现
回顾下inode相关信息:inode存储了inode号、文件属性元数据、指向文件占用的block的指针;每一个inode占用128字节或256字节。
现在又出现问题了,一个文件系统中可以说有无数多个文件,每一个文件都对应一个inode,难道每一个仅128字节的inode都要单独占用一个block进行存储吗?这太浪费空间了。
所以更优的方法是将多个inode合并存储在block中,对于128字节的inode,一个block存储8个inode,对于256字节的inode,一个block存储4个inode。这就使得每个存储inode的块都不浪费。
在ext文件系统上,将这些物理上存储inode的block组合起来,在逻辑上形成一张inode表(inode table)来记录所有的inode。
举个例子,每一个家庭都要向派出所登记户口信息,通过户口本可以知道家庭住址,而每个镇或街道的派出所将本镇或本街道的所有户口整合在一起,要查找某一户地址时,在派出所就能快速查找到。inode table就是这里的派出所。它的内容如下图所示。
实际上,在文件系统创建完成后所有的inode号都已经分配好并记录到inode table中了,只不过被使用的inode号所在的行还有文件属性的元数据信息和block位置信息,而未被使用的inode号只有一个inode号而已而没有其他信息而已。
再细细一思考,就能发现一个大的文件系统仍将占用大量的块来存储inode,想要找到其中的一个inode记录也需要不小的开销,尽管它们已经形成了一张逻辑上的表,但扛不住表太大记录太多。那么如何快速找到inode,这同样是需要优化的,优化的方法是将文件系统的block进行分组划分,每个组中都存有本组inode table范围、bmap等。
4.1.5 imap的出现
前面说bmap是块位图,用于标识文件系统中哪些block是空闲哪些block是占用的。
对于inode也一样,在存储文件(Linux中一切皆文件)时需要为其分配一个inode号。但是在格式化创建文件系统后所有的inode号都是被事先设定好存放在inode table中的,因此产生了问题:要为文件分配哪一个inode号呢?又如何知道某一个inode号是否已经被分配了呢?
既然是”是否被占用”的问题,使用位图是最佳方案,像bmap记录block的占用情况一样。标识inode号是否被分配的位图称为inodemap简称为imap。这时要为一个文件分配inode号只需扫描imap即可知道哪一个inode号是空闲的。
imap存在着和bmap和inode table一样需要解决的问题:如果文件系统比较大,imap本身就会很大,每次存储文件都要进行扫描,回导致效率不够高。同样,优化的方式是将文件系统占用的block划分成块组,每个块组有自己的imap范围。
4.1.6 块组的出现
前面一直提到的优化方法是将文件系统占用的block划分成块组(block group),解决bmap、inode table和imap太大的问题。
在物理层面上的划分是将磁盘按柱面划分为多个分区,即多个文件系统;在逻辑层面上的划分是将文件系统划分成块组。每个文件系统包含多个块组,每个块组包含多个元数据区和数据区:元数据区就是存储bmap、inode table、imap等的数据;数据区就是存储文件数据的区域。注意块组是逻辑层面的概念,所以并不会真的在磁盘上按柱、按扇区、按磁道等概念进行划分。
4.1.7 块组的划分
块组在文件系统创建完成后就已经划分完成了,也就是说元数据区bmap、inode table和imap等信息占用的block以及数据区占用的block都已经划分好了。那么文件系统如何知道一个块组元数据区包含多少个block,数据区又包含多少block呢?
它只需确定一个数据——每个block的大小,再根据bmap至多只能占用一个完整的block的标准就能计算出块组如何划分。如果文件系统非常小,所有的bmap总共都不能占用完一个block,那么也只能空闲bmap的block了。
每个block的大小在创建文件系统时可以人为指定,不指定也有默认值。
假如现在block的大小是1KB,一个bmap完整占用一个block能标识1024*8= 8192个block(当然这8192个block是数据区和元数据区共8192个,因为元数据区分配的block也需要通过bmap来标识)。每个block是1K,每个块组是8192K即8M,创建1G的文件系统需要划分1024/8=128个块组,如果是1.1G的文件系统呢?128+12.8=128+13=141个块组。
每个组的block数目是划分好了,但是每个组设定多少个inode号呢?inode table占用多少block呢?这需要由系统决定了,因为描述”每多少个数据区的block就为其分配一个inode号”的指标默认是我们不知道的,当然创建文件系统时也可以人为指定这个指标或者百分比例。见后文”inode深入”。
使用dumpe2fs可以将ext类的文件系统信息全部显示出来,当然bmap是每个块组固定一个block的不用显示,imap比bmap更小所以也只占用1个block不用显示。
下图是一个文件系统的部分信息,在这些信息的后面还有每个块组的信息。
从这张表中能计算出文件系统的大小,该文件系统共4667136个blocks,每个block大小为4K,所以文件系统大小为4667136*4/1024/1024=17.8GB。
也能计算出分了多少个块组,因为每一个块组的block数量为32768,所以块组的数量为4667136/32768=142.4即143个块组。由于块组从0开始编号,所以最后一个块组编号为Group 142。如下图所示是最后一个块组的信息。
4.2 文件系统的完整结构
将上文描述的bmap、inode table、imap、数据区的blocks和块组的概念组合起来就形成了一个文件系统,当然这还不是完整的文件系统。完整的文件系统如下图。
首先,该图中多了Boot Block、Super Block、GDT、Reserver GDT这几个概念。下面会分别介绍它们。
然后,图中指明了块组中每个部分占用的block数量,除了superblock、bmap、imap能确定占用1个block,其他的部分都不能确定占用几个block。
最后,图中指明了Superblock、GDT和Reserved GDT是同时出现且不一定存在于每一个块组中的,也指明了bmap、imap、inode table和data blocks是每个块组都有的。
4.2.1 引导块
即上图中的Boot Block部分,也称为boot sector。它位于分区上的第一个块,占用1024字节,并非所有分区都有这个boot sector。里面存放的也是boot loader,这段boot loader成为VBR,这里的Boot loader和mbr上的boot loader是存在交错关系的。开机启动的时候,首先加载mbr中的bootloader,然后定位到操作系统所在分区的boot serctor上加载此处的boot loader。如果是多系统,加载mbr中的bootloader后会列出操作系统菜单,菜单上的各操作系统指向它们所在分区的boot sector上。它们之间的关系如下图所示。
4.2.2 超级块(superblock)
既然一个文件系统会分多个块组,那么文件系统怎么知道分了多少个块组呢?每个块组又有多少block多少inode号等等信息呢?还有,文件系统本身的属性信息如各种时间戳、block总数量和空闲数量、inode总数量和空闲数量、当前文件系统是否正常、什么时候需要自检等等,它们又存储在哪里呢?
毫无疑问,这些信息必须要存储在block中。存储这些信息占用1024KB,所以也要一个block,这个block称为超级块(superblock),它的block号可能为0也可能为1。如果block大小为1024K,则引导块正好占用一个block,这个block号为0,所以superblock的号为1;如果block大小大于1024K,则引导块和超级块同置在一个block中,这个block号为0。总之superblock的起止位置是第二个1024(1024-2047)字节。
使用df命令读取的就是每个文件系统的superblock,所以它的统计速度非常快。相反,用du命令查看一个较大目录的已用空间就非常慢,因为不可避免地要遍历整个目录的所有文件。
[root@xuexi ~]# df -hT Filesystem Type Size Used Avail Use% Mounted on/dev/sda3 ext4 18G 1.7G 15G 11% /tmpfs tmpfs 491M 0 491M 0% /dev/shm/dev/sda1 ext4 190M 32M 149M 18% /boot
superblock对于文件系统而言是至关重要的,超级块丢失或损坏必将导致文件系统的损坏。所以旧式的文件系统将超级块备份到每一个块组中,但是这又有所空间浪费,所以ext2文件系统只在块组0、1和3、5、7幂次方的块组中保存超级块的信息,如Group9、Group25等。尽管保存了这么多的superblock,但是文件系统只使用第一个块组即Group0中超级块信息来获取文件系统属性,只有当Group0上的superblock损坏或丢失才会找下一个备份超级块复制到Group0中来恢复文件系统。
下图是一个ext4文件系统的superblock的信息,ext家族的文件系统都能使用dumpe2fs -h获取。
4.2.3 块组描述符表(GDT)
既然文件系统划分了块组,那么每个块组的信息和属性元数据又保存在哪里呢?
ext文件系统每一个块组信息使用32字节描述,这32个字节称为块组描述符,所有块组的块组描述符组成块组描述符表GDT(group descriptor table)。
虽然每个块组都需要块组描述符来记录块组的信息和属性元数据,但是不是每个块组中都存放了块组描述符。ext文件系统的存储方式是:将它们组成一个GDT,并将该GDT存放于某些块组中,存放GDT的块组和存放superblock和备份superblock的块相同,也就是说它们是同时出现在某一个块组中的。
假如block大小为4KB的文件系统划分了143个块组,每个块组描述符32字节,那么GDT就需要143*32=4576字节即两个block来存放。这两个GDT block中记录了所有块组的块组信息,且存放GDT的块组中的GDT都是完全相同的。
下图是一个块组描述符的信息(通过dumpe2fs获取)。
4.2.4 保留GDT(Reserved GDT)
保留GDT用于以后扩容文件系统使用,防止扩容后块组太多,使得块组描述符超出当前存储GDT的blocks。保留GDT和GDT总是同时出现,当然也就和superblock同时出现了。
例如前面143个块组使用了2个block来存放GDT,但是此时第二个block还空余很多空间,当扩容到一定程度时2个block已经无法再记录块组描述符了,这时就需要分配一个或多个Reserverd GDT的block来存放超出的块组描述符。
由于新增加了GDT block,所以应该让每一个保存GDT的块组都同时增加这一个GDT block,所以将保留GDT和GDT存放在同一个块组中可以直接将保留GDT变换为GDT而无需使用低效的复制手段备份到每个存放GDT的块组。
同理,新增加了GDT需要修改每个块组中superblock中的文件系统属性,所以将superblock和Reserverd GDT/GDT放在一起又能提升效率。
4.3 Data Block
如上图,除了Data Blocks其他的部分都解释过了。data block是直接存储数据的block,但事实上并非如此简单。
数据所占用的block由文件对应inode记录中的block指针找到,不同的文件类型,数据block中存储的内容是不一样的。以下是Linux中不同类型文件的存储方式。
-
对于常规文件,文件的数据正常存储在数据块中。
-
对于目录,该目录下的所有文件和一级子目录的目录名存储在数据块中。
文件名不是存储在其自身的inode中,而是存储在其所在目录的data block中。
-
对于符号链接,如果目标路径名较短则直接保存在inode中以便更快地查找,如果目标路径名较长则分配一个数据块来保存。
-
设备文件、FIFO和socket等特殊文件没有数据块,设备文件的主设备号和次设备号保存在inode中。
常规文件的存储就不解释了,下面分别解释特殊文件的存储方式。
4.3.1 目录文件的data block
对于目录文件,其inode记录中存储的是目录的inode号、目录的属性元数据和目录文件的block指针,这里面没有存储目录自身文件名的信息。
而其data block的存储方式则如下图所示。
由图可知,在目录文件的数据块中存储了其下的文件名、目录名、目录本身的相对名称”.”和上级目录的相对名称”..”,还存储了指向inode table中这些文件名对应的inode号的指针(并非直接存储inode号码)、目录项长度rec_len、文件名长度name_len和文件类型file_type。注意到除了文件本身的inode记录了文件类型,其所在的目录的数据块也记录了文件类型。由于rec_len只能是4的倍数,所以需要使用”