ext2文件系统需要考虑哪些因素:
1.最重要的是保证数据的安全性。
2.效率,方便文件的查找读写。
3.文件在磁盘空间占用空间小。
EXT文件系统采用一个独立的顶级树形目录架构(即所有一切都从root根目录开始,延伸到子目录)来组织和管理全部文件。目录文件记录了本目录下所有文件和子目录的信息(文件名和索引节点号inode),并依次存放在数据块中,采用把文件名(放在目录文件中)与文件的其他所有属性(放在索引节点inode表中)分开存储的管理方法来大大提高文件检索的速度。
/*
* Structure of an inode on the disk
*/
struct ext2_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Creation time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks; /* Blocks count */
__le32 i_flags; /* File flags */
.......
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
.......
};
EXT2是Linux上标准的文件系统,EXT3是在EXT2基础上增加日志形成的,其思想可以追溯到早期UNIX家族发展的基于超级块和inode的经典概念,EXT4是EXT3的进一步升级。因此,本节以EXT2为模板介绍EXT文件系统的磁盘布局。图为EXT2文件系统的磁盘布局。
在EXT文件系统中,存储的最小单位是块(block),一个块究竟多大是在格式化时确定的,如可以设定块大小为1024、2048或4096字节,一旦文件系统创建好以后,块的大小就不能改变了。
这些块被聚在一起分成几个大的块组(block group)。每个块组中有多少个块是固定的(最后一个块组除外,其所管理的块可能会少一些)。而启动块(boot block)的大小是确定的,即1KB,启动块由PC标准规定,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。
启动块之后才是EXT文件系统的开始,EXT文件系统将整个分区划成若干个同样大小的块组,每个块组都由以下部分组成:超级块(superblock)、块组描述符表(GDT)、预留块组描述符表(reserved GDT)、块位图(block bitmap)、节点位图(inode bitmap)、节点表(inode table)和数据块(datablock)。
lbs@lbs-ubuntu:ext2$ dumpe2fs ext2.bin
dumpe2fs 1.42.9 (4-Feb-2014)
Filesystem volume name:
Last mounted on:
Filesystem UUID: d8adb320-d4f0-4178-a77c-086b4d1be550
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: ext_attr resize_inode dir_index filetype sparse_super
Filesystem flags: signed_directory_hash
Default mount options: user_xattr acl
Filesystem state: clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 2560
Block count: 10240
Reserved block count: 512
Free blocks: 9819
Free inodes: 2549
First block: 1
Block size: 1024
Fragment size: 1024
Reserved GDT blocks: 39
Blocks per group: 8192
Fragments per group: 8192
Inodes per group: 1280
Inode blocks per group: 160
Filesystem created: Mon Jul 11 17:08:52 2022
Last mount time: n/a
Last write time: Mon Jul 11 17:08:52 2022
Mount count: 0
Maximum mount count: -1
Last checked: Mon Jul 11 17:08:52 2022
Check interval: 0 ()
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 128
Default directory hash: half_md4
Directory Hash Seed: a81cffdd-28b9-4d10-8034-c4fe529e1553
Group 0: (Blocks 1-8192)
主 superblock at 1, Group descriptors at 2-2
保留的GDT块位于 3-41
Block bitmap at 42 (+41), Inode bitmap at 43 (+42)
Inode表位于 44-203 (+43)
7975 free blocks, 1269 free inodes, 2 directories
可用块数: 218-8192
可用inode数: 12-1280
Group 1: (Blocks 8193-10239)
备份 superblock at 8193, Group descriptors at 8194-8194
保留的GDT块位于 8195-8233
Block bitmap at 8234 (+41), Inode bitmap at 8235 (+42)
Inode表位于 8236-8395 (+43)
1844 free blocks, 1280 free inodes, 0 directories
可用块数: 8396-10239
可用inode数: 1281-2560
superblock:记录文件系统的整体信息,包含inode/block的大小、总量、使用量、剩余量,以及文件系统的格式,文件系统挂载时间,最近一次数据写入时间,最近一次校验磁盘的时间等。
超级块存储了EXT文件系统的全局配置参数(如数据块大小、总块数和索引节点总数等,这些参数在文件系统创建时就确定下来了)和动态信息(如当前空闲块数和空闲索引节点数等,可以在运行中改变)。对超级块的访问必须是互斥的,即任一时刻最多只能有一个进程访问超级块。
超级块记录了整个文件系统的各种信息,包括逻辑块的数量、inode数量、支持的特性和维护信息等内容。为了保证整个文件系统的完整性,例如突然断电或者系统崩溃等场景,文件系统出现元数据损坏的情况,Ext2文件系统对超级块进行了备份。这样可以保证即使在第一个超级块出现损坏的情况下,仍然可以通过其它块组中的超级块进行恢复,不至于整个文件系统都不可访问。
由于每个分区的块组0的特殊性,最开始的1024字节(即0~1号扇区)并未使用,而是留给了引导区。所以,从分区的第1024字节开始为超级块开始的位置。而块组1及其后面的块组没有引导区,也就没有上述1024字节的填充,块组从超级块开始。对超级块的数据结构定义在内核文件/include/Linux/EXT4_fs.h中,超级块中包含的内容及每个字段的意义如图所示。(每个分区的块组0 最开始的1024字节(即0~1号扇区) 引导区)
/*
* Structure of the super block
*/
struct ext2_super_block {
__le32 s_inodes_count; /* Inodes count */
__le32 s_blocks_count; /* Blocks count */
__le32 s_r_blocks_count; /* Reserved blocks count */
__le32 s_free_blocks_count; /* Free blocks count */
__le32 s_free_inodes_count; /* Free inodes count */
__le32 s_first_data_block; /* First Data Block */
__le32 s_log_block_size; /* Block size */
__le32 s_log_frag_size; /* Fragment size */
__le32 s_blocks_per_group; /* # Blocks per group */
__le32 s_frags_per_group; /* # Fragments per group */
__le32 s_inodes_per_group; /* # Inodes per group */
__le32 s_mtime; /* Mount time */
__le32 s_wtime; /* Write time */
__le16 s_mnt_count; /* Mount count */
__le16 s_max_mnt_count; /* Maximal mount count */
__le16 s_magic; /* Magic signature */
__le16 s_state; /* File system state */
__le16 s_errors; /* Behaviour when detecting errors */
__le16 s_minor_rev_level; /* minor revision level */
__le32 s_lastcheck; /* time of last check */
__le32 s_checkinterval; /* max. time between checks */
__le32 s_creator_os; /* OS */
__le32 s_rev_level; /* Revision level */
__le16 s_def_resuid; /* Default uid for reserved blocks */
__le16 s_def_resgid; /* Default gid for reserved blocks */
/*
* 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...
*/
__le32 s_first_ino; /* First non-reserved inode */
__le16 s_inode_size; /* size of inode structure */
__le16 s_block_group_nr; /* block group # of this superblock */
__le32 s_feature_compat; /* compatible feature set */
__le32 s_feature_incompat; /* incompatible feature set */
__le32 s_feature_ro_compat; /* readonly-compatible feature set */
__u8 s_uuid[16]; /* 128-bit uuid for volume */
char s_volume_name[16]; /* volume name */
char s_last_mounted[64]; /* directory where last mounted */
__le32 s_algorithm_usage_bitmap; /* For compression */
/*
* 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 */
};
在超级块中,魔术标签是一个很重要的字段,在EXT2/EXT3/EXT4中,这个字段的值为0xEF53,否则这个分区就不是一个正常的EXT2/EXT3/EXT4文件系统。
lbs@lbs-ubuntu:ext2$ hexdump -n 1024 -s 1024 ext2.bin
0000400 0a00 0000 2800 0000 0200 0000 265b 0000
0000410 09f5 0000 0001 0000 0000 0000 0000 0000
0000420 2000 0000 2000 0000 0500 0000 0000 0000
0000430 e8a4 62cb 0000 ffff ef53 0001 0001 0000 //ef53
0000440 e8a4 62cb 0000 0000 0000 0000 0001 0000
0000450 0000 0000 000b 0000 0080 0000 0038 0000
0000460 0002 0000 0001 0000 add8 20b3 f0d4 7841
0000470 7ca7 6b08 1b4d 50e5 0000 0000 0000 0000
0000480 0000 0000 0000 0000 0000 0000 0000 0000
*
00004c0 0000 0000 0000 0000 0000 0000 0000 0027
00004d0 0000 0000 0000 0000 0000 0000 0000 0000
00004e0 0000 0000 0000 0000 0000 0000 1ca8 ddff
00004f0 b928 104d 3480 fec4 9e52 5315 0001 0000
0000500 000c 0000 0000 0000 e8a4 62cb 0000 0000
0000510 0000 0000 0000 0000 0000 0000 0000 0000
*
0000560 0001 0000 0000 0000 0000 0000 0000 0000
0000570 0000 0000 0000 0000 0000 0000 0000 0000
*
0000800
另一个重要的字段是每个块占用的字节数,其对应的数据结构是s_log_block_size,标识文件系统中块的大小(以字节为单位)为(B=1<<(s_log_block_size+10))。例如,如果值为0时,则块的大小为1024B,也是最小的块大小;如果值为2,那么块的大小为4096B。
块组描述符表(GDT, Group Descriptor Table)由很多块组描述符组成,整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符存储一个块组的描述信息,如在这个块组中从哪里开始是inode表、从哪里开始是数据块、空闲的inode和数据块还有多少个等。与超级块类似,块组描述符表在每个块组的开头也都有一份拷贝,这些信息是非常重要的,一旦超级块意外损坏就会丢失整个分区的数据。通常内核只用到第0个块组中的拷贝,当执行e2fsck检查文件系统一致性时,第0个块组中的超级块和块组描述符表就会拷贝到其他块组,这样当第0个块组的开头意外损坏时就可以用其他拷贝来恢复,从而减少损失。
一个块组中的块是这样利用的,数据块存储所有文件的数据,比如某个分区的块大小是1024字节,某个文件是2049字节,那么就需要三个数据块来存储,即使第三个块只存储一字节也需要占用一个整块;超级块、块组描述符表、块位图、inode位图、inode表这几部分存储该块组的描述信息。那么如何知道哪些块已经用来存储文件数据或其他描述信息,哪些块仍然空闲可用呢?块位图就是用来描述整个块组中哪些块已用、哪些块空闲的,它本身占一个块,其中的每位代表本块组中的一个块,该位为1表示该块已用,为0表示该块空闲可用。
与块位图类似,本身占一个块,其中每位表示一个inode是否空闲可用。
我们知道,一个文件除了数据需要存储之外,一些描述信息也需要存储,如文件类型(常规、目录、符号链接等)、权限、文件大小、创建/修改/访问时间等,这些信息存在inode中而不是数据块中。每个文件都有一个inode,一个块组中的所有inode组成了inode表(节点表)。
inode表占多少个块在格式化时就要决定并写入块组描述符中,mke2fs格式化工具的默认策略是一个块组有多少个8KB就分配多少个inode。由于数据块占了整个块组的绝大部分,也可以近似认为数据块有多少个8KB就分配多少个inode,换句话说,如果平均每个文件的大小是8KB,当分区存满的时候inode表会得到比较充分的利用,数据块也不浪费;如果这个分区存的都是很大的文件(比如电影),则数据块用完的时候inode会有一些浪费;如果这个分区存的都是很小的文件(比如源代码),则有可能数据块还没用完inode就已经用完了,数据块可能有很大的浪费。如果用户在格式化时能够对这个分区以后要存储的文件大小进行预测,也可以用mke2fs的-i参数手动指定每多少字节分配一个inode。
inode:记录文件的属性,一个文件占用一个inode,inode大小(ext2中)大约为128B,并记录文件数据所在的block号码,具体来说包含的信息如下:
文件的字节数
文件拥有者的User ID
文件的Group ID
文件的读、写、执行权限
文件的时间戳,共有三个:ctime表示inode上一次变动的时间,mtime表示文件内容上一次变动的时间,atime表示文件上一次打开的时间
链接数:指有多少文件名指向这个inode
文件数据的block号码
在文件的block数量很大时,通常会采用多级block来记录block号码,这里采用Bitmap标记未使用的inode号码。
/*
* Structure of an inode on the disk
*/
struct ext2_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Creation time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks; /* Blocks count */
__le32 i_flags; /* File flags */
union {
struct {
__le32 l_i_reserved1;
} linux1;
struct {
__le32 h_i_translator;
} hurd1;
struct {
__le32 m_i_reserved1;
} masix1;
} osd1; /* OS dependent 1 */
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl; /* File ACL */
__le32 i_dir_acl; /* Directory ACL */
__le32 i_faddr; /* Fragment address */
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 */
};
根据不同的文件类型有以下几种情况:
1)对于常规文件,文件的数据存储在数据块中。
2)对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它所在目录的数据块中,除文件名之外,ls -l命令看到的其他信息都保存在该文件的inode中。注意这个概念:目录也是一种文件,是一种特殊类型的文件。
3)对于符号链接,如果目标路径名较短则直接保存在inode中以便更快地查找,如果目标路径名较长则分配一个数据块来保存。
4)设备文件、FIFO和socket等特殊文件没有数据块,设备文件的主设备号和次设备号保存在inode中。
block:实际记录文件的内容,若文件太大,则会占用多个block,通常的block大小有1K,2K,4K三种,这里内核记录block信息的数据结构是Bitmap。
块位图一个块组中的块是这样利用的,数据块存储所有文件的数据,比如某个分区的块大小是1024字节,某个文件是2049字节,那么就需要三个数据块来存储,即使第三个块只存储一字节也需要占用一个整块;超级块、块组描述符表、块位图、inode位图、inode表这几部分存储该块组的描述信息。那么如何知道哪些块已经用来存储文件数据或其他描述信息,哪些块仍然空闲可用呢?块位图就是用来描述整个块组中哪些块已用、哪些块空闲的,它本身占一个块,其中的每位代表本块组中的一个块,该位为1表示该块已用,为0表示该块空闲可用。
如果一个文件有多个数据块,这些数据块很可能不是连续存放的,应该如何寻址到每个块呢?实际上,根目录的数据块是通过其inode中的索引项Blocks[0]找到的,事实上这样的索引项一共有15个,从Blocks[0]到Blocks[14],每个索引项占4字节。前12个索引项都表示块编号,如Blocks[0]字段保存着24,就表示第24个块是该文件的数据块,如果块大小是1KB,这样可以表示从0~12KB的文件。如果剩下的三个索引项Blocks[12]到Blocks[14]也是这么用的,就只能表示最大15KB的文件了,这是远远不够的,事实上,剩下的三个索引项都是间接索引。如图所示。
索引项Blocks[12]所指向的块并非数据块,而是称为间接寻址块(IndirectBlock),其中存放的都是类似Blocks[0]这种索引项,再由索引项指向数据块,设块大小为b,那么一个间接寻址块中可以存放b/4个索引项,指向b/4个数据块。所以如果把Blocks[0]到Blocks[12]都用上,最多可以表示b/4+12个数据块,对于块大小是1KB的情况,最大可表示268KB的文件。二级索引指针可以管理的文件大小为64MB,三级索引指针可以管理的文件大小为16GB。
下表列出了EXT文件系统设置不同大小的数据块时,文件和文件系统的限长。
在EXT3中,对于大文件来说,存放数据需要经过几级间接索引,这就导致了存取大文件数据时的性能比较差。在实际应用中,测试表明数据不连续的情况不会超过10%,所以在EXT4中引入了区段的概念来表示文件数据所在的位置。区段是用来描述保存文件数据所使用的连续物理块的一段范围。每个区段由EXT4_EXTent结构类型来定义,占12字节,区段的结构类型定义在内核文件/include/Linux/EXT4_fs_EXTents.h中。
对文件的存储包括对文件数据(data)和文件属性(元数据,metadata)的存储。为了在意外情况(如系统崩溃)下,可以通过恢复程序保证数据的一致性和完整性,EXT3文件系统引入了日志的概念,EXT4还能对日志进行校验和,确保有效的数据变更能够在底层文件系统上正确完成。EXT3/4日志存放在journal文件中,并提供了三种日志的记录方式:
1)data=writeback方式:在这种方式下,EXT3文件系统只对元数据写日志。虽然这会让最近修改的文件在出现意外事件时损坏,但可以得到较高的速度。
2)data=ordered方式(默认方式):在这种方式下,EXT3文件系统也只将元数据写入日志系统,但还把对数据的每一次更新都作为一个事务写入相关文件中,这样有效地解决了第一种方式中文件数据被损坏的问题。但是文件系统的运行速度要稍慢些,且不能解决文件数据被覆盖时系统崩溃而无法恢复的问题。
3)data=journal方式:在这种方式下,EXT3文件系统提供了完整的文件数据和元数据的日志记录,再写入它的最终位置。这样在系统崩溃时,就可以通过日志来完全修复,保证文件数据和元数据的一致性。
(1)相当大的文件系统和更大的文件EXT3文件系统最多只能支持32TB的文件系统和2TB的文件,根据使用的具体架构和系统设置,实际容量上限可能比这个数字还要低,即只能容纳2TB的文件系统和16GB的文件。而EXT4的文件系统容量达到1EB,而文件容量则达到16TB,这是一个非常大的数字了。对一般的台式机和服务器而言,这可能并不重要,但对于大型磁盘阵列的用户而言,这就非常重要了。
(2)更多的子目录数量
EXT3目前只支持32000个子目录,而EXT4取消了这一限制,理论上支持无限数量的子目录。
(3)更多的块和inode节点数量EXT3文件系统使用32位空间记录块数量和inode节点数量,而EXT4文件系统将它们扩充到64位。
(4)多块分配当数据写入EXT3文件系统中时,EXT3的数据块分配器每次只能分配一个4KB的块,如果写一个100MB的文件就要调用25600次数据块分配器,而EXT4的多块分配器(Multiblock Allocator, MBAlloc)支持一次调用分配多个数据块。
(5)持久性预分配如果一个应用程序需要在实际使用磁盘空间之前对它进行分配,大部分文件系统都是通过向未使用的磁盘空间写入0来实现分配的,比如P2P软件。为了保证下载文件有足够的空间存放,常常会预先创建一个与所下载文件大小相同的空文件,以免未来的数小时或数天之内磁盘空间不足导致下载失败。而EXT4在文件系统层面实现了持久预分配并提供相应的API,比应用软件自己实现更有效率。
(6)延迟分配EXT3的数据块分配策略是尽快分配,而EXT4的策略是尽可能地延迟分配,直到文件在缓冲中写完才开始分配数据块并写入磁盘,这样就能优化整个文件的数据块分配,显著提升性能。
(7)盘区结构EXT3文件系统采用间接映射地址,当操作大文件时,效率极其低下。例如,一个100MB大小的文件,在EXT3中要建立25600个数据块(以每个数据块大小为4KB为例)的映射表;而EXT4引入了盘区概念,每个盘区为一组连续的数据块,上述文件可以通过盘区的方式表示为“该文件数据保存在接下来的25600个数据块中”,提高了访问效率。
(8)新的inode节点结构EXT4支持更大的inode节点。之前的EXT3默认的inode节点大小为128字节,EXT4为了在inode节点中容纳更多的扩展属性,默认inode节点大小为256字节。另外,EXT4还支持快速扩展属性和inode节点保留。
(9)日志校验功能日志是文件系统最常用的结构,日志也很容易损坏,而从损坏的日志中恢复数据会导致更多的数据损坏。EXT4给日志数据添加了校验功能,日志校验功能可以很方便地判断日志数据是否损坏。而且EXT4将EXT3的两阶段日志机制合并成一个阶段,在增加安全性的同时提高了性能。
(10)支持“无日志”模式日志总归会占用一些开销。EXT4允许关闭日志,以便某些有特殊需求的用户可以借此提升性能。
(11)默认启动Barrier磁盘上配有内部缓存,以便重新调整批量数据的写操作顺序,优化写入性能,因此文件系统必须在日志数据写入磁盘之后才能写Commit记录。若Commit记录写入在先,而日志有可能损坏,那么就会影响数据完整性。EXT4文件系统默认启用Barrier,只有当Barrier之前的数据全部写入磁盘,才能写Barrier之后的数据。
(12)在线碎片整理尽管延迟分配、多块分配和区段功能可以有效减少文件的碎片,但碎片还是不可避免会产生。EXT4支持在线碎片整理,并将提供e4defrag工具进行个别文件或整个文件系统的碎片整理。
(13)支持快速fsck以前的文件系统版本执行fsck时很慢,因为它要检查所有的inode节点,而EXT4给每个块组的inode节点表中都添加了一份未使用inode节点的列表,所以EXT4文件系统做一致性检查时就可以跳过它们而只检查那些在使用的inode节点,从而提高了速度。
(14)支持纳秒级时间戳EXT4之前的扩展文件系统的时间戳都是以秒为单位的,这已经能够应付大多数设置,但随着处理器的速度和集成程度(多核处理器)不断提升,以及Linux开始向其他应用领域发展,EXT4将时间戳的单位提升到纳秒。EXT4给时间范围增加了两个位,从而让时间寿命再延长500年,EXT4的时间戳支持的日期达到2514年4月25日,而EXT3只达到2038年1月18日。