9Ext文件系统族

9Ext文件系统族

9.1简介

  • Ext2文件系统:Ext2文件系统的设计利用了与虚拟文件系统非常类似的结构,因为开发Ext2时,目标就是要优化与Linux的互操作,但它也可以用于其他的操作系统。
  • Ext3文件系统:这是Ext2的升级版。增加了扩展日志功能,用于系统崩溃时恢复系统

文件系统目标:

  1. 减少碎片
  2. 有效利用存储空间,磁盘上存着很多磁盘空间的管理结构,为减少管理结构占用的空间比例,增加实际存储数据的空间比例.通常会引入由管理员配置的参数,以便针对预期的使用模式来优化文件系统(例如,预期使用大量的大文件或小文件)。
  3. 维护文件内容的一致性,系统突然断点,数据来不及写回磁盘,文件系统的实现必须尽可能快速、全面地纠正可能出现的损坏。在最低限度上,它必须能够将文件系统还原到一个可用状态。
  4. 保证读写速度

9.2Ext2文件系统

Linux早期是在教育版Minix环境下开发的,因此Linux内核处理的第一个文件系统是Minix文件系统的直接改编版
Minix文件系统的代码也许从教育的角度来看很有价值,但从性能来看,它还有很多有待改进之处.商业UNIX系统的许多标准特性,Minix文件系统根本不支持
这促进了Ext文件系统的开发,该文件系统尽管在Minix文件系统上进行了很大的改进,但与商业文件系统的性能和功能比较,仍然有很明显的不足
直到Ext的第二个版本Ext2开发之后,才成为一个极其强大的文件系统,从此不再害怕与商业产品比较了.其设计主要受到BSD的快速文件系统(Fast File System,简称FFS)的影响,具体请参见[MBKQ96]
Ext2文件系统专注于高性能,以及下面列出和由文件系统作者在[CTT]中规定的目标:

  1. 支持可变块长,使得文件系统能够处理预期的应用(许多大文件或许多小文件)
  2. 快速符号链接,如果链接目标的路径足够短,则将其存储在inode自身中(不是存储在数据区中)
  3. 将扩展能力集成到设计中,使得从旧版本迁移到新版本时,无需重新格式化和重新加载硬盘
  4. 在存储介质上操作数据时采用了一种精巧复杂的策略,使得系统崩溃可能造成的影响最小。文件系统通常可以恢复到一种状态,在该状态下辅助工具fsck至少能够修复它,使得文件系统能够再次使用。这并不排除数据丢失的可能性
  5. 使用特殊的属性(经典的UNIX文件系统不具备该特性)将文件标记为不可改变的。例如,这可以防止对重要配置文件的无意修改,即使超级用户也不行

与更为现代的文件系统相比,Ext2文件系统的代码非常紧凑。与JFS的超过30 000行代码、XFS的大约90 000行代码相比,Ext2用不超过10 000行代码就足以实现

9.2.1物理结构

必须建立各种结构(在内核中定义为C语言数据类型),来存放文件系统的数据,包括文件内容、目录层次结构的表示、相关的管理数据(如访问权限或与用户和组的关联),以及用于管理文件系统内部信息的元数据。同一数据结构通常会有两个版本,一个在内存中,一个在磁盘上

名词块(block)有两个不同的含义:

  1. 一方面,有些文件系统存储在面向块的设备上,与设备之间的数据传输都以块为单位进行,不会传输单个字符
  2. 另一方面,Ext2文件系统是一种基于块的文件系统,它将硬盘划分为若干块,每个块的长度都相同,按块管理元数据和文件内容。这意味着底层存储介质的结构影响到了文件系统的结构,这很自然也会影响到所用的算法和数据结构的设计

在将硬盘划分为固定长度的块时,特别重要的一个方面是文件占用的存储空间只能是块长度的整数倍。我们根据图9-1来讲解这些情况的影响,为简单起见,我们假定块长为5个单位。我们需要存储3个文件,长度分别为2、4和11个单位

在这里插入图片描述

上图中上半部存储效率更高但是没使用这种方法,因为用于管理块内部文件边界的数据量太大了,抵消了存储效率的好处,最后使用了下半部的方法

  1. 结构概观

    下图为出了一个块组(block group)的内容,块组是Ext2文件系统的核心要素

    在这里插入图片描述

    块组是该文件系统的基本成分,容纳了文件系统的其他结构。每个文件系统都由大量块组组成(每个块组的第一个块中存着超级块信息),在硬盘上相继排布如下图

    在这里插入图片描述

    启动扇区是硬盘上第一个区域,在系统加电启动时,其内容由BIOS自动装载并执行。它包含一个启动装载程序(bootloader),用于从计算机安装的操作系统中选择一个启动。启动装载程序并非在所有系统上都是必需的。在需要启动装载程序的系统上,它们通常位于硬盘的起始处,以避免影响其后的分区。

    磁盘上剩余的空间由连续的许多块组占用,存储了文件系统元数据和各个文件的有用数据。图9-2清楚地说明了每个块组包含许多冗余信息。为什么Ext2文件系统允许这样浪费空间?有两个原因,可以证明提供额外空间的做法是正确的。

    1. 如果系统崩溃破坏了超级块,有关文件系统结构和内容的所有信息都会丢失。如果有冗余的副本,该信息是可能恢复的(难度极高,大多数用户可能一点也恢复不了)。
    2. 通过使文件和管理数据尽可能接近,减少了磁头寻道和旋转,这可以提高文件系统的性能

    数据并非在每个块组中都复制,内核也只用超级块的第一个副本工作,通常这就足够了。在进行文件系统检查时,会将第一个超级块的数据传播到剩余的超级块,供紧急情况下读取。因为该方法也会消耗大量的存储空间,Ext2的后续版本采用了稀疏超级块(sparse superblock)技术。该做法中,超级块不再存储到文件系统的每个块组中,而是只写入到块组0、块组1和其他ID可以表示为3、5、 7的幂的块组中。

    尽管在设计Ext2文件系统时假定上述两个问题对文件系统的性能和安全有很大影响,但后来发现情况不是这样。因此做出了上述修改

    块组中各个结构的简要概述

    1. 超级块是用于存储文件系统自身元数据的核心结构。其中的信息包括空闲与已使用块的数目、块长度、当前文件系统状态(在启动时用于检测前一次崩溃)、各种时间戳(例如,上一次装载文件系统的时间以及上一次写入操作的时间)。它还包括一个表示文件系统类型的魔数,这样mount例程能够确认文件系统的类型是否正确。内核只使用第一个块组的超级块读取文件系统的元信息,即使在几个超级块中都有超级块,也是如此。
    2. 组描述符包含的信息反映了文件系统中各个块组的状态,例如,块组中空闲块和inode的数目。每个块组都包含了文件系统中所有块组的组描述符信息。
    3. 数据块位图和inode位图用于保存长的比特位串。这些结构中的每个比特位都对应于一个数据块或inode,用于表示对应的数据块或inode是空闲的,还是被使用中
    4. inode表包含了块组中所有的inode,
    5. inode用于保存文件系统中与各个文件和目录相关的所有元数据,包括文件属性和文件内容所在的几个块号。
    6. 数据块部分包含了文件系统中的文件的有用数据

    尽管inode位图和数据块位图总是占用一个数据块,其余的数据项都由几个块组成。但实际使用的块数不仅依赖创建文件系统时的选项,也依赖存储介质的容量

    这些结构与虚拟文件系统各要素的相似性是无可怀疑的。尽管采用这种结构解决了许多问题,例如目录的表示,但Ext2文件系统仍然需要解决几个棘手的问题。
    如:各个文件之间的差别可能非常大,无论是长度还是用途。尽管多媒体内容(例如视频)或大型数据库很容易消耗数百兆甚至上吉字节,小的配置文件通常字节数很少。还有不同类型的元信息。例如,为设备文件存储的信息,与目录、普通文件、已命名管道都是不同的

    在设计用于存储数据的结构时,必须最优地满足所有的文件系统需求,对硬盘来说这未必是容易事,特别是在考虑到介质容量的利用和访问速度时。Ext2文件系统因此借助于技巧来解决,如下

  2. 间接块(用于保存大文件)

    即使Ext2文件系统采用了经典的UNIX方案,借助于inode来实现文件,但仍然有一些问题需要解决。硬盘划分为块,由文件使用。特定文件占用的块数目,取决于文件内容的长度(当然,也与块长度本身有关系)
    在系统内存中,从内核的角度来看,内存划分为长度相同的页,按唯一的页号或指针寻址。硬盘与内存类似,块也通过编号唯一标识。这使得存储在inode结构中的文件元数据,能够关联到位于硬盘数据块部分的文件内容。二者之间的关联,是通过将数据块的地址存储在inode中建立的
    文件占用的数据块不见得是连续的(虽然出于性能考虑,连续数据块是我们想要的一种情况),也可能散布到整个硬盘上
    问题:增加inode中块号数目不能解决问题,如存700M的文件,1个数据块4K,要用175000个块,用4字节表示一个块,要用175000x4的空间
    解决:用一个块来存放这些块的索引,称为间接块

    在inode中存储少量块号,用于小文件。对较大的文件,指向各个数据块的指针(块号)是间接存储的,如下图

    在这里插入图片描述

    这种方法容许对大/小文件的灵活存储,因为用于存储块号的区域的长度,将随文件实际长度的变化而动态变化,即前者实际上是后者的一个函数。inode本身长度是固定的,用于间接的其他数据块是动态分配的

    小文件块号直接存在inode中,大文件使用间接.文件系统在硬盘上分配一个数据块,用于存储块号.该块称为一次间接块(single indirect block).inode存储第一个间接块的块号,该块号紧接着直接块的块号存储

    在这里插入图片描述

    该方法有一个负面效应,它使得对大文件的访问代价更高。文件系统首先必须查找间接块的地址,读取下一个间接项,查找对应的间接块,并从中查找数据块的块号。因而在管理可变长度的文件的能力方面,与访问速度相应的下降方面(越大的文件,速度越慢),必然存在一个折中。

    内核提供了3次间接表示真正的巨型文件.但这出现了一个问题,特别是在32位体系结构上。由于标准库使用32位宽的long类型变量表示文件内的位置,这将文件的最大长度限制到231字节,即2 GiB,小于Ext2文件系统使用三次间接所能管理的文件长度。为处理这个缺点,内核中引入了一个专门的方案来访问大文件。这不仅影响到标准库的例程,也影响到了内核源代码

  3. 碎片

    内存和磁盘存储管理在块结构方面的相似性,意味着它们也会有碎片问题.随着时间的变化,文件系统的许多文件从磁盘的随机位置删除,又增加了许多新文件。这使得空闲磁盘空间变成长度不同的存储区,因此碎片不可避免地出现了.如下图,与内存差别在于,没有自动的硬件机制来替文件系统保证线性化。文件系统自身的代码负责完成该任务
    在这里插入图片描述

    文件块在硬盘上都是连续的(这是我们希望的情况),磁头读取数据时的移动将降到最低,因而提高了数据传输速度。相反,如果文件的块散布到整个磁盘上,磁头读取数据时就需要不停地寻道,因而降低了访问速度。

9.2.2数据结构

针对硬盘存储定义的结构,都有针对内存访问定义的对应结构。这些与虚拟文件系统定义的结构协同使用,首先用来支持与文件系统的通信并简化重要数据的管理,其次用于缓存元数据,加速对文件系统的处理。

  1. 超级块
    记录此filesystem的整体信息,包括inode/block的总量、使用量、剩余量,以及文件系统的格式与相关信息等
    超级块的数据使用ext2_get_sb例程读取(位于fs/ext2/super.c),内核通常借助file_system_type结构中的get_sb函数指针来调用该函数。该例程执行的操作在后面讲。我们在这里关注的是超级块在硬盘上的结构和布局。使用得相对广泛的ext2_super_block结构用于定义超级块,如下所示:

    //include/linux/ext2_fs.h
    struct ext2_super_block {
        __le32 s_inodes_count; /* inode数目 */ 
        __le32 s_blocks_count; /* 块数目 */ 
        __le32 s_r_blocks_count; /* 已分配块的数目 */ 
        __le32 s_free_blocks_count; /* 空闲块数目 */ 
        __le32 s_free_inodes_count; /* 空闲inode数目 */ 
        __le32 s_first_data_block; /* 第一个数据块 */ 
        __le32 s_log_block_size; /* 块长度 */ //对应3个值(0,1,2),实际块大小=1024*2^s_log_block_size,对应1024,2048,4096.最小和最大的块长度当前分别限定为1 024和4 096,且由内核常数EXT2_MIN_BLOCK_SIZE和EXT2_MAX_BLOCK_SIZE定义,块长度在用mke2fs创建文件系统期间指定.创建后无法修改
        __le32 s_log_frag_size; /* 碎片长度*/ 
        __le32 s_blocks_per_group; /* 每个块组包含的块数 */ 
        __le32 s_frags_per_group; /* # 每个块组包含的碎片*/ 
        __le32 s_inodes_per_group; /* 每个块组的inode数目 */ 
        __le32 s_mtime; /* 装载时间 */ 
        __le32 s_wtime; /* 写入时间 */ 
        __le16 s_mnt_count; /* 装载计数 */ 
        __le16 s_max_mnt_count; /* 最大装载计数 */ 
        __le16 s_magic; /* 魔数,标记文件系统类型 */ 
        __le16 s_state; /* 文件系统状态 */ 
        __le16 s_errors; /* 检测到错误时的行为 */ 
        __le16 s_minor_rev_level; /* 副修订号 */ 
        __le32 s_lastcheck; /* 上一次检查的时间 */ 
        __le32 s_checkinterval; /* 两次检查允许间隔的最长时间 */ 
        __le32 s_creator_os; /* 创建文件系统的操作系统 */ 
        __le32 s_rev_level; /* 修订号 */ 
        __le16 s_def_resuid; /* 能够使用保留块的默认UID */ 
        __le16 s_def_resgid; /* 能够使用保留块的默认GID */ 
        /* 
        * 这些字段只用于EXT2_DYNAMIC_REV超级块。
        * 
        * 请注意,兼容特性集与不兼容特性集的差别在于:如果不兼容特性中某个置位的比特位内核不了解,
        * 则应该拒绝装载该文件系统。
        * 
        * e2fsck的要求更为严格。如果它不了解某个特性,不管是兼容特性还是不兼容特性,
        * 它都必须放弃工作,而不是去尽力去弄乱不了解的东西。
        */ 
        __le32 s_first_ino; /* 第一个非保留的inode */ 
        __le16 s_inode_size; /* inode结构的长度 */ 
        __le16 s_block_group_nr; /* 当前超级块所在的块组编号 */ 
        __le32 s_feature_compat; /* 兼容特性集 */ 
        __le32 s_feature_incompat; /* 不兼容特性集 */ 
        __le32 s_feature_ro_compat; /* 只读兼容特性集 */ 
        __u8 s_uuid[16]; /* 卷的128位uuid */ 
        char s_volume_name[16]; /* 卷名 */ 
        char s_last_mounted[64]; /* 上一次装载的目录 */ 
        __le32 s_algorithm_usage_bitmap; /* 用于压缩 */ 
        /* 
        * 性能提示。仅当设置了EXT2_COMPAT_PREALLOC标志时,才能进行目录的预分配。
        */ 
        __u8 s_prealloc_blocks; /* 试图预分配的块数*/ 
        __u8 s_prealloc_dir_blocks; /* 试图为目录预分配的块数 */ 
        __u16 s_padding1; 
        /* 
        * 如果设置了EXT3_FEATURE_COMPAT_HAS_JOURNAL,日志支持才是有效的。
        */ 
        ... 
        __u32 s_reserved[190]; /* 填充字节,补齐到块结尾 */ 
    };
    

    Ext2文件系统在硬盘上存储超级块结构的所有数值时,都采用小端序格式.在数据读入内存时,内核负责将这种格式转换为CPU的本机格式.用于在不同CPU类型之间转换字节序的例程,定义在byteorder/big_endian.h和byteorder/little_endian.h两个文件中.在IA-32和AMD64类型的CPU上无需转换。而在诸如Sparc之类的系统上,超出8个比特位的数据类型,都需要切换字节序

    超级块的长度总是1 024字节。这是通过在结构末尾增加一个填充成员来解决的(s_reserved)。

    1. 结构中s_r_blocks_count用于系统用户的根储备,在磁盘满时让root还有空间使用,防止磁盘满导致系统无法启动

    2. s_state、s_lastcheck和s_checkinterval:用于系统状态检查,分别表示:系统状态,上次检查时间,两次检查时间间隔

    3. s_mnt_count和s_max_mnt_count:上一次检查以来装载次数,两次检查之间最大装载次数,前者超过后者则发起一次检查

    4. s_feature_compat、s_feature_incompat和s_feature_ro_compat:专用于描述额外的特性,确保了能够比较容易地将新特性集成到旧的设计中,用位图表示,每位表示一种特性

      1. 兼容特性(Compatible Feature):由s_feature_compat指定,可以用于文件系统代码的新版本,对旧版本没有负面影响(或功能损伤)。此类增强的例子包括,Ext3 引入的日志特性,用ACL(访问控制表)来支持细粒度的权限分配
      2. 只读特性(Read-Only Feature):如果用s_feature_ro_compat设置了只读特性,该分区就可以用只读方式装载,并且禁止写访问.只读特性的一个例子是稀疏超级块(sparse superblock)特性,它通过不在分区的每个块组中都存储超级块的做法,来节省空间。因为内核通常只使用第一个块组中的超级块(在稀疏超级块特性启用时,该超级块仍然存在),从读访问的角度来看是没有问题的。如果发生了写操作,旧版本的文件系统代码将在文件系统卸载时修改其余的超级块副本(现在已经不存在),因而覆盖了重要的数据。
      3. 不兼容特性(Incompatible Feature):由s_incompat_features指定,如果使用了旧版本的代码,则将导致文件系统不可用。如果存在此类内核不了解的增强,那么不能装载文件系统。EXT2_FEATURE_INCOMPAT_FEATURE宏为不兼容的扩展分配数值。此类增强的一个例子是即时压缩,将所有文件以压缩形式存储。对于无法解压文件内容的文件系统代码而言,无论读写压缩过的文件内容都是无意义的

    Ext2并不使用该结构的某些成员,在设计结构时,这些成员就是为了方便将来增加新特性。其设计意图在于,当增加新特性时,无需重新格式化文件系统。对于重负荷服务器系统而言,重新格式化通常是行不通的

  2. 组描述符
    如上图,每个块组都有一个组描述符的集合,紧随超级块之后。其中保存的信息反映了文件系统每个块组的内容,因此不仅关系到当前块组的数据块,还与其他块组的数据块和inode块相关

    //include/linux/ext2_fs.h
    struct ext2_group_desc
    {
        __le32	bg_block_bitmap;		/* 块位图所在的块号 */
        __le32	bg_inode_bitmap;		/* inode位图所在的块号 */
        __le32	bg_inode_table;		/* inode表块 */
        __le16	bg_free_blocks_count;	/* 空闲块数目 */
        __le16	bg_free_inodes_count;	/* 空闲inode数目 */
        __le16	bg_used_dirs_count;	/* 目录数目 */
        __le16	bg_pad;
        __le32	bg_reserved[3];
    };
    

    每个块组中都包含了文件系统中所有块组的组描述符。因此从每个块组,都可以确定系统中所有其他块组的下列信息:

    1. 块和inode位图的位置
    2. inode表的位置
    3. 空闲块和inode的数目

    每个块组有自己的块使用位图和inode位图的数据块
    保存文件系统文件的实际内容(和用作间接的数据)的数据块,位于块组的末尾

  3. ext2硬盘存储的inode结构
    每个块组都包含一个inode位图和一个本地的inode表,inode表可能延续到几个块。位图的内容与本地块组相关,不会复制到文件系统中任何其他位置
    inode位图用于概述块组中已用和空闲的inode。通常,每个inode对应到一个比特位,有“已用”和“空闲”两种状态。inode数据保存在inode表中,包括了许多顺序存储的inode结构。
    inode记录了文件的属性和文件内容所在的block号

    //include/linux/ext2_fs.h
    struct ext2_inode { 
        __le16 i_mode; /* 文件模式 */ 
        __le16 i_uid; /* 所有者UID的低16位 */ 
        __le32 i_size; /* 长度,按字节计算 */ 
        __le32 i_atime; /* 访问时间 */ 
        __le32 i_ctime; /* 创建时间 */ 
        __le32 i_mtime; /* 修改时间 */ 
        __le32 i_dtime; /* 删除时间 */ 
        __le16 i_gid; /* 组ID的低16位 */ 
        __le16 i_links_count; /* 链接计数 */ 
        __le32 i_blocks; /* 块数目 */ 
        __le32 i_flags; /* 文件标志 */ 
        union { 
            struct { 
                __le32 l_i_reserved1; 
            } linux1; 
            struct { 
                ... 
            } hurd1; 
            struct { 
                ... 
            } masix1; 
        } osd1; /* 特定于操作系统的第一个联合 */ 
        __le32 i_block[EXT2_N_BLOCKS]; /* 块指针(块号) */ 
        __le32 i_generation; /* 文件版本,用于NFS */ 
        __le32 i_file_acl; /* 文件ACL */ 
        __le32 i_dir_acl; /* 目录ACL */ 
        __le32 i_faddr; /* 碎片地址*/ 
        union {
            struct { 
                __u8 l_i_frag; /* 碎片编号 */ 
                __u8 l_i_fsize; /* 碎片长度 */ 
                __u16 i_pad1; 
                __le16 l_i_uid_high; /* 这两个字段 */ 
                __le16 l_i_gid_high; /* 此前定义为reserved2[0] */ 
                __u32 l_i_reserved2; 
            } linux2; 
            struct { 
                ... 
            } hurd2; 
            struct { 
                ... 
            } masix2; 
        } osd2; /* 特定于操作系统的第二个联合 */ 
    };
    
  4. 目录和文件
    每个目录表示为一个inode,会对其分配数据块。数据块中包含了用于描述目录项的结构

    //include/linux/ext2_fs.h
    //目录项结构
    struct ext2_dir_entry_2 {
        __le32	inode;			/* Inode number *///目录项的inode编号
        __le16	rec_len;		/* Directory entry length *///偏移量,表示从rec_len字段末尾到下一个rec_len字段末尾的偏移量,单位是字节。这使得内核能够有效地扫描目录,从一个目录项跳转到下一个目录项
        __u8	name_len;		/* Name length */
        __u8	file_type;//目录项的类型,EXT2_FT_UNKNOWN 等
        char	name[EXT2_NAME_LEN];	/* File name *///因为目录项的长度必须是4的倍数,因而名称可能需要填充最多3个0字节。如果名称的长度可以被4整除,则无需填充0字节
    };
    
    //fs/ext2/dir.c
    typedef struct ext2_dir_entry_2 ext2_dirent;
    

    不同的目录项在硬盘上的表示方式:
    在这里插入图片描述

    文件系统代码在从目录删除一项时,会利用 rec_len ,将删除项之前一项的rec_len设置为删除项之后的一项,跳过删除项

    前面列出的目录内容,并不包含 deldir 目录,因为该目录已经被删除了.deldir之前一项的rec_len字段是32,这使得文件系统代码在扫描目录内容时,直接跳到deldir的下一项sample

    文件的类型并未定义在inode自身,而是在对应目录项的file_type字段中。但对于不同的文件类型,inode的内容也会不同.只有目录和普通文件才会占用硬盘的数据块。所有其他类型都可以使用inode中的信息完全描述

    • 符号链接的目标路径长度如果小于60个字符,则将其内容完全保存到其inode中。因为inode自身没有提供保存符号链接的目标路径名的字段(这事实上会浪费一大块空间),因此使用了一个小技巧。i_block数组通常用于保存文件数据块的地址,由15个32位数据项组成(共60个字节)。对于符号链接,该数组扮演的角色有所不同,即用于存储链接目标路径名。如果目标路径名超过60个字符,则文件系统分配一个数据块来存储该字符串
    • 设备文件、命名管道和持久套接字也可以通过inode中的信息完全描述。在内存中,另外还需要的一些数据保存在VFS的inode结构中(i_cdev用于字符设备,i_bdev用于块设备,所有信息都可以据此重建)。在硬盘上,数据块指针数组的第一个元素i_block[0]用于存储其他信息。由于设备文件没有数据块,这不会造成任何问题。符号链接使用了同样的技巧
  5. 内存中的数据结构
    Linux将文件系统结构包含的最重要的信息保存在特别的数据结构,持久驻留在物理内存中,避免经常从硬盘读取管理数据

    虚拟文件系统在struct super_block和struct inode结构分别提供了一个特定于文件系统的成员,名称分别是s_fs_inof和i_private。这两个数据成员由各种文件系统的实现使用,用于存储这两个结构中与文件系统无关的数据成员所未能涵盖的信息。Ext2文件系统将ext2_sb_info和ext2_inode_info结构用于该目的。后者与硬盘上的对应物相比,没什么特别之处

    //include/linux/ext2_fs_sb.h
    //ext2在内存中的超级块数据,存在 super_block->s_fs_inof 中
    struct ext2_sb_info {
        unsigned long s_frag_size; /* 碎片的长度,以字节为单位*/ 
        unsigned long s_frags_per_block; /* 每块中的碎片数目*/ 
        unsigned long s_inodes_per_block; /* 每块中的inode数目*/ 
        unsigned long s_frags_per_group; /* 每个块组中的碎片数目
        unsigned long s_blocks_per_group; /* 块组中块的数目 */ 
        unsigned long s_inodes_per_group; /* 块组中inode的数目 */ 
        unsigned long s_itb_per_group; /* 每个块组中用于inode表的块数 */ 
        unsigned long s_gdb_count; /* 用于组描述符的块数 */ 
        unsigned long s_desc_per_block; /* 每块可容纳的组描述符的数目 */ 
        unsigned long s_groups_count; /* 文件系统中块组的数目 */ 
        unsigned long s_overhead_last; /* 上一次计算管理数据的开销 */ 
        unsigned long s_blocks_last; /* 上一次计算的可用块数 */ 
        struct buffer_head * s_sbh; /* 包含了超级块的缓冲区 */ 
        struct ext2_super_block * s_es; /* 指向缓冲区中超级块的指针 */ 
        struct buffer_head ** s_group_desc; 
        unsigned long s_mount_opt; //保存了装载选项,EXT2_MOUNT_CHECK等
        unsigned long s_sb_block; //如果超级块不是从默认的块1读取,而是从其他块读取(在第一个超级块损坏的情况下),对应的块(相对值)保存在s_sb_block中
        uid_t s_resuid; 
        gid_t s_resgid; 
        unsigned short s_mount_state; //装载状态
        unsigned short s_pad; 
        int s_addr_per_block_bits; 
        int s_desc_per_block_bits;
        int s_inode_size; 
        int s_first_ino; 
        spinlock_t s_next_gen_lock; 
        u32 s_next_generation; 
        unsigned long s_dir_count; 目录的总数,Orlov分配器所需要的。该值在磁盘结构中没有保存,因此必须在每次装载文件系统时确定。内核为此提供了ext2_count_dirs函数
        u8 *s_debts; //指向一个数组(数组项为8位数字,该数组通常比较短),每个数组项对应于一个块组。Orlov分配器使用该数组在一个块组中的文件和目录inode之间保持均衡
        struct percpu_counter s_freeblocks_counter;//空闲块的数目
        struct percpu_counter s_freeinodes_counter;//inode的数目
        struct percpu_counter s_dirs_counter;//目录的数目
        struct blockgroup_lock s_blockgroup_lock; 
    };
    

    s_mount_opt 保存了装载选项,EXT2_MOUNT_CHECK 等,提供了宏test_opt(sb, opt)用于检测该项,但是opt参数传 CHECK 而不是 EXT2_MOUNT_CHECK
    在用grep查找内核源代码,或用LXR分析内核源代码时,要特别记住这一点:搜索EXT2_MOUNT_RESERVATION只会显示该预处理器符号的定义,而不会发现其使用。如果要找到其使用处,需要搜索RESERVATION

    statfs系统调用(大多数用户亦如此)对文件系统提供的块数感兴趣。这是指可用于存储数据的块数。s_overhead_last和s_blocks_last保存了上一次计算的值,因为计算操作代价大(内核只需要从可用于文件系统的块数减去用于存储管理数据的块数即可,但需要遍历所有的块组).这些值通常是不变的。在计算之后,除非文件系统在使用过程中调整大小,否则这些值是不变的。但很少会调整文件系统的大小

  6. 预分配
    为提高块分配的性能,Ext2文件系统采用了一种称之为预分配的机制。每当对一个文件请求许多新块时,不会只分配所需要的块数。能够用于连续分配的块,会另外被秘密标记出来,供后续使用。内核确保各个保留的区域是不重叠的。这在进行新的分配时可以节省时间以及防止碎片,特别是在有多个文件并发增长时。应该强调指出:预分配并不会降低可用空间的利用率。由一个inode预分配的空间,如果有需要,那么随时可能被另一个inode覆盖。但内核会尽力避免这种做法。我们可以将预分配想象为最后分配块之前的一个附加层,用于判断如何充分利用可用空间。预分配只是建议,而分配才是最终决定。

    //include/linux/ext2_fs_sb.h
    //预留窗口,用于预分配
    struct ext2_reserve_window {
        ext2_fsblk_t		_rsv_start;	/* First byte reserved *///起始块
        ext2_fsblk_t		_rsv_end;	/* Last byte reserved or 0 *///结束块
    };
    
    //fs/ext2/ext2.h
    struct ext2_inode_info {
        struct ext2_block_alloc_info *i_block_alloc_info;
    };
    
    //include/linux/ext2_fs_sb.h
    struct ext2_sb_info {
        spinlock_t s_rsv_window_lock;
        struct rb_root s_rsv_window_root;
        struct ext2_reserve_window_node s_rsv_window_head;
    };
    
    //预留窗口红黑树结点
    struct ext2_reserve_window_node {
        struct rb_node	 	rsv_node;//红黑树中的结点
        __u32			rsv_goal_size;//预留窗口的预期长度。可使用ioctl EXT2_IOC_SETRSVSZ从用户层设置该值,而EXT2_IOC_GETRESVZ可获取当前的设置。最大允许的预留窗口长度是EXT2_MAX_RESERVE_BLOCKS,通常定义为1 027。
        __u32			rsv_alloc_hit;//跟踪预分配的命中数,即多少次分配是在预留窗口中进行的
        struct ext2_reserve_window	rsv_window;//预留窗口
    };
    
    //如果一个inode带有预分配信息,则ext2_inode_info->i_block_alloc_info指向该结构实例
    struct ext2_block_alloc_info {
        struct ext2_reserve_window_node	rsv_window_node;//预留窗口红黑树结点
        __u32			last_alloc_logical_block;//上一次分配的块在文件中的相对块号
        ext2_fsblk_t		last_alloc_physical_block;//上一次分配的块在块设备上的物理块号
    };
    

    预分配相关的结构体关系如下:
    在这里插入图片描述

    ext2_reserve_window_node的所有实例都收集在一个红黑树中,其根结点为ext2_sb_info-> s_rsv_window_root.树结点通过rsv_node嵌入到ext2_reserve_window_node中。

    红黑树能够根据树结点的预留窗口边界,对结点排序。这使得内核能够快速找到目标块所在的预分配区域。

9.2.3创建文件系统

文件系统由mke2fs用户空间工具创建的。mke2fs不仅将分区的空间划分到管理信息有用数据两个方面,还在存储介质上创建一个简单的目录结构,使得该文件系统能够装载。

这里的管理信息指的是哪些?在装载一个新格式化的Ext2分区时,其中已经包含了一个标准的子目录,名为lost+found,用于容纳存储介质上的坏块(幸亏现在硬盘的质量较好,这个目录几乎总是空的)。这涉及下列步骤。

  1. 分配一个inode和数据块,初始化根目录。数据块包含的文件列表有3项:.、…和lost+found。由于这是根目录,所以.和…都指向表示根目录的inode自身
  2. 也为lost+found目录分配一个inode和一个数据块,数据块只包含两项:…指向根目录的inode,.指向该目录本身的inode

尽管mke2fs设计为处理块特殊文件,也可以用于普通文件,并创建一个文件系统,例子如下:

用dd创建一个1.4M的文件用于初始化为文件系统

wolfgang@meitner> dd if=/dev/zero of=img.1440 bs=1k count=1440 
1550+0 records in 
1440+0 records out

这创建了一个长度为1.4 MiB的文件,与3.5英寸软盘的容量相同。该文件只包含字节0(即ASCII值0),由/dev/zero产生。

mke2fs在该文件上创建一个文件系统:

wolfgang@meitner> /sbin/mke2fs img.1440 
mke2fs 1.40.2 (12-Jul-2007) 
img.1440 is not a block special device. 
Proceed anyway? (y,n) y 
File System label= 
OS type: Linux 
Block size=1024 (log=0)
Fragment size=1024 (log=0) 
184 inodes, 1440 blocks 
72 blocks (5.00%) reserved for the super user 
First data block=1 
Maximum file system blocks=1572864 
1 block group 
8192 blocks per group, 8192 fragments per group 
184 inodes per group 
...

img.1440中的数据可使用十六进制编辑器查看,对文件系统的结构作出判断。odhexedit软件查看

使用环回接口装载该文件系统:

wolfgang@meitner> mount -t ext2 -o loop=/dev/loop0 img.1440 /mnt

接下来即可操作该文件系统,就像是它位于块设备的某个分区上一样。所有的修改都会传输到img.1440,并且可以查看文件的内容

9.2.4文件系统操作

虚拟文件系统和具体实现之间的关联大体上由3个结构建立

  • 用于操作文件内容的操作保存在file_operations
  • 用于此类文件对象自身的操作保存在inode_operations
  • 用于一般地址空间的操作保存在address_space_operations中,文件系统和块层的关联

Ext2文件系统对不同的文件类型提供了不同的file_operations实例

普通文件

//fs/ext2/file.c 
struct file_operations ext2_file_operations = { 
    .llseek = generic_file_llseek, 
    .read = do_sync_read, 
    .write = do_sync_write, 
    .aio_read = generic_file_aio_read, 
    .aio_write = generic_file_aio_write, 
    .ioctl = ext2_ioctl, 
    .mmap = generic_file_mmap, 
    .open = generic_file_open, 
    .release = ext2_release_file, 
    .fsync = ext2_sync_file, 
    .readv = generic_file_readv, 
    .splice_read = generic_file_splice_read, 
    .splice_write = generic_file_splice_write, 
};

//fs/ext2/dir.c
//用于目录
const struct file_operations ext2_dir_operations = {
	.llseek		= generic_file_llseek,
	.read		= generic_read_dir,
	.readdir	= ext2_readdir,
	.ioctl		= ext2_ioctl,
	.fsync		= ext2_sync_file,
};
//fs/ext2/file.c 
//用于普通文件
struct inode_operations ext2_file_inode_operations = { 
    .truncate = ext2_truncate, 
    .setxattr = generic_setxattr, 
    .getxattr = generic_getxattr, 
    .listxattr = ext2_listxattr, 
    .removexattr = generic_removexattr, 
    .setattr = ext2_setattr, 
    .permission = ext2_permission, 
};

//fs/ext2/namei.c 
//用于目录
struct inode_operations ext2_dir_inode_operations = { 
    .create = ext2_create, 
    .lookup = ext2_lookup, 
    .link = ext2_link, 
    .unlink = ext2_unlink, 
    .symlink = ext2_symlink, 
    .mkdir = ext2_mkdir, 
    .rmdir = ext2_rmdir, 
    .mknod = ext2_mknod, 
    .rename = ext2_rename, 
    .setxattr = generic_setxattr, 
    .getxattr = generic_getxattr, 
    .listxattr = ext2_listxattr, 
    .removexattr = generic_removexattr, 
    .setattr = ext2_setattr, 
    .permission = ext2_permission, 
};
//fs/ext2/inode.c
//文件系统和块层的关联
struct address_space_operations ext2_aops = { 
    .readpage = ext2_readpage, 
    .readpages = ext2_readpages, 
    .writepage = ext2_writepage, 
    .sync_page = block_sync_page, 
    .write_begin = ext2_write_begin, 
    .write_end = generic_write_end, 
    .bmap = ext2_bmap, 
    .direct_IO = ext2_direct_IO, 
    .writepages = ext2_writepages, 
};
//fs/ext2/super.c 
//与超级块交互
static struct super_operations ext2_sops = { 
    .alloc_inode = ext2_alloc_inode, 
    .destroy_inode = ext2_destroy_inode, 
    .read_inode = ext2_read_inode, 
    .write_inode = ext2_write_inode, 
    .delete_inode = ext2_delete_inode, 
    .put_super = ext2_put_super, 
    .write_super = ext2_write_super, 
    .statfs = ext2_statfs, 
    .remount_fs = ext2_remount, 
    .clear_inode = ext2_clear_inode, 
    .show_options = ext2_show_options, 
};
  1. 装载和卸载

    内核处理文件系统时需要file_system_type结构来容纳装载和卸载信息

    //fs/ext2/super.c
    static struct file_system_type ext2_fs_type = { 
        .owner = THIS_MODULE, 
        .name = "ext2", 
        .get_sb = ext2_get_sb, 
        .kill_sb = kill_block_super, 
        .fs_flags = FS_REQUIRES_DEV, 
    };
    
    • file_system_type->get_sb
      • ext2_get_sb
        • get_sb_bdev //如果内存中没有适当的超级块对象,数据就必须从硬盘读取(ext2_fill_super)
          • ext2_fill_super

    在这里插入图片描述

    函数流程:

    1. 查找并设置合适的块长度
    2. 读取磁盘中的超级块
    3. 判断文件系统
    4. 分析装载参数
    5. 检查文件系统特性
    6. 填充用于长久驻留内存中表示超级块的数据结构
    7. 读取组描述符并检查一致性
    8. 计算空闲块等
    9. 最后检查超级块,并将数据写回存储介质
  2. 读取并产生数据块和间接块
    在文件系统装载后,用户进程可以调用第8章的函数访问文件的内容。所需的系统调用首先转到VFS层,然后根据文件类型,调用底层文件系统的适当例程

    下面解释的函数的调用过程

    • ext2_get_block 找到数据块

      • ext2_get_blocks 请求新块
        • ext2_alloc_branch 块分配
          • ext2_alloc_blocks
            • ext2_new_blocks
              • ext2_try_to_allocate_with_rsv 预分配
                • alloc_new_reservation 创建新的预留窗口
    • 找到数据块 ext2_get_block
      ext2_get_block,它将Ext2的实现与虚拟文件系统的默认函数关联起来,使用VFS的标准函数的文件系统,都必须定义一个类型为get_block_t的函数,原型如下:

      //include/linux/fs.h
      typedef int (get_block_t)(struct inode *inode, sector_t iblock,
      	struct buffer_head *bh_result, int create);
      

      该函数不仅读取块(顾名思义),还从内存向块设备的数据块写入数据。在进行后一项工作时,在某些情况下可能必须创建新块,该行为由create参数控制

      • ext2_get_block
        • ext2_get_blocks

      在这里插入图片描述

      函数流程:

      1. 根据数据块在文件中的位置,通过描述符表找到到达目标数据块的路径
      2. 根据路径读取实际硬盘找到最后的数据块,返回了最后一个间接块的地址,由高层函数读取块的实际内容
    • 请求新块 ext2_get_blocks
      在必须处理一个尚未分配的块时,情况变得更为复杂。进程首先要向文件写入数据,从而扩大文件,致使这种情况出现。至于是使用通常的系统调用还是内存映射来向文件写入数据,在这里并不重要。在所有情况下,内核都调用ext2_get_blocks为文件请求新块。概念上,向文件添加新块包括下面4个任务。

      • 在检测到有必要添加新块之后,内核需要判断,将新块关联到文件,是否需要间接块以及间接的层次如何
      • 必须在存储介质上查找并分配空闲块
      • 新分配的块添加到文件的块列表中
      • 为获得更好的性能,内核也会进行块预留操作。这意味着,对于普通文件,会预分配若干块。如果需要更多块,那么将优先从预分配区域进行分配

      在这里插入图片描述

      在这里插入图片描述

      上图为向文件添加数据,数据超出块时的例子,
      文件使用了1级间接块,图中的值是Indirect中key的值,ext2_get_branch返回的Indirect包含了一个指针,指向块号在间接块中的位置(即1 003,因为间接块起始于地址1 000,而我们感兴趣的是第4项)。但key的值为0,因为相关的数据块尚未分配

      函数流程:

      1. 根据数据块在文件中的位置,通过描述符表找到到达目标数据块的路径
      2. 根据路径读取实际硬盘找到最后的数据块,返回了最后一个间接块的地址,由高层函数读取块的实际内容
      3. 如果需要增加新块,查找新块,根据与上一个块是否相邻分两种情况
      4. 分配新块
      5. 将新块信息加入数据结构
    • 块分配 ext2_alloc_branch
      在这里插入图片描述

      • ext2_alloc_branch
        • ext2_alloc_blocks
          • ext2_new_blocks

      函数流程:

      1. 分配新块
      2. 建立间接块与新块的关系到 Indirect 中

      在这里插入图片描述

      函数流程:
      3. 判断inode是否有预分配信息来决定是否根据预分配机制分配多余块
      4. 检查文件系统和目标块中是否有空闲块,没有则使用文件系统第一个数据块
      5. 没多余空闲空间则关闭预分配机制,然后分配数据块
      6. 分配成功,更新统计信息,返回
      7. 分配失败,尝试其他块组,如果仍失败,重新开始整个分配,并禁用预分配机制

    • 预分配的处理 ext2_try_to_allocate_with_rsv
      预分配函数,如下图:
      在这里插入图片描述

      函数流程:

      1. 判断是否有文件关联的预留窗口,没有则创建新的预留窗口
      2. 如果有但是需要的块大于预留窗口,则扩展现存的预留窗口
      3. 给预留窗口分配块,成功则更新窗口统计信息,失败则修改预留窗口设置重新分,下一次会创建新的预留窗口

      goal_in_my_reservation函数检查预期的分配是否能够在给定的预留窗口内进行,如下图:
      在这里插入图片描述

      ext2_try_to_allocate实际块分配需要区分如下图的几种情况:
      在这里插入图片描述

      函数流程:
      4. 计算目标块搜索区间
      5. 在搜索区间中找到可用的空闲块
      6. 分配块(将位图对应的位置1)

    • 创建新的预留窗口 alloc_new_reservation
      在这里插入图片描述

      函数流程:

      1. 如果有预留窗口则根据新窗口信息调整预留窗口
      2. 检查目标块是否在窗口中
      3. 不在窗口中则创建新窗口
      4. 检查窗口中是否有空闲块,如果没空闲块则丢弃该窗口返回错误
      5. 有空闲块正常结束
  3. 创建和删除inode ext2_mkdir,ext2_create
    open和mkdir系统调用用于文件或目录的创建,它们通过虚拟文件系统的各种函数,最终到达create和mkdir函数,二者都是特定于文件类型的inode_operations实例中的函数指针。而后进入到ext2_create和ext2_mkdir函数,这两个函数都在fs/ext2/namei.c
    mkdir->vfs_mkdir->ext2_mkdir
    sys_open->vfs_create->ext2_create

    在这里插入图片描述

    函数流程:

    1. 从硬盘中获取可用inode并创建inode
    2. 设置inode操作回调函数
    3. 将inode加到父目录inode数据中
  4. 注册inode ext2_new_inode

    • ext2_new_inode
      • find_group_dir 目录经典分配方式
      • find_group_orlov 目录orlov分配方式
      • find_group_other 普通文件分配方式

    ext2_new_inode在文件系统中为新文件找到一个空闲inode,根据mode(目录设置S_IFDIR,普通文件不设置)使用不同搜索策略:

    1. 对目录inode,进行Orlov分配
    2. 对目录inode进行经典分配。仅当oldalloc选项传递到内核,禁用了Orlov分配时,才会这样做。通常Orlov分配是默认策略
    3. 普通文件的inode分配
    • 目录Orlov算法分配 find_group_orlov
      在查找目录inode时,使用了Grigoriv Orlov针对OpenBSD内核提出并实现的一种标准方案。该方案的Linux版本开发得比较晚。该分配器的目标在于,确保子目录的inode与父目录的inode在同一块组中,使二者在物理上较为接近,从而最小化硬盘寻道开销。当然,并非所有目录inode都应该出现在同一块组中,那将使得它们与相关的数据距离太远
      该方案会区分新目录是在(全局)根目录下创建,还是在文件系统中的其他位置创建
      尽管子目录inode应该与父目录inode尽可能靠近,但文件系统根目录的子目录,其inode应该尽可能分散开来。否则,目录将聚集到某个特定的块组中

      在这里插入图片描述

      函数流程:

      1. 如果父目录是根目录,生成一个随机数用于开始搜索的组块号
      2. 满足 所有块组中目录最少,空闲inode和空闲块不小于平均值则找到了,如果没找到转到4,找到了转到5
      3. 如果父目录不是根目录则从父目录所在块号开始搜索,找到第一个满足 块组中目录数小于 max_dirs,空闲inode数目不少于 min_inodes ,空闲块数目不少于 min_blocks,debt 值不超过 max_debt的块组,如果没找到转到4,找到了转到5
      4. 备用算法,重新开始搜索,从父目录所在块组开始,只要遇到空闲inode数目超出平均值的第一个块组,即返回该块组
      5. 返回所选块组的编号
    • 经典目录分配 find_group_dir(内核版本2.4(包含)之前使用的)
      find_group_dir 经典目录分配,目录inode通常会尽可能均匀地散布到整个文件系统
      满足两个条件:

      1. 块组中应该仍然有空闲空间
      2. 与块组中其他类型的inode相比,目录inode的数目应该尽可能小
        如果没有满足要求的块组,内核会选择空闲空间超出平均水平且目录inode数目最少的块组
    • 普通文件分配 find_group_other
      使用 二次散列 查找inode,它基于前向搜索,从新文件父目录inode所在的块组开始。将使用找到的有空闲inode的第一个块组

      1. 首先搜索父目录inode所在的块组start。
      2. 如果上面没找到inode则内核扫描编号为start+20的块组,然后是编号为start+20+21的块组,编号为start+20+21+22的块组,等等。每步向组编号加上一个2的更高次幂,构成的序列是1,1+2,1+2+4,1+2+4+8,…,即序列1,3,7,15,…
      3. 通常,该方法会很快找到一个空闲inode。但如果在几乎全满的文件系统上,没有找到空闲inode(几乎没什么希望),那么内核将扫描所有块组,尽一切努力争取找到一个空闲的inode。内核仍然会选择有空闲inode的第一个块组。如果完全没有空闲inode可用,则放弃操作,返回一个对应的错误码
  5. 删除inode ext2_rmdir
    在这里插入图片描述

    删除目录:

    1. 检查目录中没有文件
    2. 从父目录的数据块中查找并删除当前目录(只修改了rec_len字段,没有删除实际内容,这让数据恢复有可能)
    3. 引用计数-1
      删除文件 系统调用unlink->vfs_unlink->ext2_unlink:
    4. 从父目录的数据块中查找并删除当前目录(只修改了rec_len字段,没有删除实际内容,这让数据恢复有可能)
    5. 引用计数-1
  6. 删除数据块 ext2_delete_inode
    在可以实际删除数据块之前,必须满足两个条件

    1. 硬链接计数器 inode->i_nlink 必须为0,确保文件系统中不存在对数据的引用
    2. inode结构的使用计数器(i_count)必须从内存刷出
      iput函数用于引用计数-1,并判断是否需要删除inode,调用ext2_delete_inode
  7. 地址空间操作
    Ext2文件系统提供的大部分其他地址空间操作函数都是虚拟文件系统的标准实现

9.3Ext3文件系统

提供了一种日志(journal)特性,记录了对文件系统数据所进行的操作。在发生系统崩溃之后,该机制有助于缩短fsck的运行时间

9.3.1概念

事务(transaction)概念起源于数据库领域,它有助于在操作未能完成的情况下保证数据的一致性。

Ext3的基本思想在于,将对文件系统元数据的每个操作都视为事务,在执行之前要先行记录到日 志中。在事务结束后(即,对元数据的预期修改已经完成),相关的信息从日志删除。如果事务数据已经写入到日志之后,而实际操作执行之前(或期间),发生了系统错误,那么在下一次装载文件系统时,将会完全执行待决的操作。接下来,文件系统自动恢复到一致状态。如果在事务数据尚未写到日志之前发生错误,那么在系统重启时,由于关于该操作的数据已经丢失,因而不会执行该操作,但至少保证了文件系统的一致性

系统崩溃仍然可能造成数据丢失。但在此后,文件系统总是可以非常快速
地恢复到一致状态

事务日志当然是需要额外开销的,因而Ext3的性能与Ext2相比,是有所降低的。为了在所有情况下,在性能和数据完整性之间维持适当的均衡,内核能够以3种不同的方式访问Ext3文件系统:

  1. 回写(writeback)模式,日志只记录对元数据的修改。对实际数据的操作不记入日志。这种模式提供了最高的性能,但数据保护是最低的
  2. 顺序(ordered)模式,日志只记录对元数据的修改。但对实际数据的操作会群集起来,总是在对元数据的操作之前执行。因而该模式比回写模式稍慢
  3. 日志模式,对元数据和实际数据的修改,都写入日志。这提供了最高等级的数据保护,但速度是最慢的(除了几种病态情况以外)。丢失数据的可能性降到最低

在文件系统装载时,所需要的模式通过data参数指定。默认设置是ordered

Ext3文件系统设计为完全兼容Ext2,不仅是向下兼容,而且(尽可能)向上兼容。因而,日志存储在一个专门的文件,有自身的inode。这使得Ext3文件系统能够装载到只支持Ext2的系统上。而现存的Ext2分区也可以快速地转换为Ext3分区,而且很重要的一点是,不需要复杂的数据复制操作,这对服务器系统是一个需要考虑的主要事项

日志不仅可以存储在一个专门的文件中,也可以放置到另一个独立的分区中

内核包含了一个抽象层,称之为日志化块设备(journaling block device,简称JBD)层,用于处理日志和相关的操作。尽管该层可以用于不同的文件系统,但当前只由Ext3使用。所有其他日志文件系统,如ReiserFS、XFS和JFS都有自身的机制。因而,后面将JBD和Ext3作为一个模块考虑。

  • 日志记录、句柄和事务

    事务并不是一个整块的结构。由于文件系统的结构(和性能方面的原因),必须将事务分解为更小的单位,如下图:
    在这里插入图片描述

    1. 日志记录是可以记入日志的最小单位。每个记录表示对某个块的一个更新
    2. (原子)句柄在系统一级收集了几个日志记录。例如,如果使用write系统调用发出一个写请求,那么所有与该操作相关的日志记录都会群集到一个句柄中
    3. 事务是几个句柄的集合,用于保证提供更好的性能

9.3.2数据结构

虽然事务考虑的是数据在系统范围内的有效性,但每个句柄总是与特定的进程相关。为此,task_struct中包含了一个成员,指向当前进程的句柄:

//include/linux/sched.h 
struct task_struct { 
    ... 
    /* 日志文件系统信息 */ 
    void *journal_info; 
    ... 
}

JBD层自动承担了将void指针转换为指向handle_t指针。journal_current_handle辅助函数用于获取当前进程的活动句柄。 handle_t 是 struct handle_s 数据类型的typedef别名,用于定义句柄(以下给出的是一个简化的版本)

//include/linux/jbd.h
typedef struct handle_s		handle_t;	/* 原子操作类型 */

//日志系统中的句柄结构体
struct handle_s
{
    //指向当前句柄相关的事务数据结构的指针
    transaction_t		*h_transaction;
    //日志操作还有多少空闲缓冲区可用
    int			h_buffer_credits;
    ...
};

//二者配对使用,用于将某个代码片段标记为原子的(从日志层看来)
extern handle_t *journal_start(journal_t *, int nblocks);
extern int	 journal_stop(handle_t *);

//使用如下,两个函数可用嵌套,但要保证调用次数相同,通常使用 ext3_journal_start 函数而不直接使用 journal_start:
handle_t *handle = journal_start(journal, nblocks); 
/* 进行被认为是原子的操作 */ 
journal_stop(handle);

每个句柄由各种日志操作组成,每个操作都有自身的缓冲头用于保存修改的信息,即使底层文件系统只改变一个比特位,也是如此。这初看起来会浪费大量内存,但所获得的更高的性能弥补了这个缺点,因为缓冲区的处理非常高效。
该数据结构定义如下(已经大大简化过)

//include/linux/journal-head.h
//日志操作的缓冲头
struct journal_head {
    //指向包含操作数据的缓冲头
    struct buffer_head *b_bh;
    //指向日志项所属的事务
    transaction_t *b_transaction;
    //用于实现双链表,表示与某个原子操作相关联的所有日志
    struct journal_head *b_tnext, *b_tprev;
};

//fs/jbd/transaction.c
//JBD层提供了 journal_dirty_metadata 函数,将修改的元数据写到日志
int journal_dirty_metadata(handle_t *handle, struct buffer_head *bh)
//将修改的数据写到日志,用于日志模式
int journal_dirty_data(handle_t *handle, struct buffer_head *bh)
//include/linux/journal-head.h
typedef struct transaction_s	transaction_t;

//include/linux/jbd.h
//日志系统的事务结构体
struct transaction_s
{
    //指向事务数据将写入的日志
    journal_t		*t_journal;
	tid_t			t_tid;
    //每个事务都可以有不同的状态,并且保存在t_state中
    enum {
		T_RUNNING,//可以向日志添加新的原子句柄
		...
		T_FLUSH,//此时正在将日志项刷出到磁盘
		T_COMMIT,//所有数据都已经写到磁盘,但仍然需要处理元数据
		T_FINISHED//所有日志项都已经安全地写到磁盘
	}			t_state;
    //指向与该事务关联的缓冲区
    struct journal_head	*t_buffers;
    //指定事务数据必须在物理上写到日志中的时间期限。内核使用了一个定时器,默认情况下在事务创建5秒之后到期
    unsigned long		t_expires;
    //与事务关联的句柄的数目
    int t_handle_count;
};

Ext3代码使用了一种“检查点”机制,用于检查日志中记载的改变是否已经写入到文件系统。如果已经写入到文件系统,那么日志中的数据就不再需要了,可以删除。在正常运作时,日志内容不会扮演活跃的角色。仅当系统崩溃发生时,才使用日志数据来重建对文件系统的改变,使之返回到一致状态。
与Ext2的初始定义相比,Ext3的超级块数据结构添加了几个成员,用于支持日志功能

//include/linux/ext3_fs_sb.h
struct ext3_sb_info {
    ...
    /* Journaling */
	//日志可以存储到一个文件中,也可以存储到独立的分区,这是要存储的文件
	struct inode * s_journal_inode;
	//指向日志数据结构
	struct journal_s * s_journal;
	//指定了数据从内存写到日志的频率
	unsigned long s_commit_interval;
	//日志可以存储到一个文件中,也可以存储到独立的分区,这是要存储的分区
	struct block_device *journal_bdev;
};

总结

ext2文件系统:
硬盘第一个扇区为启动扇区,在系统加电启动时,其内容由BIOS自动装载并执行,包含一个启动装载程序(bootloader),用于从计算机安装的操作系统中选择一个启动

第一个扇区后面是n个块组,每个块组由一个超级块,k个组描述符块,1个数据位图块,1个inode位图块,n个inode表块,m个数据块

内核使用第一个超级块,然后将第一个超级块的数据复制到所有其他块组的超级块中

  • 超级块是用于存储文件系统自身元数据的核心结构。其中的信息包括空闲与已使用块的数目、块长度、当前文件系统状态(在启动时用于检测前一次崩溃)、各种时间戳(例如,上一次装载文件系统的时间以及上一次写入操作的时间)。它还包括一个表示文件系统类型的魔数,这样mount例程能够确认文件系统的类型是否正确。内核只使用第一个块组的超级块读取文件系统的元信息,即使在几个超级块中都有超级块,也是如此。
  • 组描述符包含的信息反映了文件系统中各个块组的状态,例如,块组中空闲块和inode的数目。每个块组都包含了文件系统中所有块组的组描述符信息
  • 数据块位图和inode位图用于保存比特位串。这些结构中的每个比特位都对应于一个数据块或inode,用于表示对应的数据块或inode是空闲的,还是被使用中。
  • inode表包含了块组中所有的inode,inode用于保存文件系统中与各个文件和目录相关的所有元数据,这里的inode是文件系统的inode结构不是内存中的inode结构。
  • 数据块部分包含了文件系统中的文件的有用数据。

inode结构体中有15个数据块数组,前12个为直接块号,后3个分别为一次间接块,二次间接块,三次间接块。间接块用于表示大文件

为提高块分配性能,当对一个文件请求许多新块时会预分配一些块,使用了预留窗口结构。
ext2_sb_info->s_rsv_window_root为红黑树根,存着ext2_reserve_window_node->rsv_node.以预留窗口边界进行排序

ext3与ext2相比增加了日志功能用于系统恢复

=========================================

涉及的命令和配置:

工具 fsck 用于恢复崩溃的Ext2文件系统.这并不排除数据丢失的可能性

数据类型__le32、__le16,这些都是指定了位长的整数,采用的字节序是小端序
u32等,特定于机器的数据类型,而不是指定了位长的类型名

预留窗口长度,EXT2_MAX_RESERVE_BLOCKS,默认为1 027

mke2fs 用于创建ext2文件系统

创建文件

wolfgang@meitner> dd if=/dev/zero of=img.1440 bs=1k count=1440 
1550+0 records in 
1440+0 records out

格式化为ext2文件系统

wolfgang@meitner> /sbin/mke2fs img.1440 
mke2fs 1.40.2 (12-Jul-2007) 
img.1440 is not a block special device. 
Proceed anyway? (y,n) y 
File System label= 
OS type: Linux 
Block size=1024 (log=0)
Fragment size=1024 (log=0) 
184 inodes, 1440 blocks 
72 blocks (5.00%) reserved for the super user 
First data block=1 
Maximum file system blocks=1572864 
1 block group 
8192 blocks per group, 8192 fragments per group 
184 inodes per group 
...

挂载文件系统

wolfgang@meitner> mount -t ext2 -o loop=/dev/loop0 img.1440 /mnt
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值