本文将介绍 Ext2 文件系统下的稀疏文件表示,并使用 debugfs 对其进行分析。

环境准备

# 创建一个 2G 大小的 Ext2 磁盘镜像文件
dd if=/dev/zero of=ext2.img bs=1M count=2048

# 生成一个 8M 大小的全零文件(非稀疏)
dd if=/dev/zero of=8M.zero bs=1M count=8

# 创建一个 8M 大小的 Ext2 格式化的镜像文件(稀疏)
cp 8M.zero 8M.ext2
mkfs.ext2 -F 8M.ext2

# 将两个 8M 大小的文件拷入 ext2.img 中
mkdir -p tmp
sudo mount ext2.img tmp
sudo cp 8M.zero tmp
sudo cp 8M.ext2 tmp
umount tmp

稀疏文件的本质

稀疏文件即一个文件的起始位置到结束位置之间存在“空洞”(如下图所示),所谓空洞,就是该文件区域没有被分配任何的磁盘空间,体现在 Ext2 文件系统元数据的层面就是——对应的数据块指针为空。

更形式化的描述为:只要一个文件在逻辑地址空间中存在至少一个由文件系统明确标记、且未分配物理数据块的连续“空洞”区域,该文件就可以被称为稀疏文件。

当应用层对稀疏文件的稀疏块进行读取时,文件系统通常的选择是:不进行任何的磁盘读取,而是直接返回一个用 0 填充的数据块。

稀疏文件分析

文件系统基本信息

首先使用 debugfs ext2.img 进入对该 Ext2 磁盘镜像文件的调试模式:

$ debugfs ext2.img
debugfs 1.47.0 (5-Feb-2023)
debugfs:

在 debugfs 的交互式终端中使用 ls 打印稀疏文件 8M.ext2 的基本信息:

12  100644 (1)      0      0   8388608 26-Dec-2025 16:35 8M.ext2
13  100644 (1)      0      0   8388608 26-Dec-2025 16:35 8M.zero

其中,inode 号为 12,文件大小为 8388608 字节(8MB)。

inode 详细信息

输入 stat <12> 对 inode 号为 12 的文件进行具体分析,得到以下信息:

Inode: 12   Type: regular    Mode:  0644   Flags: 0x0
Generation: 4091734916    Version: 0x00000000:00000001
User:     0   Group:     0   Project:     0   Size: 8388608
File ACL: 0
Links: 1   Blockcount: 240
Fragment:  Address: 0    Number: 0  Size: 0
ctime: 0x694e48d1:9e2ccd54 -- Fri Dec 26 16:35:29 2025
atime: 0x694e48d1:9bbec8b0 -- Fri Dec 26 16:35:29 2025
mtime: 0x694e48d1:9e2ccd54 -- Fri Dec 26 16:35:29 2025
crtime: 0x694e48d1:9bbec8b0 -- Fri Dec 26 16:35:29 2025
Size of extra inode fields: 32
BLOCKS:
(0-4):17920-17924, (IND):650, (132-137):3972-3977, (DIND):651, (IND):652, (2032-2047):20464-20479
TOTAL: 30

其中的一些关键信息为:Blockcount(以 512B 为单位)为 240,即文件实际占用了 120KB 的磁盘空间;BLOCKS(以 BLOCK_SIZE 为单位,通常为 4KB)为 30,刚好与 Blockcount 的值对应。

尤其需要关注的是数据块指针的分布:

  • 直接块(0-4):17920、17921、17922、17923、17924
  • 间接块(IND):650 → 指向块 132-137(3972-3977)
  • 二级间接块(DIND):651 → 指向间接块 652
  • 间接块(IND):652 → 指向块 2032-2047(20464-20479)

总结,文件的所有逻辑块为 0-2047,即逻辑大小为 2048 * 4KB = 8MB,其中逻辑块 5-131、138-2031 没有被分配任何磁盘块,也就是所谓的稀疏文件的“空洞”。

间接块内容分析

接下来可以退出 debugfs 终端,直接对 ext2.img 的数据块内容进行分析。

首先可以验证一下间接块的一致性,使用下列命令打印指定 650 号磁盘块的内容:

dd if=ext2.img bs=4096 skip=650 count=1 2>/dev/null | hexdump -C | head -20

输出如下所示:

00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001e0  84 0f 00 00 85 0f 00 00  86 0f 00 00 87 0f 00 00  |................|
000001f0  88 0f 00 00 89 0f 00 00  00 00 00 00 00 00 00 00  |................|

由于每个数据块指针占 4B 大小,且 Ext2 文件系统中的直接块数量为 12,因此 0x1e0(十进制表示 480) 对应的磁盘块号为 $480 / 4 + 12 = 132$,且该处的数据块指针为 0x00000f84(小端序),对应的十进制正好为 3972,与上一小节中 stat <12> 得到的数据一致,后面的 3973, 3974, … 也是同理。

间接块(IND):650 → 指向块 132-137(3972-3977)

二级间接块也可以采用相同的验证方式,只不过多了一层间接层,在此就不再具体分析了。

数据块位图分析

Ext2 文件系统中引入了块组的概念,每个块组有自己独立的 inode 位图和数据块位图。我们再次使用 debugfs 对 ext2.img 进行调试。首先使用 stats 命令打印文件系统的总体元数据信息,由于我们通过 stat 8M.ext2 已经看到了该文件位于块组 0 中,因此我们主要关注块组 0 的元数据信息:

...
  Group  0: block bitmap at 129, inode bitmap at 130, inode table at 131
           3544 free blocks, 8179 free inodes, 2 used directories
...

可以看到,其数据块位图位于 129 块。我们可以采用和分析间接块时一样的方式,打印该块的数据:

dd if=ext2.img bs=4096 skip=129 count=1 2>/dev/null | hexdump -C

输出如下所示:

...
*
000008b0  ff ff ff ff ff ff ff ff  ff ff ff 01 00 00 00 00  |................|
000008c0  1f 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000008d0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
...

我们可以验证一下直接索引所指向的数据块(17920-17924)的位图分配情况,其对应的上述地址应为 $17920 / 8 = 2240$,对应的十六进制为 0x8c0,可以看到为 1f,即连续的 5 个 1,完美匹配。

对比非稀疏文件

使用 stat <13> 打印 8M.zero 的文件信息,如下所示:

Inode: 13   Type: regular    Mode:  0644   Flags: 0x0
Size: 8388608
Blockcount: 16408
BLOCKS:
(0-11):3584-3595, (IND):649, ... (2051个块)

计算得到 $16408 * 512 = 8400896$,由于 Blockcount 中将数据块指针也包含在其中,因此文件实际所占用的磁盘空间甚至要大于文件大小(8388608B, 8M),与稀疏文件 8M.ext2 仅占用 120KB 空间形成鲜明对比。

参考资料