根据原文要求,本文以 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ sudo fdisk /dev/sda

Welcome to fdisk (util-linux 2.27.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Command (m for help): p
Disk /dev/sda: 14.9 GiB, 16008609792 bytes, 31266816 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 68B7F722-7E83-47F7-BCCC-2C7591B95E0C

Device Start End Sectors Size Type
/dev/sda1 2048 31266782 31264735 14.9G Linux filesystem

逻辑块 [1]大小是 512 字节。分区从第 2048 个逻辑块开始,到第 31266782 个逻辑块结束。

我们就在这个分区上创建文件系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ sudo mkfs -t ext4 /dev/sda1

mke2fs 1.42.13 (17-May-2015)

Creating filesystem with 3908091 4k blocks and 977280 inodes
Filesystem UUID: 5ae73877-4510-419e-b15a-44ac2a2df7c6
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208

Allocating group tables: done
Writing inode tables: done
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done

mkfs,制作文件系统(make file system),通过 -t 指定文件系统,这里是 ext4mkfs 其实是一个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
$ sudo dumpe2fs -h /dev/sda1

Filesystem volume name: <none>
Last mounted on: <not available>
Filesystem UUID: 8eefa5bb-858c-4bd0-b80d-1aebc23de317
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent flex_bg sparse_super large_file huge_file uninit_bg dir_nlink extra_isize
Filesystem flags: signed_directory_hash
Default mount options: user_xattr acl
Filesystem state: clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 977280
Block count: 3908091
Reserved block count: 195404
Free blocks: 3804437
Free inodes: 977269
First block: 0
Block size: 4096
Fragment size: 4096
Reserved GDT blocks: 954
Blocks per group: 32768
Fragments per group: 32768
Inodes per group: 8144
Inode blocks per group: 509
Flex block group size: 16
Filesystem created: Wed Aug 23 11:20:00 2017
Last mount time: n/a
Last write time: Wed Aug 23 11:20:00 2017
Mount count: 0
Maximum mount count: -1
Last checked: Wed Aug 23 11:20:00 2017
Check interval: 0 (<none>)
Lifetime writes: 132 MB
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 256
Required extra isize: 28
Desired extra isize: 28
Journal inode: 8
Default directory hash: half_md4
Directory Hash Seed: 648ce1ac-dd8d-40a6-ac6f-238b2e7d97d1
Journal backup: inode blocks
Journal features: (none)
Journal size: 128M
Journal length: 32768
Journal sequence: 0x00000001
Journal start: 0

这边信息非常多,我会解释其中的大部分。

ext4 最基础的存储布局概念就是 Block(块)。ext4 以块为单位分配存储空间,就像是 LBA 一样。然而 ext4 Block 大小可能不一样。在 Superblock 中是 4096B = 4KiB。文件系统中的 Block 总数是 3908091 个。

第二个概念是 Block Group(块组),也就是固定数量的连续的块。在我们的文件系统中 Group 大小是 32768 = 32Ki。

我们有 3908091 个block。将其除以 32Ki:

1
2
3908091 / 32768 = 119, 因为它不是 32Ki 的整数倍,
所以还会有一个比 32Ki Block 小的 Group。

所以我们有 119 个 32K Blocks 的 Groups 和一个小的 Group。每个 Block 是 4KiB。由于每个逻辑块(扇区)是 512B,这意味着每个块有 8 个扇区。这就是 ext4 的基本存储安排,如下图所示。

1 Block Group = 32768 File System Blocks = 32768 * 8 Logical (Storage) Blocks

每一个块组都包含:

  • 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_superflex_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
2
3
4
$ mkfs -t ext4 -b 1024 /dev/sda
$ sudo dumpe2fs -h /dev/sda1 | grep "First block"

First block: 1

在我们的例子中:

  • 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
2
3
4
5
6
7
8
9
Group 0: (Blocks 0-32767) [ITABLE_ZEROED]
Checksum 0x9a88, unused inodes 8131
Primary superblock at 0, Group descriptors at 1-1
Reserved GDT blocks at 2-955
Block bitmap at 956 (+956), Inode bitmap at 972 (+972)
Inode table at 988-1496 (+988)
23630 free blocks, 8133 free inodes, 2 directories, 8133 unused inodes
Free blocks: 9138-32767
Free inodes: 12-8144

正如我们所知道的:

  • 它包括了 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
2
3
$ sudo dd if=/dev/sda1 bs=4096 skip=1 count=1 status=none | hexdump -n 12 -s 0 -e '"%d %d %d\n"'

956 972 988

块组 0 在本文的分类学下是 Type 1b (head)。


第二个块组(Block Group 1):

1
2
3
4
5
6
7
8
9
Group 1: (Blocks 32768-65535) [INODE_UNINIT, ITABLE_ZEROED]
Checksum 0x5017, unused inodes 8144
Backup superblock at 32768, Group descriptors at 32769-32769
Reserved GDT blocks at 32770-33723
Block bitmap at 957 (bg #0 + 957), Inode bitmap at 973 (bg #0 + 973)
Inode table at 1497-2005 (bg #0 + 1497)
31809 free blocks, 8144 free inodes, 0 directories, 8144 unused inodes
Free blocks: 33726-33791, 33793-65535
Free inodes: 8145-16288
  • 它有 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
2
3
4
5
6
7
Group 2: (Blocks 65536-98303) [INODE_UNINIT, BLOCK_UNINIT, ITABLE_ZEROED]
Checksum 0xeabf, unused inodes 8144
Block bitmap at 958 (bg #0 + 958), Inode bitmap at 974 (bg #0 + 974)
Inode table at 2006-2514 (bg #0 + 2006)
32768 free blocks, 8144 free inodes, 0 directories, 8144 unused inodes
Free blocks: 65536-98303
Free inodes: 16289-24432
  • 没有 Superblock backup。也没有 GDT、RGDT(组描述符表和预留)
  • Bitmaps 和 inode Table 都在 BG #0

BG #2 属于 Type 3-rest。


接下来我们看看在下一个 Flex BG 的 BG #16:

1
2
3
4
5
6
7
Group 16: (Blocks 524288-557055) [INODE_UNINIT, ITABLE_ZEROED]
Checksum 0x8ab4, unused inodes 8144
Block bitmap at 524288 (+0), Inode bitmap at 524304 (+16)
Inode table at 524320-524828 (+32)
24592 free blocks, 8144 free inodes, 0 directories, 8144 unused inodes
Free blocks: 532464-557055
Free inodes: 130305-138448
  • 没有 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. 1.这里原文写成了 Logical Block,实际上是 Sector 也就是扇区。注意与 Block 相区别。
  2. 2.Error:原文此处是 “since only the first block of Flex Group...” 应为 “first block group of”
    Warning:文章证明了 1 总是 head,接下来又要证明 2 不可能是 head。说实话应该首先论证这两个不可能,然后提出 Type 3 可以划分,而不是先划出 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).