EXT4 之 文件系统在磁盘上的布局 之一

前言

这篇文章尝试介绍下ext4文件系统在磁盘上的表现形式。对于ext2/3来说,大致相同,只不过ext2/ext4不可能支持ext4的全部特性。

对于EXT4 文件系统在磁盘上的布局的介绍一直在完善中。这篇文章主要主要动力室名为djwong对于文件系统知识追根溯源的态度。一些数据结构的定义是基于Linux4.12 和 e2fsprogs-1.43。欢迎及时反馈问题。

术语

ext4 将存数设备独立的划分为一组组大小相同逻辑块,不仅仅是为了减少文件管理的开销而且用于提高大文件的传输效率。通常来说块大小会被设置为4KiB(留一下,X86架构上的页大小也是4KiB),但是这个块大小其实是可以修通过2 ^ (10 + sb.s_log_block_size) 来修改的(block size = *2 ^ (10 + sb.s_log_block_size) *bytes)。在本文中,我们约定磁盘的块以逻辑块的形式来表现,不用raw LBAs、不用1024-byte block。同样的,为了方便起见,逻辑块的大小通常用$block_size来替代。事先声明下,sb代表superblock,inode代表一个真实的inode table 实体。

综述

ext4文件系统被分割称为了一系列的block groups。为了尽可能的减少因为磁盘碎片引起的种种性能问题,磁盘的块分配程序尽最大可能的在同一个group中新分配的块,这样可以减少seek的用时。block groups的大小由sb.s_block_per_groupblocks指定,当然它也可以由8*block_size_in_bytes计算得到。默认的block size是4K,每个group包含32768个blocks;所以,每个group的长度为128MB(4k*32768)。block group的数量就是当前磁盘的大小除以block group的大小。ext4文件系统中的所有域都是以小端的形式存储的。但是,jbd2(journal)中是以大端的形式存储。

块Blocks

ext4 申请空间的最小单位是’blocks’。块可以看做是1KiB和64KiB之间的一组扇区,扇区的数量必须是2的幂。大量的block被组织成一个更大的单元,我们称之为block group。每个block的大小在使用mkfs命令创建文件系统时指定为4K。如果在使用mkfs时指定的block size大小大于 page size,那么在mount的时候就会出错哇。通常情况下文件系统所能拥有的块个数为2^32;对于64位系统,则能拥有的块个数为2^64.
详细如下图所示:
这里写图片描述

Note: Files not using extents must be placed within the first 2^32 blocks of a filesystem. Files with extents must be placed within the first 2^48 blocks of a filesystem. It’s not clear what happens with larger filesystems.

布局

一个标准的block group布局如下表所示(对于其中的每一项的实际含义及其值,稍后会单独讨论)

Group 0 Padding ext4 Super Block Group Descriptors Reserved GDT Blocks Data Block Bitmap inode Bitmap inode Table Data Blocks
1024 bytes 1 block many blocks many blocks 1 block 1 block many blocks many more blocks

block group 0这个group有点儿特殊,他的前1024bytes是预留的,主要用于存储x86的一些跟启动扇区相关的信息。superblock的起始位置当然就需要偏移1024 bytes咯,但是superblock却不关注到底是存储在那个block中,虽然一直都是被存储在block为0的块中。但是也有例外,你比如block size =1024,那么superblock就自然不会被存储到block 0里头了,superblock自然就被存储到block 1里头了。对于当前block group的其他block不会被自动填充。

对于superblock来说,最经常被用到的就是block group0中所存储的superblock信息了。因为superblock、group 描述信息不光只有一份,他是在磁盘的其他地方(或者是前台block group)有冗余的。如果当前的group没有冗余备份的话,那么当前block group的起始地址从存储bitmap数据开始算起。还要注意,当文件系统刚刚格式化时,mkfs将在块组描述符之后分配“reserve GDT块”空间,并且在块位图开始之前,允许文件系统的未来扩展,在原有文件系统上拓展的大小必须是1024的整数倍。inode table所处的‘地址’由grp.bg_inode_table_*给出,其大小足以包含sb.s_inodes_per_group*sb.s_inode_size bytes.

可调整的block group(Flexible Block Group)

从ext4开始,新增了一个新的名为flex_bg的功能。在flex_bg中,一系列block group被关联在同一个逻辑block group中;bitmap、inode table位于组成该flex_bg的block group的第一个block group中,并且他们被拓展啦,从只代表一个block group拓展为代表当前flex_bg的bitmaps、inode tables啦。例如:假设flex_bg的大小是4,那么group 0中会存储superblock、group descriptor、group 0-3的bitmaps,group0-3的inode tables,group0中剩余的空间用于存放数据。这样做的最直接目的是将块元数据组合在一起,以便更快地加载,并使大文件在磁盘上连续存储。不论是否有flex_bg功能,superblock、group descriptor的冗余部分始终在block group的起始位置。可用于flex_bg的block groups数量则由2^sb.s_log_groups_per_flex定义。

元组块(Meta Block Groups)

通常情况下,每一份superblock的冗余都会将整个block group的descriptor记录下来。假设默认group 大小为2^27bytes(128MB),group descriptor占用64 bytes(2^6)的大小;那么当前文件系统最多能组织起2^21(2^27 vs 2^6)个block groups(2^21个block group 0x200000 ,最大为:128*0x200000 MB=0x1000 0000 MB =0x40 0000 GB =0x100 TB )。为了解决只支持256TB的情况,其中一个方案就是medablock group feature(META_BG),这个功能已经在ext3中被实现啦。在META_BG方案中,ext4文件系统被划分成为了很多很多medablock groups,,对于ext4 文件系统来说,block大小为4k,一个单独的metablock group分区包含了64个block group,也就是8GB的磁盘空间。medablock group方案将存储在每个group 中第一个block中的group descriptor移动为medablock group为单位的第一个block group,冗余备份被放在了每个metadata block第二个和最后一个group中。这样,系统所限制的最大支持2^21个block group就增加到了极限的2^32个 block group,当前文件系统所支持的最大磁盘大小为512PiB(128*0x1000 0000 MB=0x80 0000 0000 MB =0x2000 0000 GB =0x8 0000 PB=0x200 PB )。
其实,meta-block group可以看做是block group的 group。由于block group descriptor结构的大小为32 bytes,当block size为1k是,一个meta-block group 包含了32 个block groups,当block siz为4k时,一个meda-block group包含128个block groups。文件系统可以使用现有的一些block group方案还创建,当然也可以进行动态的修改。superblock中的s_first_meta_bg变量就是代表了第一个block group是否使用了新的布局。更详细的资料,请参照BLOCK_UNINIT章节中关于block、inode bitmap的描述

block group推迟初始化

ext4中有一个新特性,3个block group descriptor标识使得mkfs跳过对meta-block中其他block group的初始化。特别是,INODE_UNIINTBLOCK_UNINIT两个标识就代表了block group中的inode 和 block bitmap是可以计算的,因此其他的bitmap block就不用进行初始化。INODE_ZERO标识就疑问这inode table已经被初始化了,mkfs并不会对该标识进行设置,这个标识会依赖后台的内核进程去初始化。
由于不用对bitmp 、inode table进行写零初始化,因此mkfs的效率有了大大的提升。需要注意的一点事,在文件系统中实际使用到的标识时RO_COMPAT_GDT_CSUM,但是dumpe2fs命令输出的是unint_bg,其实他们是同一个标识

特殊的inode(Special inodes)

ext4 保留了一些inode用于支持一些特殊的功能,这些特殊的inode如下所示:

Inode Purpose
0 Doesn’t exist; there is no inode 0.
1 List of defective blocks.
2 Root directory.
3 User quota.
4 Group quota.
5 Boot loader.
6 Undelete directory.
7 Reserved group descriptors inode. (“resize inode”)
8 Journal inode.
9 The “exclude” inode, for snapshots(?)
10 Replica inode, used for some non-upstream feature?
11 Traditional first non-reserved inode. Usually this is the lost+found directory. See s_first_ino in the superblock.

Block和Inode分配策略

不论怎样,ext4文件系统的‘数据局部性Data Locality’是要大大的由于ext3的,是现有文件系统中搞得最好的。对于机械磁盘(通常是指需要使用探针访问的那种),将相近相似、最近经常使用的数据存放在一起,这样就减少了在访问数据时磁头所需要移动的偏移量,进而能大幅度提升磁盘的IO性能;对于SSD来说,就不存在磁头所移动的偏移量了,但是’data locality’可以显著增加单次请求中的数据量,从而减少请求次数。而且’data locality’机制能很好的控制分配空间的连续性,减少磁盘碎片。

  • 在ext4中第一种用来减少磁盘碎片问题的方法就是multi-block分配。当一个文件首先被创建(打开)的时候,系统会自动的预分配一个8kb的空间,用于数据的存储;当文件被关闭时,没有被用到的预分配的空间也会被同时释放。(When the file is closed, the unused speculative allocations are of course freed, but if the speculation is correct (typically the case for full writes of small files) then the file data gets written out in a single multi-block extent.)
  • 第二种减少碎片的小技巧就是使用延时分配的策略。在这种延时策略的指导下,当一个文件需要更多的空间的时候,文件系统决定将分配空间的时间进行延时,直到所有的脏buffer开始写入磁盘。但是,如非特殊需求,不建议使用。
  • 第三种情况就是,文件系统尽可能的把同一个文件的数据库向inode一样存放在同一个block group中。这样的话,就能显著的减小文件系统因为seek导致的耗时。
  • 第四种情况,如果能行的通,那么在同一个directory中的inode作为一个directory被放置在同一个block中。提出这种方法是基于这样一个假设:在同一个目录下的文件都是有关联的,所以我们才将他们尽可能的放在一起。
  • 第五种情况,将磁盘卷切割为128MB的block groups,用这种方法来维护数据的局部性。但问题是,存在这样一种情况,当我们在root目录下创建一个目录是,inode分配器会将block groups扫描一遍,并将目录放到它所能找到的负载最小的block group中。

如果上面的五种方法都失败啦,还有一种方法就是使用e4defrag来减少碎片问题。

校验(Checksums)

早在2012年,medadata checksum就被添加到了ext4和jbd2数据结构中啦,同时也在metadta 功能标识中新增一个metadata_csum标识。在superblock中的一个标识标示了checksum所用到的算法,在2012.10的时候,该算法支持crc32c。一些数据结构没有足够的空间来存放完整的32bytes的checksum数据,所以,就只存储了16bytes。但是当开启了64bit时,相关的数据结构就有空间存放32 bytes的数据啦。然而,现有的32bit文件系统,是不能被直接的拓展为64bit的的。
对于一个可以获得的文件系统是可以通过tune2fs -O metadata_csum命令来将checksum信息写入磁盘的。在执行tune2fs时,如果遇到了没有足够的空间来存储checksum信息是,他会提示你用e2fsck -D将directory重新进行排列一下。这有利于文件系统对其数据存储的优化。如果在这个过程中还用了_ignore_标识,那么你的目录是不受保护滴。下面的表格描述了数据项在各种checksum情况下的表现。checksum函数是由superblok指明的,除非另有说明。

Metadata Length Ingredients
Superblock __le32 The entire superblock up to the checksum field. The UUID lives inside the superblock.
MMP __le32 UUID + the entire MMP block up to the checksum field.
Extended Attributes __le32 UUID + the entire extended attribute block. The checksum field is set to zero.
Directory Entries __le32 UUID + inode number + inode generation + the directory block up to the fake entry enclosing the checksum field.
HTREE Nodes __le32 UUID + inode number + inode generation + all valid extents + HTREE tail. The checksum field is set to zero.
Extents __le32 UUID + inode number + inode generation + the entire extent block up to the checksum field.
Bitmaps __le32 or __le16 UUID + the entire bitmap. Checksums are stored in the group descriptor, and truncated if the group descriptor size is 32 bytes (i.e. ^64bit)
Inodes __le32 UUID + inode number + inode generation + the entire inode. The checksum field is set to zero. Each inode has its own checksum.
Group Descriptors __le16 If metadata_csum, then UUID + group number + the entire descriptor; else if gdt_csum, then crc16(UUID + group number + the entire descriptor). In all cases, only the lower 16 bits are stored.

大文件的分配(Bigalloc)

在通常情况下,默认的block size是4k,这也是MMU所能支持的最优的打消了。幸运的是,ext4在设计之初就没有将block size的大小定义超过page size的大小。但是,从文件系统角度来说,就经常会处理到超大型文件,希望在内存分配的时候尽可能的减少碎片问题和数据开销。bigalloc特性为我们提供了这种能力。可以通过mkfs命令来设置block族的大小(被存储在superblock结构体的s_log_cluster_size中);这样一来,bitmap就只是追踪族而不是一个个独立的块了。换句话说,block group可以是GB级别的啦(而不再是之前的128MB咯),但有个问题,一旦我们这样高了,那么他最低的单位也就是族了,哪怕是只针对一个目录。TaoBao在这方面貌似有一个方案,但是具体的细节不太清楚(TaoBao had a patchset to extend the “use units of clusters instead of blocks” to the extent tree, though it is not clear where those patches went– they eventually morphed into “extent tree v2” but that code has not landed as of May 2015. 吊炸天有木有。。)

内嵌数据(Inline Data)

内嵌数据是为那些超小的、只存储在一个inode中文件数据而设计的,理论上来说是可以减少磁盘、seek的消耗。当文件小于60 bytes的时候,该文件的数据就被存储在了inode.i_block里面(ibody )。如果文件的剩余部分可以用拓展的属性空间存储,那么就会存在一个名为system.data在inode中的拓展属性。当然了,对于拓展属性的数量肯定是有限制的。如果数据的大小大于i_block+ibody,就会为其分配一个block并将数据移动到该block中。
但是这种技术被推迟了;主要原因是效率太低了。
内嵌数据功能时依赖于system.data这个属性的,主要有就行。

内嵌目录(Inline Directories)

实在是不想翻译了。。。
The first four bytes of i_block are the inode number of the parent directory. Following that is a 56-byte space for an array of directory entries; see struct ext4_dir_entry If there is a “system.data” attribute in the inode body, the EA value is an array of struct ext4_dir_entry as well. Note that for inline directories, the i_block and EA space are treated as separate dirent blocks; directory entries cannot span the two.
Inline directory entries are not checksummed, as the inode checksum should protect all inline data contents.

struct ext4_dir_entry {
        __le32  inode;                  /* Inode number */
        __le16  rec_len;                /* Directory entry length */
        __le16  name_len;               /* Name length */
        char    name[EXT4_NAME_LEN];    /* File name */
};

/*
 * The new version of the directory entry.  Since EXT4 structures are
 * stored in intel byte order, and the name_len field could never be
 * bigger than 255 chars, it's safe to reclaim the extra byte for the
 * file_type field.
 */
struct ext4_dir_entry_2 {
        __le32  inode;                  /* Inode number */
        __le16  rec_len;                /* Directory entry length */
        __u8    name_len;               /* Name length */
        __u8    file_type;
        char    name[EXT4_NAME_LEN];    /* File name */
};

Orphan File [PROPOSED]

实在是不想翻译了。。。
Proposed by Jan Kara in early 2015, the orphan file feature aims to reduce locking contention during delete operations by replacing the singly linked orphan inode list (and lock) with a file containing multiple blocks. Each CPU ought to be able to claim its own block, which implies that the orphan list can be updated locklessly. Each block contains a list of orphaned inodes; recovery involves iterating all blocks of the orphan file looking for non-zero inode numbers to erase. This feature will come with a rocompat feature flag to indicate the ability to use an orphan file and a compat flag indicating that the orphan file actually contains orphaned inode records. The format of an orphan file block is as follows:

Field Length Description
inodes __le32[] A list of orphaned inodes. 0 means the entry is empty. The length of the array is the size of the block minus the tail.
magic __le32 Magic number of orphan file blocks, 0x0B10CA04.
checksum __le32 Checksum of the UUID + inode number + inode generation + the orphan block up to but not including the footer.

The inode number of the orphan file itself has not been settled; as of May 2015 the test patches re-use inode #9.

常用的拓展属性(Large Extended Attribute Values)

一方面需要让ext4支持拓展属性,一方面呢原始inode又对属性支持的不太好。针对这EA_INODE就应运而生了,EA_INODE特性允许我们将拓展属性存储到inode中。这种EA_INODE是由拓展属性的名称索引连接过来的,对于目录来说是不会出现的。Inode的i_atime变量用于存储xattr value的checksum值,i_ctimei_version存储了一个64 bit的引用值,用来开启在多个inode之前所共享的large xattr value。为了向下兼容,i_mtimei_generation存储了备份的inode number。

The Super Block

superblock记录了一系列整个文件系统系统的诸如块个数,inode个数,支持的功能,校验信息等等。如果sparse_super功能开启了,那么superblock的冗余只存在与group number为0或者3 、5、7次幂的group number中;如果没有被设置,那么superblock在所有的group中都有冗余备份。
ext4 superblock由如下数据结构定义struct ext4_super_block
superblock struct
合计大小1024 bytes

未完…待下一篇博文补充。

英文的原文参考这里

猜你喜欢

转载自blog.csdn.net/cpwolaichile/article/details/74455352
今日推荐