Linux ext4 文件系统的最简完整教学(译)持续更新中
Comment根据原文要求,本文以 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License 进行许可。
原文作者:Mete Balci
原文:A Minimum Complete Tutorial of Linux ext4 File System
2017 年 8 月 25 日
翻译:Tilnel @ 2025 年 8 月
为了避免歧义,译文的叙述部分使用 1Ki(B) = 1024(B) ,依此类推,而不是原文的 K(B)。
#引言
我尽量使用例子简洁而全面地描述 Linux ext4 文件系统。
声明:我不是 ext4 方面的专家。尽管我写这篇文章的目标是使其尽量全面足以理解 ext4 如何工作,但也忽略了很多特性。所以这并不是完整的描述。
我管这个叫做“最小完整教程”是因为我会忽略一些可选的部分,以及一些额外的特性,以此保持简洁。在此基础上全面地描述 ext4 的所有功能。ext4 并不简单,所以完全读完可能需要几个小时。
我没有按照先后顺序去执行教程中的例子,所以由于文件系统是动态的如果你自己去尝试执行的话,可能得到不一样的结果。不过这并不影响我们理解原理。
#相关资料
我参考了 Ext4 Disk Layout documentation,ext4 文件系统在 Linux Kernel 中的源码,e2fsprogs(包括 debugfs 和 dumpe2fs),以及 coreutils 的源码(包括 ls)。
#历史
从 2010 年开始,ext4 就是 Linux 的默认文件系统,它是 ext2、ext3 的继任者。“ext” 的意思是 “extended”,第一个版本的文件系统就叫这个。在 1992 年左右它被采用了很短一段时间后就被 ext2 取代了。2000 年时,支持文件系统日志的 ext3 也诞生了。
#教程
#创建 ext4 文件系统
如果你在用 Linux,可能你已经把 ext4 作为你的主文件系统了。不过,为了保护你的文件,我们还是重新创建一块来供我们实验吧。这里我在 /dev/sda 的一个 U 盘上创建了一个 ext4 文件系统。
首先我创建了一个包含了一个 Linux 分区的 GUID 分区表(GPT)。如果你对 GPT 和 Logical Block Addressing(逻辑块寻址,LBA)不熟,推荐你先阅读我的这篇文章:A Quick Tour of GUID Partition Table (GPT)。
小心!使用 fdisk 和 mkfs 的时候要非常非常注意。确保你写入的设备上没有重要数据。
这是分区表:
1 | $ sudo fdisk /dev/sda |
逻辑块 [1]大小是 512 字节。分区从第 2048 个逻辑块开始,到第 31266782 个逻辑块结束。
我们就在这个分区上创建文件系统。
1 | $ sudo mkfs -t ext4 /dev/sda1 |
mkfs
,制作文件系统(make file system),通过 -t
指定文件系统,这里是 ext4
。mkfs
其实是一个 wrapper,它会根据文件系统调用对应的 mkfs.<fs>
。所以这里是 mkfs.ext4
在执行。
这里我们可以看到:
- 一个包含了 3908091 个块的文件系统被创建。
- 块大小是 4096 字节,这是自动选择的。我们也可以使用别的块大小。块大小和逻辑块大小(LBA所使用的)并不一样。这里说的约 4 百万个 “block” 指的是文件系统块而不是逻辑块。
- 有 977280 个 “inodes”。我们之后将会了解这个。
- 文件系统 UUID 就是 GPT 中的分区 UUID。
- 有 group tables,inode tables 和 journal(日志)。
- 最后有一个统计数据。我在 mkfs 的源码中找了找,发现它实际上也就写了一些文件系统信息。所以文章也不会特别提到。
文章将会解释所有这些和相关概念。
#Superblock
我们从 Superblock 开始,因为它是包含了文件系统基本信息的一片位置固定的区域。我们可以使用 dumpe2fs
的 -h
仅打印 Superblock 选项来导出其内容。
1 | $ sudo dumpe2fs -h /dev/sda1 |
这边信息非常多,我会解释其中的大部分。
ext4 最基础的存储布局概念就是 Block(块)。ext4 以块为单位分配存储空间,就像是 LBA 一样。然而 ext4 Block 大小可能不一样。在 Superblock 中是 4096B = 4KiB。文件系统中的 Block 总数是 3908091 个。
第二个概念是 Block Group(块组),也就是固定数量的连续的块。在我们的文件系统中 Group 大小是 32768 = 32Ki。
我们有 3908091 个block。将其除以 32Ki:
1 | 3908091 / 32768 = 119, 因为它不是 32Ki 的整数倍, |
所以我们有 119 个 32K Blocks 的 Groups 和一个小的 Group。每个 Block 是 4KiB。由于每个逻辑块(扇区)是 512B,这意味着每个块有 8 个扇区。这就是 ext4 的基本存储安排,如下图所示。
每一个块组都包含:
- Superblock,包含文件系统的信息。位于每组的第一个块。
- (块)组描述符(表)【(Block) Group Descriptors (Table)】包含了数据块 Bitmap,inode Bitmap 和 inode Table 在块组中的位置。每个块组都有一个组描述符。根据组大小的不同,它有可能占据一块或多个块,并且总是跟在 Superblock 后面。
- 预留的组描述符表 块。这些是为了未来扩展文件系统的时候预留的。ext4 文件系统的大小可以调整,如果是扩展的话,就意味着有更多的块,更多的块组。更多的组描述符就需要空间。通常是紧跟在 GDT 后的 0 或更多个 block。
rezise_inode
(在 Superblock 的 Filesystem features 中)标志了预留存在与否。 - 数据块 Bitmap。标识了哪些数据块已经被使用了。它占据一个 block,位置不固定,在组描述符中指定。
- inode Bitmap。标识了 inode Table 中的哪些项(entries)已经被使用了。它占据一到多个 blocks(经常是多个)并且位置不固定,由组描述符指定。
- 数据 Blocks。包含了实际的文件内容。占据一到多个 blocks。块组里除去上文所述,剩下的 block 都属于这部分。
在 Bitmap 中,每一个 bit 都标识了一个对应的 block/inode 的使用情况。由于 Bitmap 占据 1block = 4KiB = 32Kibits,故而块组中至多可以有 32Ki 个 blocks——事实上也是这样。并且,一个块组中最多可以有 32Ki 个 inodes,但上面的 Superblock 信息表示,我们只有 8144 个。
再回头看看 Superblock:
- 预留的 GDT 块有 954 个。用作未来文件系统的扩展。
- 每组的 inode block 有 509 个。这个数字与 inode size 以及每组的 inode 数量紧密相关。注意到 $256 B * 8144 / 4096 B = 509$。
目前为止我们还没有看到 ext4 文件系统布局的全貌。Superblock 的 Filesystem features 中还有两个至关重要的标志在影响着布局:sparse_super
和 flex_bg
。
#Superblock Backups
Superblock 包含了文件系统的重要信息,丢失就等于文件系统完全损坏了。因而在设计的时候,这些 Superblock 在每个块组当中都有备份,或者说是冗余。然而这可能有点多了,比如在我们的例子中,其实是一个很小的文件系统,120 个块组难道真的要有 120 个备份吗?sparse_super
就标志了这样一件事,即只有少量块组存在 Superblock 的备份,特别是 0 号块组,以及编号为 3, 5, 7 的幂次的块。
对于我们的这个案例,从 0 到 119 号块组,以下的块组是包含了冗余的:
1 | 1 3 5 7 9 25 27 49 81 |
如果我们把每个数都乘上 32768 (=32Ki, 每组的块数),就得到了 mkfs 输出过的 Superblock 的备份块:
1 | 32768 98304 163840 229376 294912 819200 884736 1605632 2654208 |
sparse_super
也会影响组描述符。在默认情况下,组描述符在每个块组中都会有。但如果 sparse_super
被标记,组描述符就只会出现在上列含有 Superblock backup 的块组中。可以认为 Superblock 和组描述符总是在一起。
#块组和弹性块组(Flexible Block Groups)
另一个标志 flex_bg
代表 Flexible Block Groups,也就是弹性块组。这个特性是在 ext4 引入的。FlexBG 简单来说就是块组之上的一级管理,也就是“块组的组”,它把整个大组内所有块组的数据块 bitmap、inode bitmap 和 inode table 都聚合在第一个块组中。
继续观察之前的 Superblock,可以发现 FlexBG 大小是 16,所以每 16 个连续的块组就打包成一整个弹性组。每个弹性组的第一个块组就会包含下辖所有块组的数据块 bitmap,inode bitmap 和 inode table。
我们有:
1 | 120 / 16 = 7 ... 8, 7 个 16BG 和 1 个 8BG 的 FlexBG |
注意:我们这里并没有
meta_bg
的 feature。这个 feature 对于块组的安排又是不一样的。
为了理解块组的内容,我认为大体可以将其简化为 3 类(请理解这只是我的描述方式,而不是官方的分类):
- Type 1a(在 Block Size = 1024B 情况下的 0 号块组,Block 0):Block 0 被标记为使用但实际没有使用(因而前 1024B 都没有被使用)。Block 1 包含了 primary Superblock(主 Superblock,对应备-backup)。接下来是连续的组描述符块和预留的组描述符块。后面和 Type 3 相同。
- Type 1b(在 Block Size > 1024B 情况下的 Block 0):Block 0 的首部 1024B 作为 padding,没有被使用。从 1024B 的偏移量开始,是 primary Superblock。接下来与 1a 相同。
- Type 2:包含了 Superblock 和组描述符备份的块组。Block 0 是 Superblock,接下来与 Type 1a 相同。
- Type 3:不包含 Superblock 和组描述符备份的块组。可能包含数据块 Bitmap,inode Bitmap,inode Table,以及数据块。Bitmap 和 inode Table 的位置在组描述符中记录,所以顺序可能有变。
Block 0 首部的 1024B 可能被用作启动码,因而 ext4 不会使用它。
Superblock 中标识了 “First block value”。它只有在 Block Size = 1024 B 的时候才等于 1。其他时候都等于 0。
1 | $ mkfs -t ext4 -b 1024 /dev/sda |
在我们的例子中:
- Type 1b 的 Block Group 有 1 个(0)
- Type 2 的有 9 个(包含了 Superblock 冗余的块)
- 剩下全部 110 个都是 Type 3
当设置了弹性块组的时候,这些每类块组又分为两类,因为在弹性块组中只有第一个块组[2]中才有 Bitmaps 和 inode Table。这里我会将它们命名为 Type 2-head / Type 2-rest,以此类推。Type 1 总是 head,因为它总是 Flex BG 中的 0 号。
同时,Type 2-head 块组是不可能的。因为备份块的序号总是 3, 5, 7
这些奇数。而 Flex BG 的 size 总是 2 的乘方,也就是偶数,意味着其头部块组的序号也总是偶数。所以 Type 2 作为备份快,不可能位于 Flex BG 的头部。
现在我们已经完全搞清楚我们的 ext4 文件系统的块组和块的排布了。事实上我们可以用 dumpe2fs
输出所有 Block Groups 的信息。
1 | $ sudo dumpe2fs /dev/sda1 > dump |
由于包含了所有块组(120个)的信息,文件会很长。下面逐一地去介绍。
第一个块组(Block Group 0):
1 | Group 0: (Blocks 0-32767) [ITABLE_ZEROED] |
正如我们所知道的:
- 它包括了 0-32767 号块,因为每个块组有 32Ki Blocks
- Block 0 包含了 Superblock
- Block 1 包含了组描述符(且只有 Block 1 有)
- Block 2-955 是预留的 GDT Blocks
- Block 956 是数据块 Bitmap
- Block 972 是 Inode Bitmap(我们后面再解释为什么不是 957)
- Block 988-1496 是 inode table。$1496 - 988 + 1 = 509$,刚好和 Superblock 的信息对得上
也有不用 dumpe2fs
的办法,那就是手动解析磁盘的数据。
组描述符是从第二个 block 开始的,所以我们用 dd bs=4096 skip=1
。前三个 32 位的数是数据块 Bitmap、inode Bitmap 和 inode Table 的位置。
1 | $ sudo dd if=/dev/sda1 bs=4096 skip=1 count=1 status=none | hexdump -n 12 -s 0 -e '"%d %d %d\n"' |
块组 0 在本文的分类学下是 Type 1b (head)。
第二个块组(Block Group 1):
1 | Group 1: (Blocks 32768-65535) [INODE_UNINIT, ITABLE_ZEROED] |
- 它有 Superblock backup。因为 $ 1 = 3 ^ 0$
- 因为有 Superblock backup,所以也有组描述符和预留的块
- 它在事实上没有数据块 Bitmap,inode Bitmap 和 inode Table。这些都引用了 Block Group 0 中的(e.g. Block Group 1 的数据块 Bitmap 在 bg #0 + 957)因为 Block Group 1 和 0 在同一个 Flex BG 中。inode Bitmap 和 inode Table 同理。这也就是为什么在 Block Group 0 中,数据块 Bitmap 和 Inode Bitmap 并不是连续的区域了。一个从 956 开始,一个从 972 开始,中间就是 16 Blocks,它们包含了所有块组的数据块 Bitmap 信息(从 0 到 15,$[956, 972)$)。以此类推,inode Bitmap 就是 $[972, 988)$,inode Tables 就是 $[988, 9132)$ (
989 + 16 × 509 = 9132
)
这里我有点困惑,也许你也有点感觉。在 Block Group 0 中,为什么 inode Table 在 9132 结束,但是可用空间从 9138 开始呢?其实 9132-9137 是已经在使用的数据块。所以这里并没有什么特别的怪事发生。如果重复实验的话,可能会得到不一样的结果。
所以在概念上我们其实不能说数据块在遵守这些元数据规定的结构,而是所有的块都是数据块,只不过有一些用来存储元数据了。存储元数据的块在数据块 Bitmap 中也是要被标记使用的。
Block Group 1 在本文的分类学里面是 Type 2-rest。它有 Superblock 备份,但不是弹性块组的第一个(因此没有 Bitmaps 和 inode Table)。
接下来是 Block Group 2:
1 | Group 2: (Blocks 65536-98303) [INODE_UNINIT, BLOCK_UNINIT, ITABLE_ZEROED] |
- 没有 Superblock backup。也没有 GDT、RGDT(组描述符表和预留)
- Bitmaps 和 inode Table 都在 BG #0
BG #2 属于 Type 3-rest。
接下来我们看看在下一个 Flex BG 的 BG #16:
1 | Group 16: (Blocks 524288-557055) [INODE_UNINIT, ITABLE_ZEROED] |
没有 Superblock backup。也没有 GDT、RGDT(组描述符表和预留)
存储了 Flex BG 中所有 BG(#16-#31)的 Bitmaps 和 inode Tables
BG #16 属于 Type 3-head.
到这里,所有的分类都看过一遍了。
上面每个组的第一行最后都有几个 flag,它们通过节省初始化的时间来加速 mkfs 。
INODE_UNINIT
[3]:inode Table 不清零。在被使用的时候,它们会自然地被覆盖。
ITABLE_ZEROED
:inode Bitmap 会被清零。它们标识了 inode Table 中的使用情况,所以是必须初始化的。
BLOCK_UNINIT
:数据块未初始化。等到有数据来的时候直接覆盖就好。这其实就是懒惰初始化。
这些特性是被 filesystem feature
uninit_bg
决定的。参见 Superblock。
- 1.这里原文写成了 Logical Block,实际上是 Sector 也就是扇区。注意与 Block 相区别。 ↩
- 2.Error:原文此处是 “since only the first block of Flex Group...” 应为 “first block group of”
Warning:文章证明了 1 总是 head,接下来又要证明 2 不可能是 head。说实话应该首先论证这两个不可能,然后提出 Type 3 可以划分,而不是先划出 3 个新的再取消其中两种。看的头昏。 ↩ - 3.原文下面这段描述是错的。这里改过来了。原文如下:
INODE_UNINIT: inode Bitmap is not zeroed, because it can be calculated on-the-fly (e.g. no inodes are used so all of them are free).
ITABLE_ZEROED: inode Bitmap is zeroed.
BLOCK_UNINIT: Data Block Bitmap is not zeroed. (e.g. no data stored so all of them are free). ↩