ext4 文件系统的使用
目前,ext4 文件系统仍然处于非常活跃的状态,因此内核在相应的地方都加上了 DEV 标志。在编译内核时,需要在内核的
.config 文件中启用 EXT4DEV_FS 选项才能编译出最终使用的内核模块 ext4dev.ko。
由于 ext4 内部采用的关键数据结构与 ext3 并没有什么关键区别,因此在创建文件系统时依然是使用 mkfs.ext3
命令,如下所示:
清单7. 创建 ext4 文件系统,目前与创建 ext3 文件系统没什么两样
[root@vmfc8 ~]# mkfs.ext3 /dev/sda3
为了保持向前兼容性,现有的 ext3 文件系统也可以当作 ext4 文件系统进行加载,命令如下所示:
清单8. 挂载 ext4 文件系统
[root@vmfc8 ~]# mount -t ext4dev -o extents /dev/sda3
/tmp/test
-o extents 选项就是指定要启用 extent
特性。如果不在这个文件系统中执行任何写入操作,以后这个文件系统也依然可以按照 ext3 或 ext4
格式正常挂载。但是一旦在这个文件系统中写入文件之后,文件系统所使用的特性中就包含了 extent 特性,因此以后再也不能按照 ext3
格式进行挂载了,如下所示:
清单9. 写入文件前后 ext4 文件系统特性的变化据
[root@vmfc8 ext4]# umount /tmp/test; mount -t ext4dev -o extents /dev/sda3 /tmp/test; \
dumpe2fs /dev/sda3 > sda3.ext4_1
[root@vmfc8 ext4]# umount /tmp/test; mount -t ext4dev -o extents /dev/sda3 /tmp/test; \
echo hello > /tmp/test/hello; dumpe2fs /dev/sda3 > sda3.ext4_2
[root@vmfc8 ext4]# diff sda3.ext4_1 sda3.ext4_2
6c6
< Filesystem features: has_journal resize_inode dir_index filetype \
needs_recovery sparse_super large_file
---
> Filesystem features: has_journal resize_inode dir_index filetype \
needs_recovery extents sparse_super large_file
…
[root@vmfc8 ext4]# umount /tmp/test; mount -t ext3 /dev/sda3 /tmp/test
mount: wrong fs type, bad option, bad superblock on /dev/sda3,
missing codepage or helper program, or other error
In some cases useful info is found in syslog - try
dmesg | tail or so
e2fsprogs 工具的支持
在本系列前面的文章中,我们已经初步体验了 e2fsprogs 包中提供的诸如 debugfs、dumpe2fs
之类的工具对于深入理解文件系统和磁盘数据来说是如何方便。作为一种新生的文件系统,ext4
文件系统要想得到广泛应用,相关工具的支持也非常重要。从 1.39 版本开始,e2fsprogs 已经逐渐开始加入对 ext4
文件系统的支持,例如创建文件系统使用的 mkfs.ext3 命令以后会被一个新的命令 mkfs.ext4
所取代。但是截止到本文撰写时为止,e2fsprogs 的最新版本(1.40.7)对于 ext4 的支持尚不完善,下面的例子给出了
debugfs 查看 hello 文件时的结果:
清单10. debugfs 命令对 ext4 文件系统的支持尚不完善
[root@vmfc8 ext4]# echo "hello world" > /tmp/test/hello
[root@vmfc8 ext4]# debugfs /dev/sda3
debugfs 1.40.2 (12-Jul-2007)
debugfs: stat hello
Inode: 12 Type: regular Mode: 0644 Flags: 0x80000 Generation: 827135866
User: 0 Group: 0 Size: 12
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 8
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x47ced460 -- Thu Mar 6 01:12:00 2008
atime: 0x47ced460 -- Thu Mar 6 01:12:00 2008
mtime: 0x47ced460 -- Thu Mar 6 01:12:00 2008
BLOCKS:
(0):127754, (1):4, (4):1, (5):28672
TOTAL: 4
从上面的输出结果中我们可以看出,尽管这个索引节点的 i_flags 字段值为 0x80000,表示使用 extent
方式来存储数据,而不是原有的直接/间接索引模式来存储数据(此时 i_flags 字段值为 0),但是对 i_block
数组中内容的显示却依然沿用了原有的模式。如果文件占用多个 extent 进行存储,会发现 debugfs 依然尝试将
i_block[12]、i_block[13]、i_block[14]
分别作为一级、二级和三级间接索引使用,显然从中读出的数据也是毫无意义的。
索引节点中使用的 i_flags 值是在内核源代码的 /include/linux/ext4_fs.h
中定义的,如下所示:
清单11. i_flags 值定义节选
#define EXT4_EXTENTS_FL 0x00080000
ext4 文件系统中文件的删除与恢复
在 ext4 文件系统中删除文件时,所执行的操作与在 ext2/ext3
文件系统中非常类似,也不会真正修改存储文件数据所使用的磁盘数据块的内容,而是仅仅删除或修改了相关的元数据信息,使文件数据无法正常索引,从而实现删除文件的目的。因此,在
ext4 文件系统中恢复删除文件也完全是可能的。
前文中已经介绍过,在 ext4
文件系统中删除文件时,并没有将目录项中的索引节点号清空,因此通过遍历目录项的方式完全可以完整地恢复出文件名来。
对于文件数据来说,实际数据块中的数据在文件删除前后也没有任何变化,这可以利用本系列文章第一部分中介绍的直接比较数据块的方法进行验证。然而由于
extent 的引入,在 ext4 中删除文件与 ext3 也有所区别。下面让我们通过一个实例来验证一下。
在下面的例子中,我们要创建一个非常特殊的文件,它每 7KB 之后的都是一个数字(7 的倍数),其他地方数据全部为 0。
清单12. 创建测试文件
[root@vmfc8 ext4]# cat -n create_extents.sh
1 #!/bin/bash
2
3 if [ $# -ne 2 ]
4 then
5 echo "$0 [filename] [size in kb]"
6 exit 1
7 fi
8
9 filename=$1
10 size=$2
11 i=0
12
13 while [ $i -lt $size ]
14 do
15 i=`expr $i + 7`
16 echo -n "$i" | dd of=$1 bs=1024 seek=$i
17 dones
[root@vmfc8 ext4]# ./create_extents.sh /tmp/test/sparsefile.70K 70
[root@vmfc8 ext4]# ls -li /tmp/test/sparsefile.70K
13 -rw-r--r-- 1 root root 71682 2008-03-06 10:49 /tmp/test/sparsefile.70K
[root@vmfc8 ext4]# hexdump -C /tmp/test/sparsefile.70K
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00001c00 37 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |7...............|
00001c10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00003800 31 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |14..............|
00003810 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00005400 32 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |21..............|
00005410 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00007000 32 38 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |28..............|
00007010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00008c00 33 35 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |35..............|
00008c10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
0000a800 34 32 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |42..............|
0000a810 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
0000c400 34 39 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |49..............|
0000c410 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
0000e000 35 36 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |56..............|
0000e010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
0000fc00 36 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |63..............|
0000fc10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00011800 37 30 |70|
00011802
之所以要使用这个文件当作测试文件,完全为了回避 extent 的优点,否则在 4KB 大小数据块的 ext4 文件系统中,一个
extent 就可以表示 128MB 的空间,因此要想测试 extent 树的变化情况就必须创建非常大的文件才行。
由于 debugfs 对 ext4 的支持尚不完善,我们自己编写了一个小程序(list_extents)来遍历 extent
树的内容,并显示索引节点和叶子节点的数据块的位置。该程序的源代码可以在本文下载部分中获得,其用法如下:
清单13. 查看测试文件使用的 extent 树信息
[root@vmfc8 ext4]# ./list_extents /dev/sda3 13
root node: depth of the tree: 1, 1 entries in root level
idx: logical block: 1, block: 20491
- logical block: 1 - 1, physical block: 20481 - 20481
- logical block: 3 - 3, physical block: 20483 - 20483
- logical block: 5 - 5, physical block: 20485 - 20485
- logical block: 7 - 8, physical block: 20487 - 20488
- logical block: 10 - 10, physical block: 20490 - 20490
- logical block: 12 - 12, physical block: 20492 - 20492
- logical block: 14 - 15, physical block: 20494 - 20495
- logical block: 17 - 17, physical block: 20497 - 20497
list_extents 程序会对指定磁盘进行搜索,从其中的索引节点表中寻找搜索指定的索引节点号(13)所对应的项,并将其
i_block 数组当作一棵 extent 树进行遍历。从输出结果中我们可以看出,这棵 extent 树包括 1 个索引节点和
1个包含了8个 ext4_extent 结构的叶子节点,其中索引节点保存 i_block 数组中,而叶子节点则保存在20491
这个数据块中。下面让我们来看一下该文件的索引节点在删除文件前后的变化:
清单14. ext4 文件系统中删除文件前后文件索引节点的变化
[root@vmfc8 ext4]# echo "stat <13>" | debugfs /dev/sda3
debugfs 1.40.2 (12-Jul-2007)
debugfs: Inode: 13 Type: regular Mode: 0644 Flags: 0x80000
Generation: 2866260918
User: 0 Group: 0 Size: 71682
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 88
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x47cf5bb1 -- Thu Mar 6 10:49:21 2008
atime: 0x47cf5bb0 -- Thu Mar 6 10:49:20 2008
mtime: 0x47cf5bb1 -- Thu Mar 6 10:49:21 2008
BLOCKS:
(0):127754, (1):65540, (3):1, (4):20491, (6):3, (7):1,
(8):20483, (9):5, (10):1, (11):20485, (IND):7, (12):32775,
(13):98311, (14):163847, (15):229383, (DIND):2, (IND):32770,
(IND):98306, (IND):163842, (IND):229378, (TIND):20487, (DIND):14386
TOTAL: 22
[root@vmfc8 ext4]# rm -f /tmp/test/sparsefile.70K
[root@vmfc8 ext4]# sync
[root@vmfc8 ext4]# echo "stat <13>" | debugfs /dev/sda3
debugfs 1.40.2 (12-Jul-2007)
debugfs: Inode: 13 Type: regular Mode: 0644 Flags: 0x80000
Generation: 2866260918
User: 0 Group: 0 Size: 0
File ACL: 0 Directory ACL: 0
Links: 0 Blockcount: 0
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x47cf5ebc -- Thu Mar 6 11:02:20 2008
atime: 0x47cf5bb0 -- Thu Mar 6 10:49:20 2008
mtime: 0x47cf5bb1 -- Thu Mar 6 10:49:21 2008
dtime: 0x47cf5ebc -- Thu Mar 6 11:02:20 2008
BLOCKS:
(0):62218, (1):4, (3):1, (4):20491, (6):3, (7):1,
(8):20483, (9):5, (10):1, (11):20485, (IND):7, (12):32775,
(13):98311, (14):163847, (15):229383, (DIND):2, (IND):32770,
(IND):98306, (IND):163842, (IND):229378, (TIND):20487, (DIND):14386
TOTAL: 22
首先需要注意的一点是,对于上面这棵 extent 树来说,只需要使用 i_block 数组的前 6 个元素就可以存储一个
ext4_extent_header 和一个 ext4_extent_idx 结构了,而 i_block
数组中的所有元素却都是有数据的。之所以出现这种情况是因为在创建这个特殊的测试文件的过程中,我们是不断创建一个文件并在此文件尾部追加数据从而生成新文件的。当该文件使用的
extent 超过 4 个时,便扩充成一棵 extent 树,但是剩余 3 个 extent 的内容(i_block 数组的后 9
个元素)并没有被清空。
对比删除文件前后的变化会发现,ext4 与 ext3 非常类似,也都将文件大小设置为 0,这使得 debugfs 的 dump
命令也无从正常工作了。不过与 ext3 不同的是,ext4 并没有将 i_block 数组的元素全部清空,而是将
ext4_extent_header 结构中有效项数设置为 0,这样就将 extent 树破坏掉了。另外,比较叶子节点(数据块
20491)中的数据变化会发现,下面这些域在删除文件时也都被清除了:
ext4_extent_header 结构中的 eh_entries。
ext4_extent 结构中的 ee_len、ee_start_hi 以及 ee_start。
图3. 删除文件前后 extent 树中叶子节点数据块的变化
了解清楚这些变化之后,我们会发现在 ext4 中恢复删除文件的方法与 ext3
基本类似,也可以使用全文匹配、提前备份元数据和修改内核实现 3 种方法。
正如前面介绍的一样,由于 ext4 文件系统中采用了 extent
的设计,试图最大程度地确保文件数据会被保存到连续的数据块中,因此在 ext2/ext3
恢复删除文件时所介绍的正文匹配方法也完全适用,正常情况下采用这种方式恢复出来的数据会比 ext2/ext3
中更多。详细内容请参看本系列文章第 4 部分的介绍,本文中不再赘述。
尽管 ext4 是基于 extent 来管理空间的,但是在 ext3
中备份数据块位置的方法依然完全适用,下面给出了一个例子。
清单15. 备份文件数据块位置
[root@vmfc8 ext4]# export LD_PRELOAD=/usr/local/lib/libundel.so
[root@vmfc8 ext4]# rm -f /tmp/test/sparsefile.70K
[root@vmfc8 ext4]# tail -n 1 /var/e2undel/e2undel
8,3::13::71682::4096::(1-1): 20481-20481,(3-3): 20483-20483,
(5-5): 20485-20485,(7-8): 20487-20488,(10-10): 20490-20490,
(12-12): 20492-20492,(14-15): 20494-20495,
(17-17): 20497-20497::/tmp/test/sparsefile.70K
当然,如果内核实现中可以在删除文件时,extent树(其中包括i_block数组,其他extent索引节点和叶子节点)中的数据保留下来,那自然恢复起来就更加容易了。由于
ext4
的开发依然正在非常活跃地进行中,相关代码可能会频繁地发生变化,本文就不再深入探讨这个话题了,感兴趣的读者可以自行尝试。
小结
本文从 ext3 的在支持大文件系统方面的缺陷入手,逐渐介绍了 ext4
为了支持大文件系统而引入的一些设计特性,并探讨了这些特性对磁盘数据格式引起的变化,以及对恢复删除文件所带来的影响,最终讨论了在
ext4 文件系统中如何恢复删除文件的问题。在本系列的下一篇文章中,我们将开始讨论另外一个设计非常精巧的文件系统 reiserfs
上的相关问题。