http://www.ibm.com/developerworks/cn/linux/l-cn-filesrc5/
ext3 自从诞生之日起,就由于其可靠性好、特性丰富、性能高、版本间兼容性好等优势而迅速成为 Linux 上非常流行的文件系统,诸如 Redhat 等发行版都将 ext3 作为默认的文件系统格式。为了尽量保持与 ext2 文件系统实现更好的兼容性,ext3 在设计时采用了很多保守的做法,这些保守的设计为 ext3 赢得了稳定、健壮的声誉,迅速得到了 Linux 用户(尤其是原有的 ext2 文件系统的用户)的青睐,但同时这也限制了它的可扩展能力,无法支持特别大的文件系统。
随着硬盘存储容量越来越大(硬盘容量每年几乎都会翻一倍,现在市面上已经有 1TB 的硬盘出售,很快桌面用户也可以享用这么大容量的存储空间了),企业应用所需要和产生的数据越来越多(Lawrence Livermore National Labs 使用的 BlueGene/L 系统上所使用的数据早已超过了 1PB),以及在线重新调整大小特性的支持,ext3 所面临的可扩充性问题和性能方面的压力也越来越大。在 ext3 文件系统中,如果使用 4KB 大小的数据块,所支持的最大文件系统上限为16TB,这是由于它使用了 32 位的块号所决定的(232 * 212 B = 244 B = 16 TB)。为了解决这些限制,从 2006 年 8 月开始,陆续有很多为 ext3 设计的补丁发布出来,这些补丁主要是扩充了两个特性:针对大文件系统支持的设计和 extent 映射技术。不过要想支持更大的文件系统,就必须对磁盘上的存储格式进行修改,这会破坏向前兼容性。因此为了为庞大的 ext3 用户群维护更好的稳定性,设计人员决定从 ext3 中另辟一支,设计下一代 Linux 上的文件系统,即 ext4。
ext4 的主要目标是解决 ext3 所面临的可扩展性、性能和可靠性问题。从 2.6.19 版本的内核开始,ext4 已经正式进入内核源代码中,不过它被标记为正在开发过程中,即 ext4dev。本文将介绍 ext4 为了支持更好的可扩展性方面所采用的设计,并探讨由此而引起的磁盘数据格式的变化,以及对恢复删除文件所带来的影响。
可扩展性
为了支持更大的文件系统,ext4 决定采用 48 位的块号取代 ext3 原来的 32 位块号,并采用 extent 映射来取代 ext3 所采用的间接数据块映射的方法。这样既可以增大文件系统的容量,又可以改进大文件的访问效率。在使用 4KB 大小的数据块时,ext4 可以支持最大 248 * 212 = 260 B(1 EB)的文件系统。之所以采用 48 位的块号而不是直接将其扩展到 64 位是因为,ext4 的开发者认为 1 EB 大小的文件系统对未来很多年都足够了(实际上,按照目前的速度,要对 1 EB 大小的文件系统执行一次完整的 fsck 检查,大约需要 119 年的时间),与其耗费心机去完全支持 64 位的文件系统,还不如先花些精力来解决更加棘手的可靠性问题。
将块号从 32 位修改为 48 位之后,存储元数据的结构都必须相应地发生变化,主要包括超级块、组描述符和日志。下面给出了 ext4 中所使用的新结构的部分代码。
清单1. ext4_super_block 结构定义
520 /* 521 * Structure of the super block 522 */ 523 struct ext4_super_block { 524 /*00*/ __le32 s_inodes_count; /* Inodes count */ 525 __le32 s_blocks_count; /* Blocks count */ 526 __le32 s_r_blocks_count; /* Reserved blocks count */ 527 __le32 s_free_blocks_count; /* Free blocks count */ 528 /*10*/ __le32 s_free_inodes_count; /* Free inodes count */ 529 __le32 s_first_data_block; /* First Data Block */ 530 __le32 s_log_block_size; /* Block size */ … 594 /* 64bit support valid if EXT4_FEATURE_COMPAT_64BIT */ 595 /*150*/ __le32 s_blocks_count_hi; /* Blocks count */ 596 __le32 s_r_blocks_count_hi; /* Reserved blocks count */ 597 __le32 s_free_blocks_count_hi; /* Free blocks count */ … 606 };
在 ext4_super_block 结构中,增加了 3 个与此相关的字段:s_blocks_count_hi、s_r_blocks_count_hi、s_free_blocks_count_hi,它们分别表示 s_blocks_count、s_r_blocks_count、s_free_blocks_count 高 32 位的值,将它们扩充到 64 位。
清单2. ext4_group_desc 结构定义
121 /* 122 * Structure of a blocks group descriptor 123 */ 124 struct ext4_group_desc 125 { 126 __le32 bg_block_bitmap; /* Blocks bitmap block */ 127 __le32 bg_inode_bitmap; /* Inodes bitmap block */ 128 __le32 bg_inode_table; /* Inodes table block */ 129 __le16 bg_free_blocks_count; /* Free blocks count */ 130 __le16 bg_free_inodes_count; /* Free inodes count */ 131 __le16 bg_used_dirs_count; /* Directories count */ 132 __u16 bg_flags; 133 __u32 bg_reserved[3]; 134 __le32 bg_block_bitmap_hi; /* Blocks bitmap block MSB */ 135 __le32 bg_inode_bitmap_hi; /* Inodes bitmap block MSB */ 136 __le32 bg_inode_table_hi; /* Inodes table block MSB */ 137 };
类似地,在 ext4_group_desc 中引入了另外 3 个字段:bg_block_bitmap_hi、bg_inode_bitmap_hi、bg_inode_table_hi,分别表示 bg_block_bitmap、bg_inode_bitmap、bg_inode_table 的高 32 位。
另外,由于日志中要记录所修改数据块的块号,因此 JBD也需要相应地支持 48 位的块号。同样是为了为 ext3 广大的用户群维护更好的稳定性,JBD2 也从 JBD 中分离出来,详细实现请参看内核源代码。
采用 48 位块号取代原有的 32 位块号之后,文件系统的最大值还受文件系统中最多块数的制约,这是由于 ext3 原来采用的结构决定的。回想一下,对于 ext3 类型的分区来说,在每个分区的开头,都有一个引导块,用来保存引导信息;文件系统的数据一般从第 2 个数据块开始(更确切地说,文件系统数据都是从 1KB 之后开始的,对于 1024 字节大小的数据块来说,就是从第 2 个数据块开始;对于超过 1KB 大小的数据块,引导块与后面的超级块等信息共同保存在第 1 个数据块中,超级块从 1KB 之后的位置开始)。为了管理方便,文件系统将剩余磁盘划分为一个个块组。块组前面存储了超级块、块组描述符、数据块位图、索引节点位图、索引节点表,然后才是数据块。通过有效的管理,ext2/ext3 可以尽量将文件的数据放入同一个块组中,从而实现文件数据在磁盘上的最大连续性。
在 ext3 中,为了安全性方面的考虑,所有的块描述符信息全部被保存到第一个块组中,因此以缺省的 128MB (227 B)大小的块组为例,最多能够支持 227 / 32 = 222 个块组,最大支持的文件系统大小为 222 * 227 = 249 B= 512 TB。而ext4_group_desc 目前的大小为 44 字节,以后会扩充到 64 字节,所能够支持的文件系统最大只有 256 TB。
为了解决这个问题,ext4 中采用了元块组(metablock group)的概念。所谓元块组就是指块组描述符可以存储在一个数据块中的一些连续块组。仍然以 128MB 的块组(数据块为 4KB)为例,ext4 中每个元块组可以包括 4096 / 64 = 64 个块组,即每个元块组的大小是 64 * 128 MB = 8 GB。
采用元块组的概念之后,每个元块组中的块组描述符都变成定长的,这对于文件系统的扩展非常有利。原来在 ext3 中,要想扩大文件系统的大小,只能在第一个块组中增加更多块描述符,通常这都需要重新格式化文件系统,无法实现在线扩容;另外一种可能的解决方案是为块组描述符预留一部分空间,在增加数据块时,使用这部分空间来存储对应的块组描述符;但是这样也会受到前面介绍的最大容量的限制。而采用元块组概念之后,如果需要扩充文件系统的大小,可以在现有数据块之后新添加磁盘数据块,并将这些数据块也按照元块组的方式进行管理即可,这样就可以突破文件系统大小原有的限制了。当然,为了使用这些新增加的空间,在 superblock 结构中需要增加一些字段来记录相关信息。(ext4_super_block 结构中增加了一个 s_first_meta_bg 字段用来引用第一个元块组的位置,这样还可以解决原有块组和新的元块组共存的问题。)下图给出了 ext3 为块组描述符预留空间和在 ext4 中采用元块组后的磁盘布局。
图 1. ext3 与 ext4 磁盘布局对比
![图 1. ext3 与 ext4 磁盘布局对比](https://i-blog.csdnimg.cn/blog_migrate/86ec6cdbb68e32aa3779efb2c0a1a565.png)
extent
ext2/ext3 文件系统与大部分经典的 UNIX/Linux 文件系统一样,都使用了直接、间接、二级间接和三级间接块的形式来定位磁盘中的数据块。对于小文件或稀疏文件来说,这非常有效(以 4KB 大小的数据块为例,小于 48KB 的文件只需要通过索引节点中 i_block 数组的前 12 个元素一次定位即可),但是对于大文件来说,需要经过几级间接索引,这会导致在这些文件系统上大文件的性能较差。
测试表明,在生产环境中,数据不连续的情况不会超过10%。因此,在 ext4 中引入了 extent 的概念来表示文件数据所在的位置。所谓 extent 就是描述保存文件数据使用的连续物理块的一段范围。每个 extent 都是一个 ext4_extent 类型的结构,大小为 12 字节。定义如下所示:
清单3. ext4 文件系统中有关 extent 的结构定义
69 /* 70 * This is the extent on-disk structure. 71 * It's used at the bottom of the tree. 72 */ 73 struct ext4_extent { 74 __le32 ee_block; /* first logical block extent covers */ 75 __le16 ee_len; /* number of blocks covered by extent */ 76 __le16 ee_start_hi; /* high 16 bits of physical block */ 77 __le32 ee_start; /* low 32 bits of physical block */ 78 }; 79 80 /* 81 * This is index on-disk structure. 82 * It's used at all the levels except the bottom. 83 */ 84 struct ext4_extent_idx { 85 __le32 ei_block; /* index covers logical blocks from 'block' */ 86 __le32 ei_leaf; /* pointer to the physical block of the next * 87 * level. leaf or next index could be there */ 88 __le16 ei_leaf_hi; /* high 16 bits of physical block */ 89 __u16 ei_unused; 90 }; 91 92 /* 93 * Each block (leaves and indexes), even inode-stored has header. 94 */ 95 struct ext4_extent_header { 96 __le16 eh_magic; /* probably will support different formats */ 97 __le16 eh_entries; /* number of valid entries */ 98 __le16 eh_max; /* capacity of store in entries */ 99 __le16 eh_depth; /* has tree real underlying blocks? */ 100 __le32 eh_generation; /* generation of the tree */ 101 }; 102 103 #define EXT4_EXT_MAGIC cpu_to_le16(0xf30a)
每个 ext4_extent 结构可以表示该文件从 ee_block 开始的 ee_len 个数据块,它们在磁盘上的位置是从 ee_start_hi<<32 + ee_start 开始,到 ee_start_hi<<32 + ee_start + ee_len – 1 结束,全部都是连续的。尽管 ee_len 是一个 16 位的无符号整数,但是其最高位被在预分配特性中用来标识这个 extent 是否被初始化过了,因此可以一个 extent 可以表示 215 个连续的数据块,如果采用 4KB 大小的数据块,就相当于 128MB。
如果文件大小超过了一个 ext4_extent 结构能够表示的范围,或者其中有不连续的数据块,就需要使用多个 ext4_extent 结构来表示了。为了解决这个问题,ext4 文件系统的设计者们采用了一棵 extent 树结构,它是一棵高度固定的树,其布局如下图所示:
图 2. ext4 中 extent 树的布局结构
![图 2. ext4 中 extent 树的布局结构](https://i-blog.csdnimg.cn/blog_migrate/037a76362820d6cf91c8251d6655aa14.png)
在 extent 树中,节点一共有两类:叶子节点和索引节点。保存文件数据的磁盘块信息全部记录在叶子节点中;而索引节点中则存储了叶子节点的位置和相对顺序。不管是叶子节点还是索引节点,最开始的 12 个字节总是一个 ext4_extent_header 结构,用来标识该数据块中有效项(ext4_extent 或 ext4_extent_idx 结构)的个数(eh_entries 域的值),其中 eh_depth 域用来表示它在 extent 树中的位置:对于叶子节点来说,该值为 0,之上每层索引节点依次加 1。extent 树的根节点保存在索引节点结构中的 i_block 域中,我们知道它是一个大小为 60 字节的数组,最多可以保存一个 ext4_extent_header 结构以及 4 个 ext4_extent 结构。对于小文件来说,只需要一次寻址就可以获得保存文件数据块的位置;而超出此限制的文件(例如很大的文件、碎片非常多的文件以及稀疏文件)只能通过遍历 extent 树来获得数据块的位置。
索引节点
索引节点是 ext2/ext3/ext4 文件系统中最为基本的一个概念,它是文件语义与数据之间关联的桥梁。为了最大程度地实现向后兼容性,ext4 尽量保持索引节点不会发生太大变化。ext4_inode 结构定义如下所示:
清单4. ext4_inode 结构定义
284 /* 285 * Structure of an inode on the disk 286 */ 287 struct ext4_inode { 288 __le16 i_mode; /* File mode */ 289 __le16 i_uid; /* Low 16 bits of Owner Uid */ 290 __le32 i_size; /* Size in bytes */ 291 __le32 i_atime; /* Access time */ 292 __le32 i_ctime; /* Inode Change time */ 293 __le32 i_mtime; /* Modification time */ 294 __le32 i_dtime; /* Deletion Time */ 295 __le16 i_gid; /* Low 16 bits of Group Id */ 296 __le16 i_links_count; /* Links count */ 297 __le32 i_blocks; /* Blocks count */ 298 __le32 i_flags; /* File flags */ … 310 __le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */ 311 __le32 i_generation; /* File version (for NFS) */ 312 __le32 i_file_acl; /* File ACL */ 313 __le32 i_dir_acl; /* Directory ACL */ 314 __le32 i_faddr; /* Fragment address */ … 339 __le16 i_extra_isize; 340 __le16 i_pad1; 341 __le32 i_ctime_extra; /* extra Change time (nsec << 2 | epoch) */ 342 __le32 i_mtime_extra; /* extra Modification time(nsec << 2 | epoch) */ 343 __le32 i_atime_extra; /* extra Access time (nsec << 2 | epoch) */ 344 __le32 i_crtime; /* File Creation time */ 345 __le32 i_crtime_extra; /* extra FileCreationtime (nsec << 2 | epoch) */ 346 };
与 ext3 文件系统中使用的 ext3_inode 结构对比一下可知,索引节点结构并没有发生太大变化,不同之处在于最后添加了 5 个与时间有关的字段,这是为了提高时间戳的精度。在 ext2/ext3 文件系统中,时间戳的精度只能达到秒级。随着硬件性能的提升,这种精度已经无法区分在同一秒中创建的文件的时间戳差异,这对于对精度要求很高的程序来说是无法接受的。在 ext4 文件系统中,通过扩充索引节点结构解决了这个问题,可以实现纳秒级的精度。最后两个新增字段 i_crtime 和 i_crtime_extra 用来表示文件的创建时间,这可以用来满足某些应用程序的需求。
前面已经介绍过,尽管索引节点中的 i_block 字段保持不变,但是由于 extent 概念的引入,对于这个数组的使用方式已经改变了,其前 3 个元素一定是一个 ext4_extent_header 结构,后续每 3 个元素可能是一个 ext4_extent 或 ext4_extent_idx 结构,这取决于所表示的文件的大小。这种设计可以有效地表示连续存放的大文件,但是对于包含碎片非常多的文件或者稀疏文件来说,就不是那么有效了。为了解决这个问题,ext4 的设计者们正在讨论设计一种新型的 extent 来表示这种特殊文件,它将在叶子节点中采用类似于 ext3 所采用的间接索引块的形式来保存为该文件分配的数据块位置。该类型的 ext4_extent_header 结构中的 eh_magic 字段将采用一个新值,以便与目前的 extent 区别开来。
采用这种结构的索引节点还存在一个问题:我们知道,在 ext3 中 i_blocks 是以扇区(即 512 字节)为单位的,因此单个文件的最大限制是 232 * 512 B = 2 TB。为了支持更大的文件,ext4 的 i_blocks 可以以数据块大小为单位(这需要 HUGE_FILE 特性的支持),因此文件上限可以扩充到 16TB(数据块大小为 4KB)。同时为了避免需要对整个文件系统都需要进行类似转换,还引入了一个 EXT4_HUGE_FILE_FL 标志,i_flags 中不包含这个标志的索引节点的 i_blocks 依然以 512 字节为单位。当文件所占用的磁盘空间大小增大到不能够用以512字节为单位的i_blocks来表示时,ext4自动激活EXT4_HUGE_FILE_FL标志,以数据块为单位重新计算i_blocks的值。该转换是自动进行的,对用户透明。
目录项
ext4 文件系统中使用的目录项与 ext2/ext3 并没有太大的区别。所使用的结构定义如下所示:
清单5. 目录项结构定义
737 /* 738 * Structure of a directory entry 739 */ 740 #define EXT4_NAME_LEN 255 741 742 struct ext4_dir_entry { 743 __le32 inode; /* Inode number */ 744 __le16 rec_len; /* Directory entry length */ 745 __le16 name_len; /* Name length */ 746 char name[EXT4_NAME_LEN]; /* File name */ 747 }; 748 749 /* 750 * The new version of the directory entry. Since EXT4 structures are 751 * stored in intel byte order, and the name_len field could never be 752 * bigger than 255 chars, it's safe to reclaim the extra byte for the 753 * file_type field. 754 */ 755 struct ext4_dir_entry_2 { 756 __le32 inode; /* Inode number */ 757 __le16 rec_len; /* Directory entry length */ 758 __u8 name_len; /* Name length */ 759 __u8 file_type; 760 char name[EXT4_NAME_LEN]; /* File name */ 761 };
与 ext2/ext3 类似,当目录项被删除时,也会将该目录项的空间合并到上一个目录项中。与 ext2/ext3 不同的地方在于,在删除目录项时,该目录项中的索引节点号并没有被清空,而是得以保留了下来,这使得在恢复删除文件时,文件名就可以通过查找目录项中匹配的索引节点号得以正确恢复。详细数据如下所示:
清单6. 删除文件前后目录项的变化
[root@vmfc8 ext4]# ./read_dir_entry root.block.547.orig 4096 offset | inode number | rec_len | name_len | file_type | name ================================================================= 0: 2 12 1 2 . 12: 2 12 2 2 .. 24: 11 20 10 2 lost+found 44: 12 16 5 1 hello 60: 13 32 12 1 testfile.35K 80: 14 12 4 1 hole 92: 15 4004 4 1 home [root@vmfc8 ext4]# ./read_dir_entry root.block.547.deleted 4096 offset | inode number | rec_len | name_len | file_type | name ================================================================= 0: 2 12 1 2 . 12: 2 12 2 2 .. 24: 11 36 10 2 lost+found 44: 12 16 5 1 hello 60: 13 32 12 1 testfile.35K 80: 14 12 4 1 hole 92: 15 4004 4 1 home
上面给出了保存根目录的数据块(547)在删除 hello 文件前后的变化,从中我们可以看出,唯一的区别在于 hello 所使用的 16 个字节的空间后来被合并到 lost+found 目录项所使用的空间中了,而索引节点号等信息都得以完整地保留了下来。清单中使用的 read_dir_entry 程序用来显示目录项中的数据,其源码可以在本文下载部分中获得。有关如何抓取保存目录数据的数据块的方法,请参看本系列文章第 2 部分的介绍。
在 ext2/3 文件系统中,一个目录下面最多可以包含 32,000 个子目录,这对于大型的企业应用来说显然是不够的。ext4 决定将其上限扩充到可以支持任意多个子目录。然而对于这种链表式的存储结构来说,目录项的查找和删除需要遍历整个目录的所有目录项,效率显然是相当低的。实际上,从 ext2 开始,文件系统的设计者引入了一棵 H-树来对目录项的 hash 值进行索引,速度可以提高 50 - 100 倍。相关内容已经超出了本文的范围,感兴趣的读者可自行参考 Linux 内核源代码中的相关实现。
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 /* Inode uses extents */
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 树中叶子节点数据块的变化
![图3. 删除文件前后 extent 树中叶子节点数据块的变化](https://i-blog.csdnimg.cn/blog_migrate/15f79b4ba2df9eefac29d2a2b497a1f2.png)
了解清楚这些变化之后,我们会发现在 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 上的相关问题。
参考资料
- Theodore Ts'o 对 ext4 的讨论: http://kerneltrap.org/node/6776,其中介绍了为什么要将 ext4 与 ext3 的开发分离开来
- “Planned Extensions to the Linux Ext2/Ext3 Filesystem”: http://e2fsprogs.sourceforge.net/extensions-ext23/,Theodore Ts'o 在 2002 年就针对当时 ext2/ext3 的问题提出了一系列改进计划,包括目录索引、在线调整文件系统大小、索引节点扩充、采用 extent 映射以更好地支持大文件等特性,这些设计现在有很多已经进入主流内核中了。
- “ext4 merge plans for 2.6.25”:http://lwn.net/Articles/266191/,其中介绍了在 2.6.25 版本的内核中将正式采纳的对 ext4 特性改进的实现
- 本文中节选的源代码均参考的是 2.6.23 版本的内核
- “The new ext4 filesystem: current status and future plans”:https://ols2006.108.redhat.com/2007/Reprints/mathur-Reprint.pdf,其中介绍了 ext4 文件系统目前最新的进展状态,以及今后的开发计划
- “Ext4 Development Wiki”: http://ext4.wiki.kernel.org,这是最初为 ext4 开发人员准备的一个 wiki 页面
- e2fsprogs 项目主页: http://e2fsprogs.sourceforge.net/,其中包含了 e2fsprogs 包的最新实现,最新版本中已经包含了对 ext4 文件系统的支持