catalog
1. 简介 2. Ext2文件系统 3. Ext3文件系统 4. 小结
1. 简介
VFS虚拟文件系统接口和数据结构构成了一个框架,各个文件系统的实现都必须在框架内运转,但这并不要求每个文件系统在持久存储其内容的块设备上组织文件时,都需要采用同样的思想、方法和概念,与此相反,Linux支持多种文件系统概念
即使由于虚拟文件系统的存在,使得这些文件系统从用户空间和内核空间都可以通过相同的接口访问,我们接下里重点讨论Ext2/3文件系统,它们已经说明了文件系统开发中的关键概念
Ext2/3的特征可以简要地如下描述
1. Ext2文件系统: 该文件系统一直伴随着Linux,它已经成为许多服务器和桌面系统的支柱,工作性能很出色,Ext2文件系统的设计利用了与虚拟文件系统非常类似的结构,因为开发Ext2时,目标就是要优化与Linux的互操作,但它也可以用于其他的操作系统 2. Ext3文件系统: 这是Ext2的演化和发展,它仍然与Ext2兼容,但提供了扩展日志功能,这对系统崩溃的恢复特别有用,但总的来说,文件系统的基本原理是一样的
0x1: 文件系统面临的挑战
1. 磁盘空间碎片化
在管理基于磁盘文件系统的存储空间时,会遇到一个特殊问题: 碎片,随着文件的移动和新文件增加,可用空间变得越来越"破碎",特别是在文件很小的情况下,由于这对访问速度有负面影响,因此文件系统必须尽可能减少碎片产生
2. 存储空间利用率
另一个重要的需求是有效利用存储空间,在这里文件系统必须做一个平衡
1. 要完全利用空间,必须将大量管理数据存储在磁盘上,这抵消了更紧凑的数据存储带来的好处,甚至可能情况更糟 2. 还要避免浪费磁盘容量,如果空间未能有效使用,那么就失去了减少管理数据带来的好处 3. 维护文件内容的一致性也是一个关键问题,需要在规划和实现文件系统期间慎重考虑,因为即使是最稳定的内核也可能突然PANIC,可能是软件错误、也可能是由于断电、硬件故障等一些列原因。即使此类事故造成不可恢复的错误(例如,如果修改被缓存在物理内存中,没有写回磁盘,那么数据就会丢失),文件系统的实现必须尽可能快速、全面地纠正出现的损坏,在最低限度上,它必须能够将文件系统还原到一个可用的状态
各个文件系统实现处理该平衡问题的方法均有所不同,通常会引入由管理员配置的参数,以便针对预期的使用模式来优化文件系统(例如: 管理预期使用大量的打文件或小文件)
最后,在评价文件系统的质量时,速度也是一个重要的因素,即使硬盘与CPU或物理内存相比速度慢了很多数量级,但糟糕的文件系统会进一步降低系统的速度
Relevant Link:
2. Ext2文件系统
Ext2文件系统专注于高性能,以下是它的设计目标
1. 支持可变块长,使得文件系统能够处理预期的应用(许多打文件、小文件) 2. 快速符号链接,如果链接目标的路径足够短,则将其存储在inode自身中(而不是存储在数据区中,本质上是数组寻址和指针寻址的差别) 3. 将扩展能力集成到设计中,使得从旧版本迁移到新版本时,无需重新格式化和重新加载硬盘 4. 在存储介质上操作数据时采用了一种精巧复杂的策略,使得系统崩溃可能造成的影响最小。文件系统通常可以恢复到一种状态: 在这种状态下辅助工具fsck至少能修复它,使得文件系统能够再次使用,但这并不排除数据丢失的可能性 5. 使用特殊的属性将文件标记为不可改变的,例如,这可以防止对重要配置文件的无意修改,即使超级用户也不行
0x1: 物理结构
必须建立各种结构,来存放文件系统的数据,包括
1. 文件内容 2. 目录层次结构的表示 3. 相关的管理数据,如 1) 访问权限 2) 与用户、组、OTHER的关联 4. 用于管理文件系统内部信息的元数据
这些对从块设备读取数据进行分析而言,都是必要的,这些结构的持久副本需要存储在硬盘上,这样数据在两次会话之间不会丢失,下一次启动重新激活内核时,数据仍然是可用的。因为硬盘和物理内存的需求是不同的,同一数据结构通常会有两个版本,一个用户在磁盘上的持久存储,另一个用于在内存中的处理
在讨论文件系统的时候,经常使用的名次"块(block)"通常有两个不同的含义
1. 一方面,有些文件系统存储在面向块的设备上,与设备之间的数据传输都以块为单位进行,不会传输单个字符 2. 另一方面,Ext2文件系统是一种"基于块"的文件系统,它将硬盘划分为若干块,每个块的长度都相同,按块管理元数据和文件内容,这意味着底层存储介质的结构影响到了文件系统的结构,这很自然也会影响到所用的算法和数据结构的设计
将硬盘划分为固定长度的块时,特别重要的一个方面是文件占用的存储空间只能是块长度的整数倍
1. 结构概观
下图给出了一个块组(block group)的内容,块组是Ext2文件系统的核心要素,是文件系统的基本成分,容纳了文件系统的其他结构,每个文件系统都由大量块组组成,在硬盘上相继排布
1. 超级块 用于存储文件系统自身元数据的核心结构,其中的信息包括 1) 空闲、已使用块的数目 2) 块长度 3) 当前文件系统状态(在启动时用于检查前一次崩溃) 4) 时间戳(例如上一次装载文件系统的时间、上一次写入操作的时间) 5) 一个表示文件系统类型的魔数,这样mount例程能够确认文件系统的类型是否正确 .. //http://www.cnblogs.com/LittleHann/p/3865490.html 搜索:0x10: struct super_block 2. 组描述符 包含的信息反映了文件系统中各个块组的状态,例如,块组中空闲块和inode的数目,每个块组都包含了文件系统中所有块组的组描述符信息 3. 数据块位图、inode位图 用于保存长的比特位串,这些结构中的每个比特位都对应于一个数据块或inode,用于表示对应的数据块或inode是空闲的,还是被使用中 4. inode表 包含了块组中所有的inode,inode用于保存文件系统中与各个文件和目录相关的所有元数据 5. 数据块 包含了文件系统中的文件的有用数据
启动扇区是硬盘上的一个区域,在系统加电启动时,其内容由BIOS自动装载并执行,它包含一个启动装载程序,用于从计算机安装的操作系统中选择一个启动,还负责继续启动过程。显然,该区域不可能填充文件系统的数据
启动装载程序并非在所有系统上都是必须的,在需要启动装载程序的系统上,它们通常位于硬盘的起始处,以避免影响其后的分区
硬盘上剩余的空间由连续的许多块组占用,存储了文件系统元数据和各个文件的有用数据,每个块组包含许多冗余信息,有两个原因
1. 如果系统崩溃破坏了超级块,有关文件系统结构和内容的所有信息都会丢失,如果有冗余的副本,该信息是可能恢复的 2. 通过使文件和管理数据尽可能接近,减少了磁头寻道和旋转,这可以提高文件系统的性能
实际上,数据并非在每个块组中都复制,内核也之用超级块的第一个副本工作(通常情况下足够了),在进行文件系统检查时,会将第一个超级块的数据传播到剩余的超级,供紧急情况下读取。因为该方法也会消耗大量的存储空间,Ext2的后续版本采用了稀疏超级块(sparse superblock)技术,该做法中,超级块不再存储到文件系统的每个块组中,而是只写入到块组0、块组1..、其他ID可以表示为3、5、7的幂的块组中
超级块的数据缓存在内存中,使得内核不必重复地从硬盘读取数据
这些结构与虚拟文件系统的基本要素本质是一致的,采用这种结构解决了许多问题,例如目录表示,但Ext2文件系统仍然需要解决几个问题
1. 各个文件之间的差别可能非常大,无论是长度还是用途 2. 如果文件系统内容只在内存中操作,而不是存储到慢速的外部介质上,这些问题就不那么严重,在高速的物理内存建立、扫描、修改所需的结构几乎不花费时间,但是在硬盘上执行同样的操作要慢得多 3. 在设计用于存储数据的结构时,必须最优地满足所有的文件系统需求,对硬盘来说这是一件很困难的事,特别是在考虑到介质容量的利用和访问速度时,Ext2文件系统因此借助于技巧来解决,我们继续讨论这个话题
2. 间接
Ext2文件系统采用了经典的UNIX方案,借助于inode实现文件,但仍然有一些问题需要解决,硬盘划分为块由文件使用,特定文件占用的块数目取决于文件内容的长度(同时也与块长度本身有关系,上取整对齐的原理)
在系统内存中,从内核的角度来看,内存划分为长度相同的页,按唯一的页号或指针寻址,硬盘与内存类似,块也通过编号唯一标识,这使得存储在inode结构中的文件元数据,能够关联到位于硬盘数据块部分的文件内容,二值之间的关联,是通过将数据块的地址存储在inode中建立的
文件占用的数据块不一定是连续的(虽然出于性能考虑,连续数据块是我们想要的一种最好的情况),也可能散步到整个硬盘上,这直接意味着inode中必须逐块记录每个文件所包含的块所在的地址
为了解决兼顾管理数据块最大长度和inode元数据不过分膨胀的问题,所有的UNIX文件系统(包括Ext2)对此都使用了一种经过证实的解决方案,称之为间接(indirection),使用间接,在inode中仅需要耗费少量字节存储块号,刚好够用来表示平均意义上长度较小的文件,对较大的文件,指向各个数据块的指针(块号)是间接存储的
这种方法容许对大/小文件的灵活存储,因为用于存储块号的区域的长度,将随文件实际长度的变化而动态变化,即inode本身是固定的,用于间接的其他数据块是动态分配的
1. 该方法用于小文件的情形,inode中直接存储的块号即足够标识所有数据块,因为inode只包含少量块号,因此inode结构占用的硬盘空间很少 2. 如果文件较大,inode中的块号不足以标识所有的数据块,则会使用间接,文件系统在硬盘上分配一个数据块,不存储文件数据,专门用于存储块号,该块号称为"一次间接块(single indirect block)",可以容纳数百个块号,inode必须存储第一个间接块的块号,以便访问,该块号紧接着直接块的块号存储 3. inode的长度总是固定的,间接块占用的空间,对于支持大文件来说是必然的,但对于小文件不会带来额外的开销。需要明白的是,在文件变得越来越大时,借助于间接来增加可用空间必然会遇到极限,因此一个合乎逻辑的方案是采用二次间接,这仍然需要分配一个硬盘块,来存储数据块的块号,但这里的数据块并不存储有用的文件数据,而是存储其他数据块的块号,后者才存储有用的文件数据,这是一种典型的多级索引的思想
使用二次间接显著增加了各个文件的可管理空间(即允许磁盘上保存更大的文件),当然该方法有一个负面效应,它使得对打文件的访问代价更高,文件系统首先必须查找间接块的地址,读取下一个间接项,查找对应的间接块,并从中查找数据块的块号。因而在管理可变长度的文件的能力方面,与访问速度相应的下降方面(越大的文件,速度越慢),必然存在一个折中
需要明白的是,二次间接并不是最终方案,内核还提供了三次间接来表示真正的巨型文件,原理上和二次间接是一致的
3. 碎片
内存和磁盘存储管理在块结构方面的相似性,意味着它们都会面临"碎片问题",随着时间的变化,文件系统的许多文件从磁盘的随机位置删除,又增加了许多新文件,这使得空闲磁盘空间变成长度不同的碎片存储区
在这种不可避免的碎片的场景下,数据将散步到磁盘的不同区域,变得支离破碎,重要的是,这些对用户进程是透明的,进程访问文件时看到的总是一个连续的线性结构,而不会考虑到硬盘上数据碎片的程度,这和处理器向进程提供连续内存视图的原理思想类似,其差别在于,没有自动的硬件机制(MMU)来替文件系统保证线性化,文件系统自身的代码负责完成该任务
当然,在使用直接或一次、二次、三次间接块指向文件数据块时,都没有困难,通过指针中的信息,总是可以唯一地识别出数据块号,由此看来,数据库是顺序的,还是散步到这个硬盘上,是不相关的事情,即从外部访问者的角度来说,逻辑上是连续的
但是,在磁盘管理场景中,这种存储的不连续会使磁头读取数据时需要不停寻道,因而降低了访问速度,因此,Ext2文件系统尽力防止碎片,在无法避免碎片时,它试图将同一个文件的块维持在同一个块组中,如果文件系统尚未满载,尚有适当的存储空间可用,那么这种做法就很有用,这自动减少了对碎片的敏感程度
0x2: 数据结构
我们知道,针对硬盘存储定义的结构,都有针对内存访问定义的对应结构,这些与虚拟文件系统定义的结构协同使用,首先用来支持与文件系统的通信并简化重要数据的管理,其次用于缓存元数据,加速对文件系统的处理
1. 超级块
超级块是文件系统的核心结构,保存了文件系统所有的特征数据,内核在装载文件系统时,最先看到的就是超级块的内容,超级块的数据使用ext2_read_super读取,内核通常借助file_system_type结构中的read_super函数指针来调用该函数
/source/fs/ext2/super.c
struct super_block * ext2_read_super (struct super_block * sb, void * data, int silent) { .. }
对于Ext2文件系统来说,ext2_super_block结构用于定义超级块
\linux-2.6.32.63\include\linux\ext2_fs.h
/* * Structure of the super block */ struct ext2_super_block { /* Inodes count inode数目 */ __le32 s_inodes_count; /* Blocks count 块数目 */ __le32 s_blocks_count; /* Reserved blocks count 已分配块的数目 */ __le32 s_r_blocks_count; /* Free blocks count 空闲块数目 */ __le32 s_free_blocks_count; /* Free inodes count 空闲inode数目 */ __le32 s_free_inodes_count; /* First Data Block 第一个数据块 */ __le32 s_first_data_block; /* Block size 块长度 将块长度除以1024之后,再取以二为底的对数(0、1、2),分别对应的块长度(1024、2048、4096) 我们想要的块长度必须在用mke2fs创建文件系统期间指定,文件系统创建以后,该值不能修改,因为它表示了一个基本的文件系统常数 */ __le32 s_log_block_size; /* Fragment size 碎片长度 */ __le32 s_log_frag_size; /* # Blocks per group 每个块组包含的块数 在创建文件系统时,这些值也必须固定下来 */ __le32 s_blocks_per_group; /* # Fragments per group 每个块组包含的碎片 */ __le32 s_frags_per_group; /* # Inodes per group 每个块组的inode数目 在创建文件系统时,这些值也必须固定下来 */ __le32 s_inodes_per_group; /* Mount time 装载时间 */ __le32 s_mtime; /* Write time 写入时间 */ __le32 s_wtime; /* Mount count 装载次数 */ __le16 s_mnt_count; /* Maximal mount count 最大装载计数 */ __le16 s_max_mnt_count; /* Magic signature 魔数,标记文件系统类型 对于Ext2来说是0xEF53 */ __le16 s_magic; /* File system state 文件系统状态 1. 在分区正确地卸载时,设置为EXT2_VALID_FS,向mount程序表明该分区没有问题 2. 如果文件未能正确卸载(例如由于断点),该变量仍然保持在文件系统上次装载后设置的状态值: EXT2_ERROR_FS,在这种情况下,下一次装载文件系统时,将自动触发一致性检验 */ __le16 s_state; /* Behaviour when detecting errors 检测到错误时的行为 */ __le16 s_errors; /* minor revision level 副修订号 */ __le16 s_minor_rev_level; /* time of last check 上一次检查的时间 如果在该日期之后,时间已经超过了一定的阀值,那么即使文件系统的状态是干净的,也会执行一次检查 */ __le32 s_lastcheck; /* max. time between checks 两次检查允许间隔的最长时间,用于实现强制一致性检查 */ __le32 s_checkinterval; /* OS 创建文件系统的操作系统 */ __le32 s_creator_os; /* Revision level 修订号 */ __le32 s_rev_level; /* Default uid for reserved blocks 能够使用保留块的默认UID 这些块其他用户无法使用,默认情况下为0,这对应于系统的超级用户(root用户),对于普通用户看来空间已经用尽的文件系统上,该用户仍然能够写入 这些额外的空闲空间通常称之为根储备(root reverve) 跟储备(通常在创建文件系统时,留出可用空间约5%),并向超级用户(也可以配置为其他用户)提供了一定的安全缓冲,确保在硬盘接近全满时能够采取对应的措施 */ __le16 s_def_resuid; /* Default gid for reserved blocks 能够使用保留块的默认GID */ __le16 s_def_resgid; /* * These fields are for EXT2_DYNAMIC_REV superblocks only. * * Note: the difference between the compatible feature set and * the incompatible feature set is that if there is a bit set * in the incompatible feature set that the kernel doesn‘t * know about, it should refuse to mount the filesystem. * * e2fsck‘s requirements are more strict; if it doesn‘t know * about a feature in either the compatible or incompatible * feature set, it must abort and not try to meddle with * things it doesn‘t understand... */ /* First non-reserved inode 第一个非保留的inode */ __le32 s_first_ino; /* size of inode structure inode结构的长度 */ __le16 s_inode_size; /* block group # of this superblock 当前超级块所在的块组编号 */ __le16 s_block_group_nr; /* compatible feature set 兼容特性集 */ __le32 s_feature_compat; /* incompatible feature set 不兼容特性集 */ __le32 s_feature_incompat; /* readonly-compatible feature set 只读兼容特性集 */ __le32 s_feature_ro_compat; /* 128-bit uuid for volume 卷的128bit uuid */ __u8 s_uuid[16]; /* volume name 卷名 */ char s_volume_name[16]; /* directory where last mounted 上一次装载的目录 */ char s_last_mounted[64]; /* For compression 用于压缩 */ __le32 s_algorithm_usage_bitmap; /* * Performance hints. Directory preallocation should only * happen if the EXT2_COMPAT_PREALLOC flag is on. */ __u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/ __u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */ __u16 s_padding1; /* * Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set. */ __u8 s_journal_uuid[16]; /* uuid of journal superblock */ __u32 s_journal_inum; /* inode number of journal file */ __u32 s_journal_dev; /* device number of journal file */ __u32 s_last_orphan; /* start of list of inodes to delete */ __u32 s_hash_seed[4]; /* HTREE hash seed */ __u8 s_def_hash_version; /* Default hash version to use */ __u8 s_reserved_char_pad; __u16 s_reserved_word_pad; __le32 s_default_mount_opts; __le32 s_first_meta_bg; /* First metablock block group */ __u32 s_reserved[190]; /* Padding to the end of the block */ };
注意到一个细节,大部分字段的数据类型都是__le32、__le16等,这些都是指定了位长的整数,采用的字节序是小端序,之所以不使用C语言的基本类型,是因为不同处理器以不同的位长来表示基本类型,因此,使用基本类型将导致超级块格式依赖处理器类型,这显然是不可接受的,在不同计算机系统之间切换可移动介质时,元数据必须总是以相同的格式存储,不管什么类型的处理器。内核的其他部分,也需要位长可以保证且不随处理器改变的数据类型,因此,\linux-2.6.32.63\include\asm-generic\types.h中包含的特定于体系结构的文件,定义了一系列类型(从__s8到__u64),以便控制到所用CPU类型的正确基本数据类型的映射,特定于字节序的类型直接基于这些定义
此外,为确保文件系统在不同计算机系统之间的可移动性,Ext2文件系统的设计规定,在硬盘上存储超级块结构的所有数值时,都采用小端序格式,在数据读入内存时,内核负责将这种格式转换为CPU的本机格式
Ext2文件系统的设计是非常谨慎的,确保了能够比较容易地将新特性集成到旧的设计中,为此,超级块结构中有3个成员专门用于描述额外的特性
1. s_feature_compat: 兼容特性(compatible feature) 可以用于文件系统代码的新版本,对旧版本没有负面影响(或功能损伤),此类增强的例子包括Ext3引入的日志特性,用ACL(访问控制表)来支持细粒度的权限分配 2. __le32 s_feature_incompat:不兼容特性(imcompatible features) 如果使用了旧版本的代码,则将导致文件系统不可用,如果存在此类内核不了解的增强,那么不能装载文件系统 3. __le32 s_feature_ro_compat: 只读特性(read_only feature) 在使用旧版本的文件系统代码时,此类增强不会损害对文件系统的读访问,但写访问可能导致错误和文件系统的不一致。如果设置了s_feature_ro_compat属性,该分区就可以用只读方式装载,并且禁止写访问
Ext2并不使用该结构的某些成员,在设计结构时,这些成员就是为了方便将来增加新特性,这么做的目的在于,当增加新特性时,无需重新格式化文件系统,对于重负荷服务器系统而言,为了升级文件系统而重新格式化通常是不可接受的
2. 组描述符
每个块组都有一个组描述符的集合,紧随超级块之后,其中保存的信息反映了文件系统每个块组的内容,因此不仅关系到当前块组的数据块,还与其他块组的数据块和inode块相关,用于定于单个组描述符的数据结构比超级块短得多
\linux-2.6.32.63\include\linux\ext2_fs.h
/* * Structure of a blocks group descriptor */ struct ext2_group_desc { /* Blocks bitmap block 块位图块 bg_block_bitmap引用的块不用来存储数据,其中每个比特位都表示当前块组中的一个数据块 1. 如果一个比特位置位,则表明对应的块正在由文件系统使用 2. 否则该快是可用的 由于第一个数据块的位置是已知的,而所有数据块是按线性排序的,因此内核很容易在块位图中比特置位和相关块的位置之间转换 */ __le32 bg_block_bitmap; /* Inodes bitmap block inode位图块 bg_inode_bitmap也是一个块号,对应块的各个比特位用于描述一个块组的所有inode 由于inode结构所在的块和inode结构的长度都是已知的,因此内核很容易在位图中的比特位位置和相应的inode在硬盘上的位置之间转换 */ __le32 bg_inode_bitmap; /* Inodes table block inode表块 */ __le32 bg_inode_table; /* Free blocks count 空闲块数目 */ __le16 bg_free_blocks_count; /* Free inodes count 空闲inode数目 */ __le16 bg_free_inodes_count; /* Directories count 目录数目 */ __le16 bg_used_dirs_count; __le16 bg_pad; __le32 bg_reserved[3]; };
在组描述符集合中,内核使用该结构的一个副本来描述一个对应的块组
每个块组都包含许多组描述符,文件系统中的每个块组都对应于一个组描述符副本,因此从每个块组,都可以确定系统中所有其他块组的信息
1. 块和inode位图的位置: 只是用于一个块组,而不会复制到文件系统的每个块组 2. inode表的位置 3. 空闲块和inode的数目
将分区划分为块组,是经过系统化的考虑的,这种做法显著提高了速度,文件系统总是试图将文件的内容存储到一个块组中,以最小化磁头在inode、块位图、数据块之间寻道的代价
3. inode
每个块组都包含一个inode位图和一个本地的inode表,inode表可能延续到几个块,位图的内容与本地块组相关,不会复制到文件系统中任何其他位置
1. inode位图(bitmap)用于概述块组中已用和空闲的inode,通常,每个inode对应到一个比特位,有"已用"、"空闲"两种状态 2. inode表存储了inode数据,包括了顺序存储的inode结构
inode数据如何保存到存储介质,定义如下
\linux-2.6.32.63\include\linux\ext2_fs.h
struct ext2_inode { /* File mode 文件模式 */ __le16 i_mode; /* Low 16 bits of Owner Uid 所有者UID的低16bit */ __le16 i_uid; /* Size in bytes 长度,按字节计算 i_size、i_blocks字段分别以字节和块为单位指定了文件长度,需要注意的是,随着Ext2文件系统的变化,i_blocks并不一定能从i_size推断出来 文件洞(file hole)方法用来确保稀疏文件不浪费空间,该方法将空洞占用的空间降到最低 */ __le32 i_size; /* Access time 访问时间 */ __le32 i_atime; /* Creation time 创建时间 */ __le32 i_ctime; /* Modification time 修改时间 */ __le32 i_mtime; /* Deletion Time 删除时间 */ __le32 i_dtime; /* Low 16 bits of Group Id 组ID的低16bit */ __le16 i_gid; /* Links count 链接计数,指定了指向inode的硬链接的数目 */ __le16 i_links_count; /* Blocks count 块数目 */ __le32 i_blocks; /* File flags 文件标志 */ __le32 i_flags; /* OS dependent 1 特定于操作系统的第一个联合 */ union { struct { __le32 l_i_reserved1; } linux1; struct { __le32 h_i_translator; } hurd1; struct { __le32 m_i_reserved1; } masix1; } osd1; /* Pointers to blocks 块指针(块号) 该数组有EXT2_N_BLOCKS个数组项,默认情况下,EXT2_N_BLOCKS = 12 +3 1. 前12个元素用于寻址直接块,符号链接如果小于60字符,则将其内容直接保存到inode中 2. 后3个用于实现简单、二次、三次间接 */ __le32 i_block[EXT2_N_BLOCKS]; /* File version (for NFS) 文件版本(用于NFS) */ __le32 i_generation; /* File ACL 文件ACL */ __le32 i_file_acl; /* Directory ACL 目录ACL,Linux对文件和目录分别采取了不同的ACL控制 */ __le32 i_dir_acl; /* Fragment address 碎片地址 */ __le32 i_faddr; union { struct { __u8 l_i_frag; /* Fragment number 碎片编号 */ __u8 l_i_fsize; /* Fragment size 碎片长度 */ __u16 i_pad1; __le16 l_i_uid_high; /* these 2 fields */ __le16 l_i_gid_high; /* were reserved2[0] */ __u32 l_i_reserved2; } linux2; struct { __u8 h_i_frag; /* Fragment number */ __u8 h_i_fsize; /* Fragment size */ __le16 h_i_mode_high; __le16 h_i_uid_high; __le16 h_i_gid_high; __le32 h_i_author; } hurd2; struct { __u8 m_i_frag; /* Fragment number */ __u8 m_i_fsize; /* Fragment size */ __u16 m_pad1; __u32 m_i_reserved2[2]; } masix2; } osd2; /* OS dependent 2 */ };
该结构包含两个特定于操作系统的联合,根据用途接受不同的数据,Ext2文件系统不仅用于Linux,而且也用于GNU和HURD、Masix操作系统
每个块组有多少inode,这取决于文件系统创建时的设置,在创建文件系统时,每个块组的ionde数目可以设置为任意(合理)值,这个数值保存在s_inodes_per_group字段中
4. 目录和文件
我们继续讨论目录的表示,它定义了文件系统的拓朴结构,在经典的UNIX文件系统中,目录也是一种特殊的文件,其中是inode指针和对应的文件名列表,表示了当前目录下的文件和子目录,对于Ext2文件系统,每个目录表示为一个inode,会对其分配数据块,数据块中包含了用于描述目录项的结构
struct ext2_dir_entry_2 { /* Inode number inode编号 它是一个指针,指向目录项的inode */ __le32 inode; /* Directory entry length 目录项长度 它是一个偏移量,表示从rec_len字段末尾到下一个rec_len字段末尾的偏移量,单位是字节 这使得内核能够有效地扫描目录,从一个目录项跳到下一个目录项,这在遍历文件目录的时候很有用 文件系统代码在从目录删除一项时,会利用该信息,因为不必移动被删除目录文件之后的所有信息 而只要将rec_len的偏移继续后移,这样从逻辑上被跳过的目录项就不存在了(对于系统来说,无法找到意味着被删除) */ __le16 rec_len; /* Name length 名称长度 */ __u8 name_len; /* file_type指定了目录项的类型,该变量的可能值 enum { EXT2_FT_UNKNOWN, EXT2_FT_REG_FILE, //普通文件 EXT2_FT_DIR, //目录 EXT2_FT_CHRDEV, //字符特殊文件 EXT2_FT_BLKDEV, //块特殊文件 EXT2_FT_FIFO, //FIFO(命名管道) EXT2_FT_SOCK, //套接字 EXT2_FT_SYMLINK, //符号链接 EXT2_FT_MAX }; S_ISREG等宏的判断依据就是inode的这个字段值 应该注意到,只有目录和普通文件才会占用硬盘的数据块,所有其他类型都可以使用inode中的信息完全描述 */ __u8 file_type; /* File name 文件名 */ char name[EXT2_NAME_LEN]; };
5. 内存中的数据结构
为避免经常从低速的硬盘读取管理数据结构,Linux将这些结构包含的最重要的信息保存在特别的数据结构,持久驻留在物理内存中,这样可以大大提高访问速度,也减少了和硬盘的交互
虚拟文件系统在struct super_block、struct inode结构分别提供了一个特定于文件系统的成员(s_fs_inof、i_private),这两个数据成员对应于Ext2文件系统的ext2_sb_info、ext2_inode_info结构
可以看到,在物理内存和虚拟文件系统之间有一套大致平行的数据结构,用于高速访问和持久化保存
\linux-2.6.32.63\include\linux\efs_fs_sb.h
/* efs superblock information in memory */ struct efs_sb_info { __u32 fs_magic; /* superblock magic number */ __u32 fs_start; /* first block of filesystem */ __u32 first_block; /* first data block in filesystem */ __u32 total_blocks; /* total number of blocks in filesystem */ __u32 group_size; /* # of blocks a group consists of */ __u32 data_free; /* # of free data blocks */ __u32 inode_free; /* # of free inodes */ __u16 inode_blocks; /* # of blocks used for inodes in every grp */ __u16 total_groups; /* # of groups */ };
为提高分配的性能,Ext2文件系统采用了一种称之为"预分配"的机制,每当对一个文件请求多次新块时,不会只分配所需要的块数,而是能够用于连续分配的块,会另外被秘密标记出来,供后续使用
1. 内核确保各个保留的区域是不重叠的,这在进行新的分配时可以节省时间以及防止碎片,特别是在有多个文件并发增长时,应该强调指出 2. 预分配并不会降低可用空间的利用率,由一个inode预分配的空间,如果有需要,那么随时可能被另一个inode覆盖,但内核会尽力避免这种做法 3. 可以将预分配理解为最后分配快之前的一个附加层,用于判断如何充分利用可用空间,预分配只是建议,而分配才是最终决定
实现该机制需要几个数据结构,预留窗口(reservation window)其中指定了起始块、结束块,这定义了一个预留的区域
/source/fs/ext2/ext2.h
struct ext2_reserve_window { /* First byte reserved 第一个预留的字节 */ ext2_fsblk_t _rsv_start; /* Last byte reserved or 0 最后一个预留的字节,或为0 */ ext2_fsblk_t _rsv_end; };
该窗口需要与其他Ext2数据结构联合使用,才能发挥作用
0x3: 创建文件系统
文件系统并非由内核自身创建,而是由mke2fs用户空间工具创建,mke2fs不仅将分区的空间划分到管理信息和有用数据两部分,还在存储介质上创建一个简单的目录结构,使得该文件系统能够装载
尽管mke2fs设计为处理特殊文件,也可以将其用于块介质上的某个普通文件,并创建一个文件系统(Linux万物皆文件哲学)
1. dd if=/dev/zero of=img.1440 bs=1k count=1440 //创建一个1.4MB文件,该文件只包含字节0 2. mke2fs在该文件上创建一个文件系统 /sbin/mke2fs img.1440 /* [root@iZ23lobjjltZ fstesst]# /sbin/mke2fs img.1440 mke2fs 1.39 (29-May-2006) img.1440 is not a block special device. Proceed anyway? (y,n) y Filesystem 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 filesystem blocks=1572864 1 block group 8192 blocks per group, 8192 fragments per group 184 inodes per group Writing inode tables: done Writing superblocks and filesystem accounting information: done This filesystem will be automatically checked every 21 mounts or 180 days, whichever comes first. Use tune2fs -c or -i to override */ 3. 使用环回接口装载该文件系统 mount -t ext2 -o loop=/dev/loop0 img.1440 /mnt 4. 操作该文件系统,就像是它位于块设备的某个分区上一样,所有的修改都会传输到img.1440,并且可以查看文件的内容
从更本质的角度理解,文件系统是一种纯粹虚拟的概念,它本质上就是由一些用于描述元数据和真实数据的数据结构组成,Ext2规定了一种组织方式,而文件系统并不依赖于底层存储介质是什么
0x4: 文件系统操作
我们知道,虚拟文件系统和具体实现之间的关联大致由3个结构建立,结构中包含了一系列的函数指针,所有的文件系统都必须实现该关联,这是Linux VFS的强制框架性接口规范
1. 用于操作文件内容的操作函数: file_operations 2. 用于此类文件对象自身的操作: inode_operations 3. 用于一般地址空间的操作: address_space_operations
Ext2文件系统对不同的文件类型提供了不同的file_operations实例
\linux-2.6.32.63\fs\ext2\file.c
const 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, .unlocked_ioctl = ext2_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = ext2_compat_ioctl, #endif .mmap = generic_file_mmap, .open = generic_file_open, .release = ext2_release_file, .fsync = simple_fsync, .splice_read = generic_file_splice_read, .splice_write = generic_file_splice_write, }; #ifdef CONFIG_EXT2_FS_XIP const struct file_operations ext2_xip_file_operations = { .llseek = generic_file_llseek, .read = xip_file_read, .write = xip_file_write, .unlocked_ioctl = ext2_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = ext2_compat_ioctl, #endif .mmap = xip_file_mmap, .open = generic_file_open, .release = ext2_release_file, .fsync = simple_fsync, }; #endif
大多数项都指向了VFS的标准函数
目录也有自身的file_operations实例,但比文件操作的要简单的多,因为很多文件操作对目录是没有意义的
\linux-2.6.32.63\fs\ext2\dir.c
const struct file_operations ext2_dir_operations = { .llseek = generic_file_llseek, .read = generic_read_dir, .readdir = ext2_readdir, .unlocked_ioctl = ext2_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = ext2_compat_ioctl, #endif .fsync = simple_fsync, };
相比于文件操作缺失的字段由编译器自动初始化为NULL指针
普通文件的inode_operations初始化如下
const struct inode_operations ext2_file_inode_operations = { .truncate = ext2_truncate, #ifdef CONFIG_EXT2_FS_XATTR .setxattr = generic_setxattr, .getxattr = generic_getxattr, .listxattr = ext2_listxattr, .removexattr = generic_removexattr, #endif .setattr = ext2_setattr, .check_acl = ext2_check_acl, .fiemap = ext2_fiemap, };
目录有更多可用的inode操作
\linux-2.6.32.63\fs\ext2\namei.c
const 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, #ifdef CONFIG_EXT2_FS_XATTR .setxattr = generic_setxattr, .getxattr = generic_getxattr, .listxattr = ext2_listxattr, .removexattr = generic_removexattr, #endif .setattr = ext2_setattr, .check_acl = ext2_check_acl, };
文件系统和下层的块层通过address_space_operations关联,在Ext2文件系统中,这些操作初始化如下
\linux-2.6.32.63\fs\ext2\inode.c
const 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, .migratepage = buffer_migrate_page, .is_partially_uptodate = block_is_partially_uptodate, .error_remove_page = generic_error_remove_page, };
ext2_sops用于与超级块交互(读、写、分配inode)
\linux-2.6.32.63\fs\ext2\super.c
static const struct super_operations ext2_sops = { .alloc_inode = ext2_alloc_inode, .destroy_inode = ext2_destroy_inode, .write_inode = ext2_write_inode, .delete_inode = ext2_delete_inode, .put_super = ext2_put_super, .write_super = ext2_write_super, .sync_fs = ext2_sync_fs, .statfs = ext2_statfs, .remount_fs = ext2_remount, .clear_inode = ext2_clear_inode, .show_options = ext2_show_options, #ifdef CONFIG_QUOTA .quota_read = ext2_quota_read, .quota_write = ext2_quota_write, #endif };
文件系统的操作涉及很多的函数,我们重点讨论其关键机制和原理
1. 装载和卸载
内核处理文件系统时需要另一个结构来容纳装载和卸载信息,file_system_type结构用于该目的
\linux-2.6.32.63\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, };
mount系统调用通过get_sb来读取文件系统超级块的内容,Ext2文件系统依赖虚拟文件系统的一个标准函数(get_sb_bdev)来完成该工作
static int ext2_get_sb(struct file_system_type *fs_type, int flags, const char *dev_name, void *data, struct vfsmount *mnt) { //指向ext2_fill_super的一个函数指针作为参数传递给get_sb_bdev,用于填充一个超级块对象 return get_sb_bdev(fs_type, flags, dev_name, data, ext2_fill_super, mnt); }
ext2_fill_super流程如下
static int ext2_fill_super(struct super_block *sb, void *data, int silent) { struct buffer_head * bh; //文件系统的元信息英镐一直驻留在内存中,并由ext2_sb_info数据结构保存 struct ext2_sb_info * sbi; struct ext2_super_block * es; struct inode *root; unsigned long block; unsigned long sb_block = get_sb_block(&data); unsigned long logic_sb_block; unsigned long offset = 0; unsigned long def_mount_opts; long ret = -EINVAL; int blocksize = BLOCK_SIZE; int db_count; int i, j; __le32 features; int err; sbi = kzalloc(sizeof(*sbi), GFP_KERNEL); if (!sbi) return -ENOMEM; sbi->s_blockgroup_lock = kzalloc(sizeof(struct blockgroup_lock), GFP_KERNEL); if (!sbi->s_blockgroup_lock) { kfree(sbi); return -ENOMEM; } sb->s_fs_info = sbi; sbi->s_sb_block = sb_block; /* * See what the current blocksize for the device is, and * use that as the blocksize. Otherwise (or if the blocksize * is smaller than the default) use the default. * This is important for devices that have a hardware * sectorsize that is larger than the default. 首先需要设置一个初始块长度,用于读取超级块,由于文件系统中使用的块长度还不知道 因此内核首先通过sb_min_blocksize查找最小可能值,通常默认设置为1024字节 */ blocksize = sb_min_blocksize(sb, BLOCK_SIZE); if (!blocksize) { printk ("EXT2-fs: unable to set blocksize\n"); goto failed_sbi; } /* * If the superblock doesn‘t start on a hardware sector boundary, * calculate the offset. */ if (blocksize != BLOCK_SIZE) { logic_sb_block = (sb_block*BLOCK_SIZE) / blocksize; offset = (sb_block*BLOCK_SIZE) % blocksize; } else { logic_sb_block = sb_block; } //调用sb_bread读取超级块所在的数据块 if (!(bh = sb_bread(sb, logic_sb_block))) { printk ("EXT2-fs: unable to read superblock\n"); goto failed_sbi; } /* * Note: s_es must be initialized as soon as possible because * some ext2 macro-instructions depend on its value */ es = (struct ext2_super_block *) (((char *)bh->b_data) + offset); sbi->s_es = es; sb->s_magic = le16_to_cpu(es->s_magic); //确认该分区实际上是否包含了一个Ext2文件系统,通过魔数进行匹配 if (sb->s_magic != EXT2_SUPER_MAGIC) goto cantfind_ext2; /* Set defaults before we parse the mount options */ def_mount_opts = le32_to_cpu(es->s_default_mount_opts); if (def_mount_opts & EXT2_DEFM_DEBUG) set_opt(sbi->s_mount_opt, DEBUG); if (def_mount_opts & EXT2_DEFM_BSDGROUPS) set_opt(sbi->s_mount_opt, GRPID); if (def_mount_opts & EXT2_DEFM_UID16) set_opt(sbi->s_mount_opt, NO_UID32); #ifdef CONFIG_EXT2_FS_XATTR if (def_mount_opts & EXT2_DEFM_XATTR_USER) set_opt(sbi->s_mount_opt, XATTR_USER); #endif #ifdef CONFIG_EXT2_FS_POSIX_ACL if (def_mount_opts & EXT2_DEFM_ACL) set_opt(sbi->s_mount_opt, POSIX_ACL); #endif if (le16_to_cpu(sbi->s_es->s_errors) == EXT2_ERRORS_PANIC) set_opt(sbi->s_mount_opt, ERRORS_PANIC); else if (le16_to_cpu(sbi->s_es->s_errors) == EXT2_ERRORS_CONTINUE) set_opt(sbi->s_mount_opt, ERRORS_CONT); else set_opt(sbi->s_mount_opt, ERRORS_RO); sbi->s_resuid = le16_to_cpu(es->s_def_resuid); sbi->s_resgid = le16_to_cpu(es->s_def_resgid); set_opt(sbi->s_mount_opt, RESERVATION); //parse_options分析用于指定装载选项的参数 if (!parse_options ((char *) data, sbi)) goto failed_mount; sb->s_flags = (sb->s_flags & ~MS_POSIXACL) | ((EXT2_SB(sb)->s_mount_opt & EXT2_MOUNT_POSIX_ACL) ? MS_POSIXACL : 0); ext2_xip_verify_sb(sb); /* see if bdev supports xip, unset EXT2_MOUNT_XIP if not */ /* 对文件系统特定的检查,能够揭示内核是否能够装载该文件系统 */ if (le32_to_cpu(es->s_rev_level) == EXT2_GOOD_OLD_REV && (EXT2_HAS_COMPAT_FEATURE(sb, ~0U) || EXT2_HAS_RO_COMPAT_FEATURE(sb, ~0U) || EXT2_HAS_INCOMPAT_FEATURE(sb, ~0U))) printk("EXT2-fs warning: feature flags set on rev 0 fs, " "running e2fsck is recommended\n"); /* * Check feature flags regardless of the revision level, since we * previously didn‘t change the revision level when setting the flags, * so there is a chance incompat flags are set on a rev 0 filesystem. */ features = EXT2_HAS_INCOMPAT_FEATURE(sb, ~EXT2_FEATURE_INCOMPAT_SUPP); if (features) { printk("EXT2-fs: %s: couldn‘t mount because of " "unsupported optional features (%x).\n", sb->s_id, le32_to_cpu(features)); goto failed_mount; } if (!(sb->s_flags & MS_RDONLY) && (features = EXT2_HAS_RO_COMPAT_FEATURE(sb, ~EXT2_FEATURE_RO_COMPAT_SUPP))){ printk("EXT2-fs: %s: couldn‘t mount RDWR because of " "unsupported optional features (%x).\n", sb->s_id, le32_to_cpu(features)); goto failed_mount; } blocksize = BLOCK_SIZE << le32_to_cpu(sbi->s_es->s_log_block_size); if (ext2_use_xip(sb) && blocksize != PAGE_SIZE) { if (!silent) printk("XIP: Unsupported blocksize\n"); goto failed_mount; } /* If the blocksize doesn‘t match, re-read the thing.. 如果保存在s_blocksize中的文件系统块长度和最初指定的最小值并不匹配,则使用sb_set_blocksize修改最初设置的块长度,并再次读取超级块 */ if (sb->s_blocksize != blocksize) { brelse(bh); if (!sb_set_blocksize(sb, blocksize)) { printk(KERN_ERR "EXT2-fs: blocksize too small for device.\n"); goto failed_sbi; } logic_sb_block = (sb_block*BLOCK_SIZE) / blocksize; offset = (sb_block*BLOCK_SIZE) % blocksize; bh = sb_bread(sb, logic_sb_block); if(!bh) { printk("EXT2-fs: Couldn‘t read superblock on " "2nd try.\n"); goto failed_sbi; } es = (struct ext2_super_block *) (((char *)bh->b_data) + offset); sbi->s_es = es; if (es->s_magic != cpu_to_le16(EXT2_SUPER_MAGIC)) { printk ("EXT2-fs: Magic mismatch, very weird !\n"); goto failed_mount; } } sb->s_maxbytes = ext2_max_size(sb->s_blocksize_bits); if (le32_to_cpu(es->s_rev_level) == EXT2_GOOD_OLD_REV) { sbi->s_inode_size = EXT2_GOOD_OLD_INODE_SIZE; sbi->s_first_ino = EXT2_GOOD_OLD_FIRST_INO; } else { sbi->s_inode_size = le16_to_cpu(es->s_inode_size); sbi->s_first_ino = le32_to_cpu(es->s_first_ino); if ((sbi->s_inode_size < EXT2_GOOD_OLD_INODE_SIZE) || !is_power_of_2(sbi->s_inode_size) || (sbi->s_inode_size > blocksize)) { printk ("EXT2-fs: unsupported inode size: %d\n", sbi->s_inode_size); goto failed_mount; } } sbi->s_frag_size = EXT2_MIN_FRAG_SIZE << le32_to_cpu(es->s_log_frag_size); if (sbi->s_frag_size == 0) goto cantfind_ext2; sbi->s_frags_per_block = sb->s_blocksize / sbi->s_frag_size; sbi->s_blocks_per_group = le32_to_cpu(es->s_blocks_per_group); sbi->s_frags_per_group = le32_to_cpu(es->s_frags_per_group); sbi->s_inodes_per_group = le32_to_cpu(es->s_inodes_per_group); if (EXT2_INODE_SIZE(sb) == 0) goto cantfind_ext2; sbi->s_inodes_per_block = sb->s_blocksize / EXT2_INODE_SIZE(sb); if (sbi->s_inodes_per_block == 0 || sbi->s_inodes_per_group == 0) goto cantfind_ext2; sbi->s_itb_per_group = sbi->s_inodes_per_group / sbi->s_inodes_per_block; sbi->s_desc_per_block = sb->s_blocksize / sizeof (struct ext2_group_desc); sbi->s_sbh = bh; sbi->s_mount_state = le16_to_cpu(es->s_state); sbi->s_addr_per_block_bits = ilog2 (EXT2_ADDR_PER_BLOCK(sb)); sbi->s_desc_per_block_bits = ilog2 (EXT2_DESC_PER_BLOCK(sb)); if (sb->s_magic != EXT2_SUPER_MAGIC) goto cantfind_ext2; if (sb->s_blocksize != bh->b_size) { if (!silent) printk ("VFS: Unsupported blocksize on dev " "%s.\n", sb->s_id); goto failed_mount; } if (sb->s_blocksize != sbi->s_frag_size) { printk ("EXT2-fs: fragsize %lu != blocksize %lu (not supported yet)\n", sbi->s_frag_size, sb->s_blocksize); goto failed_mount; } if (sbi->s_blocks_per_group > sb->s_blocksize * 8) { printk ("EXT2-fs: #blocks per group too big: %lu\n", sbi->s_blocks_per_group); goto failed_mount; } if (sbi->s_frags_per_group > sb->s_blocksize * 8) { printk ("EXT2-fs: #fragments per group too big: %lu\n", sbi->s_frags_per_group); goto failed_mount; } if (sbi->s_inodes_per_group > sb->s_blocksize * 8) { printk ("EXT2-fs: #inodes per group too big: %lu\n", sbi->s_inodes_per_group); goto failed_mount; } if (EXT2_BLOCKS_PER_GROUP(sb) == 0) goto cantfind_ext2; sbi->s_groups_count = ((le32_to_cpu(es->s_blocks_count) - le32_to_cpu(es->s_first_data_block) - 1) / EXT2_BLOCKS_PER_GROUP(sb)) + 1; db_count = (sbi->s_groups_count + EXT2_DESC_PER_BLOCK(sb) - 1) / EXT2_DESC_PER_BLOCK(sb); sbi->s_group_desc = kmalloc (db_count * sizeof (struct buffer_head *), GFP_KERNEL); if (sbi->s_group_desc == NULL) { printk ("EXT2-fs: not enough memory\n"); goto failed_mount; } bgl_lock_init(sbi->s_blockgroup_lock); sbi->s_debts = kcalloc(sbi->s_groups_count, sizeof(*sbi->s_debts), GFP_KERNEL); if (!sbi->s_debts) { printk ("EXT2-fs: not enough memory\n"); goto failed_mount_group_desc; } //逐块读取组描述符 for (i = 0; i < db_count; i++) { block = descriptor_loc(sb, logic_sb_block, i); sbi->s_group_desc[i] = sb_bread(sb, block); if (!sbi->s_group_desc[i]) { for (j = 0; j < i; j++) brelse (sbi->s_group_desc[j]); printk ("EXT2-fs: unable to read group descriptors\n"); goto failed_mount_group_desc; } } //调用ext2_check_descriptors检查一致性 if (!ext2_check_descriptors (sb)) { printk ("EXT2-fs: group descriptors corrupted!\n"); goto failed_mount2; } sbi->s_gdb_count = db_count; get_random_bytes(&sbi->s_next_generation, sizeof(u32)); spin_lock_init(&sbi->s_next_gen_lock); /* per fileystem reservation list head & lock */ spin_lock_init(&sbi->s_rsv_window_lock); sbi->s_rsv_window_root = RB_ROOT; /* * Add a single, static dummy reservation to the start of the * reservation window list --- it gives us a placeholder for * append-at-start-of-list which makes the allocation logic * _much_ simpler. */ sbi->s_rsv_window_head.rsv_start = EXT2_RESERVE_WINDOW_NOT_ALLOCATED; sbi->s_rsv_window_head.rsv_end = EXT2_RESERVE_WINDOW_NOT_ALLOCATED; sbi->s_rsv_window_head.rsv_alloc_hit = 0; sbi->s_rsv_window_head.rsv_goal_size = 0; ext2_rsv_window_add(sb, &sbi->s_rsv_window_head); err = percpu_counter_init(&sbi->s_freeblocks_counter, ext2_count_free_blocks(sb)); if (!err) { err = percpu_counter_init(&sbi->s_freeinodes_counter, ext2_count_free_inodes(sb)); } if (!err) { err = percpu_counter_init(&sbi->s_dirs_counter, ext2_count_dirs(sb)); } if (err) { printk(KERN_ERR "EXT2-fs: insufficient memory\n"); goto failed_mount3; } /* * set up enough so that it can read an inode */ sb->s_op = &ext2_sops; sb->s_export_op = &ext2_export_ops; sb->s_xattr = ext2_xattr_handlers; root = ext2_iget(sb, EXT2_ROOT_INO); if (IS_ERR(root)) { ret = PTR_ERR(root); goto failed_mount3; } if (!S_ISDIR(root->i_mode) || !root->i_blocks || !root->i_size) { iput(root); printk(KERN_ERR "EXT2-fs: corrupt root inode, run e2fsck\n"); goto failed_mount3; } sb->s_root = d_alloc_root(root); if (!sb->s_root) { iput(root); printk(KERN_ERR "EXT2-fs: get root inode failed\n"); ret = -ENOMEM; goto failed_mount3; } if (EXT2_HAS_COMPAT_FEATURE(sb, EXT3_FEATURE_COMPAT_HAS_JOURNAL)) ext2_warning(sb, __func__, "mounting ext3 filesystem as ext2"); //ext2_setup_super进行最后的检查并输出适当的警告信息(例如装载的文件系统处于不一致状态) ext2_setup_super (sb, es, sb->s_flags & MS_RDONLY); return 0; cantfind_ext2: if (!silent) printk("VFS: Can‘t find an ext2 filesystem on dev %s.\n", sb->s_id); goto failed_mount; failed_mount3: percpu_counter_destroy(&sbi->s_freeblocks_counter); percpu_counter_destroy(&sbi->s_freeinodes_counter); percpu_counter_destroy(&sbi->s_dirs_counter); failed_mount2: for (i = 0; i < db_count; i++) brelse(sbi->s_group_desc[i]); failed_mount_group_desc: kfree(sbi->s_group_desc); kfree(sbi->s_debts); failed_mount: brelse(bh); failed_sbi: sb->s_fs_info = NULL; kfree(sbi->s_blockgroup_lock); kfree(sbi); return ret; }
2. 读取并产生数据块和间接块
在文件系统装载后,用户进程可以调用相关系统调用访问文件的内容,系统调用首先转到VFS层,然后根据文件类型,调用底层文件系统的适当例程,通常虚拟文件系统提供了默认操作(例如generic_file_read、generic_file_mmap),如果底层文件系统没有设置对应的操作,则Linux内核会默认初始化为默认操作
从VFS的角度来看,文件系统的目的在于,建立文件的内容与相关存储介质上对应块之间的关联
1) 找到数据块
ext2_get_block是一个关键函数,它将Ext2的实现与虚拟文件系统的默认函数关联起来,需要重点注意的是,所有希望使用VFS的标准文件系统,都必须定义一个类型为get_block_t的函数
\linux-2.6.32.63\include\linux\fs.h
typedef int (get_block_t)(struct inode *inode, sector_t iblock, struct buffer_head *bh_result, int create); //该函数不仅读取块,还从内存向块设备的数据块写入数据
对于该原型,Ext2使用的函数是ext2_get_block,执行查找块的重要任务
/source/fs/ext2/inode.c
int ext2_get_block(struct inode *inode, sector_t iblock, struct buffer_head *bh_result, int create) { unsigned max_blocks = bh_result->b_size >> inode->i_blkbits; int ret = ext2_get_blocks(inode, iblock, max_blocks, bh_result, create); if (ret > 0) { bh_result->b_size = (ret << inode->i_blkbits); ret = 0; } return ret; }
2) 请求新块
在必须处理一个尚未分配的块时,情况变得更加复杂一点
1. 进程首先要向文件写入数据,从而扩大文件 1) 使用常规的系统调用写入数据 2) 通过内存映射向文件写入数据 2. 调用ext2_get_blocks为文件请求新块
概念上,向文件添加新块包括下列4个任务
1. 在检测到有必要添加新块之后,内核需要判断,将新块关联到文件,是否需要间接块以及间接的层次如何 2. 必须在存储介质上查找并分配空闲块 3. 新分配的块添加到文件的块列表中 4. 为获得更好的性能,内核也会进行快预留操作,这意味着对于普通文件,会预分配若干块,如果需要更都块,那么将会优先从预分配区域进行分配
搜索新块需要下面几个步骤
1. 首先搜索目标块(goal block),从文件系统的角度来看,该块是分配操作的理想候选者 2. 目标块的搜索只是基于一般原则,并不考虑文件系统中的实际情况,查找最佳的新块时,将调用ext2_find_goal函数,在进行搜索时,必须区分下面两种情况 1) 当将要分配的块在逻辑上紧随着文件中上一次分配的块时(即数据将要连续写入),文件系统试图分配硬盘上的下一个物理块,即如果数据在文件中是顺序存储的,那么在硬盘上也应该尽可能连续存储 2) 如果新块的逻辑位置与上一次分配的块不是紧邻的,那么将调用ext2_find_near函数查找最适当的新块,内核会尽可能找到一个接近的块,或至少在同一个柱面中的块 3. 在内核得到这两部分信息(间接链中需要分配新块的位置、新块的预期地址)之后,内核将要在硬盘上分配一块 4. 可能不仅需要新数据块,很可能还需要分配一些保存间接信息的块
3) 块分配
ext2_alloc_branch负责对给定的新路径分配所需的块,并建立连接块的间接链
4) 预分配的处理
在Ext2分配函数的层次中,ext2_try_to_allocate_with_rsv,该函数是用于分配新块及预留窗口的主要函数
5) 创建新的预留窗口
我们知道,alloc_new_reservation用来创建新的预留窗口
3. 创建、删除inode
inode也必须由Ext2文件系统的底层函数创建和删除,首先讨论文件或目录的创建,open、mkdir最后进入到vfs层函数,而后进入ext2_create、ext2_mkdir函数,这通过inode_operations关联起来
\linux-2.6.32.63\fs\ext2\namei.c
/* 1. dir: 将要创建新子目录的父目录 2. dentry: 指定了新目录的路径名 3. mode: 指定了新目录的访问模式 */ static int ext2_mkdir(struct inode * dir, struct dentry * dentry, int mode) { struct inode * inode; int err = -EMLINK; if (dir->i_nlink >= EXT2_LINK_MAX) goto out; inode_inc_link_count(dir); //ext2_new_inode在硬盘上的适当为自豪分配了一个新的indoe,它将向inode提供适当的文件、inode、地址空间操作 inode = ext2_new_inode (dir, S_IFDIR | mode); err = PTR_ERR(inode); if (IS_ERR(inode)) goto out_dir; inode->i_op = &ext2_dir_inode_operations; inode->i_fop = &ext2_dir_operations; if (test_opt(inode->i_sb, NOBH)) inode->i_mapping->a_ops = &ext2_nobh_aops; else inode->i_mapping->a_ops = &ext2_aops; inode_inc_link_count(inode); //ext2_make_empty向inode添加默认的"."、".."目录项,即生成对应的目录项结构,并将其写入到数据块中 err = ext2_make_empty(inode, dir); if (err) goto out_fail; //ext2_add_link将新目录添加到父目录的inode的数据中 err = ext2_add_link(dentry, inode); if (err) goto out_fail; d_instantiate(dentry, inode); unlock_new_inode(inode); out: return err; out_fail: inode_dec_link_count(inode); inode_dec_link_count(inode); unlock_new_inode(inode); iput(inode); out_dir: inode_dec_link_count(dir); goto out; }
创建新文件的方式类似,sys_open系统调用会到达vfs_create,然后继续调用Ext2文件系统提供的底层函数ext2_create
4. 注册inode
在创建目录和文件时,ext2_new_inode用于为新的文件系统项查找一个空闲的inode,但搜索策略随情况而变,这可以根据mode参数区分。搜索本身对性能没什么要求,但从文件系统的性能来考虑,最好将inode定位到一个能够快速访问数据的位置,为此,内核采用了三种不同的策略
1. 对目录inode,进行Orlov分配,这是默认策略 2. 对目录inode,进行经典分配,仅当oldalloc选项传递到内核,禁用了Orlov分配时,才使用经典分配 3. 普通文件的inode分配
1) Orlov分配
在查找目录inode时,使用了Grigoriv Orlov针对OpenBSD内核提出并实现的一种标准方案,该分配器的目标在于,确保子目录的inode与父目录的inode在同一个块组中,使二者在物理上较为接近,从而最小化磁盘寻道开销
该方案会区分新目录是在(全局)根目录下创建,还是在文件系统中的其他位置创建
2) 经典目录分配
在内核2.4之前的经典方案中,系统的各个块组通过前向搜索进行扫描,要特别注意的是以下两个条件
1. 块组中应该仍然有空闲空间 2. 与块组中其他类型的inode相比,目录inode的数目应该尽可能小
在这种方案下,目录inode通常会尽可能均匀地散步到整个文件系统,如果没有满足要求的块组,内核会选择空闲空间超出平均水平且目录inode数目最少的数组
3) 其他文件的inode分配
在为普通文件、链接和目录以外的所有其他文件类型查找inode时,应用了一个更简单的方案,称之为二次散列(quadratic hashing),它基于前向搜索,从新文件父目录inode所在的块组开始,将使用找到的有空闲inode的第一个块组
1. 首先搜索父目录inode所在的块组 2. 假定从其组ID是start,如果该块组没有空闲inode,则内核扫描编号为start+2^0的块组,然后是编号为start+2^0+2^1的块组... 3. 每步向组编号加上一个2的更高次幂 4. 通常该方案会很快找到一个空闲inode,但如果在几乎全满的文件系统上,那么内核将扫描所有块组,尽一切努力争取到一个空闲inode
5. 删除inode
目录和文件的inode都可以删除,我们首先讨论删除目录,在调用rmdir系统调用之后,代码穿透内核,最终到达inode_operations结构的rmdir函数指针,对于Ext2文件系统来说,对应于ext2_rmdir函数
删除目录需要以下几个操作
1. 从父目录的inode数据区中,删除当前目录对应的目录项 2. 接下来,释放磁盘上已经分配的数据块(inode和用于保存子目录项的数据块) 3. 为了确保要删除的目录不再包含任何文件,需要使用ext2_empty_dir函数检查其数据块的内容,如果内核只找到对应于"."、".."的目录项,则该目录可以删除,否则放弃操作并返回错误码 4. 从父目录的数据块中删除对应目录项的工作,委托给ext2_unlink函数
注意到,每个目录都会有一个引用计数,这从一定程度上表明了当前目录下的文件数目,如果使用ls枚举得到的数目小于父目录的引用计数,则可能从一定程度上说明当前目录遭到了rootkit的文件隐藏攻击
ext2_delete_entry将该目录项从目录表中删除,目录表中对应的数据并未从物理上删除,相反是通过ext2_dir_entry_2结构的rec_len字段进行设置,以便在扫描目录表时跳过被删除项,这种方法能够在很大程序上提高速度,因为实际删除目录项需要重写大量数据
1. 通过查看文件系统在硬盘上的结构(假定有读写分区上裸数据的权限),通过重置被删除文件目录项的前一项的rec_len字段,即可重新激活被删除文件的目录项,从而有可能恢复被删除的文件,当然前提是该文件分配的数据块尚未被其他数据覆盖 2. 同时,这也可能导致敏感数据遭到泄漏
6. 删除数据块
数据块的删除与inode对象的引用计数密切相关,在可以实际删除数据块之前,必须满足以下几个条件
1. 硬链接计数器nlink = 0,确保文件系统中不存在对数据的引用 2. inode结构的使用计数器i_count必须从内存刷出
内核使用iput函数,将内存中inode对象的引用计数器减一,因而在其中进行检查以确认inode是否仍然需要,如果不需要,则删除该inode,这是虚拟文件系统的一个标准函数
需要明白的是,数据块的删除是一个逻辑上虚拟的概念,它既不会删除在硬盘上占用的空间,也不会用0字节覆盖原来的内容,只是将块位图或inode位图中对应的比特位清零
7. 地址空间操作
Ext2文件系统提供的大部分其他地址空间操作函数,都以类似的方式实现为标准函数的前端,并通过ext2_get_block与Ext2文件系统的底层代码关联起来
3. Ext3文件系统
Ext文件系统的第三次扩展,逻辑上称之为Ext3,提供了一种日志(journal)特性,记录了对文件系统数据所进行的操作,在发生系统奔溃之后,该机制有助于缩短fsck的运行时间。事务(transaction)概念起源于数据库领域,它有助于在操作未完成的情况下保证数据的一致性,一致性问题同样也会发生在文件系统中(并非Ext特有),如果文件系统操作被无意中断,这种情况下需要保证元数据的正确性和一致性
0x1: 概念
Ext3的基本思想在于,将对文件系统元数据的每个操作都视为事务,在执行之前要先行记录到日志中,在事务结束后,相关的信息从日志删除,如果在事务中发生了系统错误,那么在下一次装载文件系统时,将会完全执行待决的操作,将系统自动恢复到一致状态
事务日志是需要额外开销的,为了在所有情况下,在性能和数据完整性之间维持适当的均衡,内核能够以3种不同的方式访问Ext3文件系统
1. 回写(writeback)模式: 日志只记录对元数据的修改,对实际数据的操作不记入日志,这种模式提供了最高的性能,但数据保护是最低的 2. 顺序(ordered)模式: 日志只记录对元数据的修改,但对实际数据的操作会群集起来,总是在对元数据的操作之前执行,因而该模式比回写模式稍慢 3. 日志模式: 对元数据和实际数据的修改,都写入日志,这提供了最高等级的保护,但速度是最慢的,丢失数据的可能性降到最低 //在文件系统装载时,所需要的模式通过data参数指定,默认设置是ordered
内核包含了一个抽象层,称之为日志化块设备(journaling block devices JDB层),用于处理日志和相关的操作
1. 日志记录、句柄、事务
事务并不是一个整块的结构,由于文件系统的结构(和性能方面的原因),必须将事务分解为更小的单位
1. "日志记录"是可以记入日志的最小单位,每个记录表示对某个块的一个更新 2. 原子句柄在系统一级收集了几个日志记录 3. 事务是几个句柄的集合,用户保证提高更好的性能
0x2: 数据结构
虽然事务考虑的是数据在系统范围内的有效性,但每个句柄总是与特定的进程相关,在task_struct中包含了一个句柄,用于指向当前进程的句柄
struct task_struct { .. //日志文件系统信息 void *journal_info; .. }
JBD层自动承担了将void指针转换为指向handle_t指针
每个句柄由各种日志操作组成,每个操作都有自身的缓冲头用于保存修改的信息,即使底层文件系统只改变一个比特位,也是如此
0x3: 自动回滚
Ext3代码使用了一种"检查点"机制,用于检查日志中记载的改变是否已经写入到文件系统,如果已经写入到文件系统,那么日志中的数据就不再需要,可以删除。在正常运作时,日志内容不会起到作用,仅当系统发生崩溃时,才使用日志数据来重建对文件系统的改变,使之返回到一致状态
4. 小结
文件系统用于物理块设备(如硬盘)上组织文件数据,以便持久存储信息,从这个意义上讲,文件系统更像是一个"法则",一个组织文件的规范,是一个虚拟逻辑概念
Copyright (c) 2015 LittleHann All rights reserved