恢复被删除的文件-linux篇

 恢复被删除的文件-linux篇

http://blog.chinaunix.net/u2/83058/showart_1404769.html

 

本文转自IBM中国上的文档
[http://www.ibm.com/developerworks/cn/linux/l-cn-filesrc/]
[http://www.ibm.com/developerworks/cn/linux/l-cn-filesrc2/]
[http://www.ibm.com/developerworks/cn/linux/l-cn-filesrc3/]
[http://www.ibm.com/developerworks/cn/linux/l-cn-filesrc4/]

要想恢复误删除的文件,必须清楚数据在磁盘上究竟是如何存储的,以及如何定位并恢复数据。本文从数据恢复的角度,着重介绍了 ext2 文件系统中使用的一些基本概念和重要数据结构,并通过几个实例介绍了如何手工恢复已经删除的文件。最后针对 ext2 现有实现存在的大文件无法正常恢复的问题,通过修改内核中的实现,给出了一种解决方案。

对于很多 Linux 的用户来说,可能有一个问题一直都非常头疼:对于那些不小心删除的数据来说,怎样才能恢复出来呢?大家知道,在 Windows 系统上,回收站中保存了最近使用资源管理器时删除的文件。即便是对于那些在命令行中删除的文件来说,也有很多工具(例如 recover4all,FinalData Recovery)可以把这些已经删除的文件恢复出来。在Linux 下这一切是否可能呢?

实际上,为了方便用户的使用,现在 Linux 上流行的桌面管理工具(例如gnome和KDE)中都已经集成了回收站的功能。其基本思想是在桌面管理工具中捕获对文件的删除操作,将要删除的文件移动到用户根目录下的 .Trash 文件夹中,但却并不真正删除该文件。当然,像在 Windows 上一样,如果用户在删除文件的同时,按下了 Shift 键并确认删除该文件,那么这个文件就不会被移动到 .Trash 文件夹中,也就无从恢复了。此时,习惯了使用 Windows 上各种恢复工具的人就会顿足捶胸,抱怨 Linux 上工具的缺乏了。但是请稍等一下,难道按照这种方式删除的文件就真的无从恢复了么?或者换一个角度来看,使用 rm 命令删除的文件是否还有办法能够恢复出来呢?

背景知识

在开始真正进行实践之前,让我们首先来了解一下在 Linux 系统中,文件是如何进行存储和定位的,这对于理解如何恢复文件来说非常重要。我们知道,数据最终以数据块的形式保存在磁盘上,而操作系统是通过文件系统来管理这些数据的。ext2/ext3 是 Linux 上应用最为广泛的文件系统,本文将以 ext2 文件系统为例展开介绍。

我们知道,在操作系统中,文件系统是采用一种层次化的形式表示的,通常可以表示成一棵倒置的树。所有的文件和子目录都是通过查找其父目录项来定位的,目录项中通过匹配文件名可以找到对应的索引节点号(inode),通过查找索引节点表(inode table)就可以找到文件在磁盘上的位置,整个过程如图1所示。


图 1. 文件数据定位过程
恢复被删除的文件-linux篇_第1张图片

对于 ext2 类型的文件系统来说,目录项是使用一个名为 ext2_dir_entry_2 的结构来表示的,该结构定义如下所示:


清单 1. ext2_dir_entry_2 结构定义
                
struct ext2_dir_entry_2 {
__le32 inode; /* 索引节点号 */
__le16 rec_len; /* 目录项的长度 */
__u8 name_len; /* 文件名长度 */
__u8 file_type; /* 文件类型 */
char name[EXT2_NAME_LEN]; /* 文件名 */
};

在 Unix/Linux 系统中,目录只是一种特殊的文件。目录和文件是通过 file_type 域来区分的,该值为 1 则表示是普通文件,该值为 2 则表示是目录。

对于每个 ext2 分区来说,其在物理磁盘上的布局如图 2 所示:


图 2. ext2 分区的布局
恢复被删除的文件-linux篇_第2张图片

从图 2 中可以看到,对于 ext2 文件系统来说,磁盘被划分成一个个大小相同的数据块,每个块的大小可以是1024、2048 或 4096 个字节。其中,第一个块称为引导块,一般保留做引导扇区使用,因此 ext2 文件系统一般都是从第二个块开始的。剩余的块被划分为一个个的块组,ext2 文件系统会试图尽量将相同文件的数据块都保存在同一个块组中,并且尽量保证文件在磁盘上的连续性,从而提高文件读写时的性能。

至于一个分区中到底有多少个块组,这取决于两个因素:

  1. 分区大小。
  2. 块大小。

最终的计算公式如下:

分区中的块组数=分区大小/(块大小*8)

这是由于在每个块组中使用了一个数据块位图来标识数据块是否空闲,因此每个块组中最多可以有(块大小*8)个块;该值除上分区大小就是分区中总的块组数。

每个块组都包含以下内容:

  1. 超级块。存放文件系统超级块的一个拷贝。
  2. 组描述符。该块组的组描述符。
  3. 数据块位图。标识相应的数据块是否空闲。
  4. 索引节点位图。标识相应的索引节点是否空闲。
  5. 索引节点表。存放所有索引节点的数据。
  6. 数据块。该块组中用来保存实际数据的数据块。

在每个块组中都保存了超级块的一个拷贝,默认情况下,只有第一个块组中的超级块结构才会被系统内核使用;其他块组中的超级块可以在 e2fsck 之类的程序对磁盘上的文件系统进行一致性检查使用。在 ext2 文件系统中,超级块的结构会通过一个名为 ext2_super_block 的结构进行引用。该结构的一些重要域如下所示:


清单 2. ext2_super_block 结构定义
                
struct ext2_super_block {
__le32 s_inodes_count; /* 索引节点总数 */
__le32 s_blocks_count; /* 块数,即文件系统以块为单位的大小 */
__le32 s_r_blocks_count; /* 系统预留的块数 */
__le32 s_free_blocks_count; /* 空闲块数 */
__le32 s_free_inodes_count; /* 空闲索引节点数 */
__le32 s_first_data_block; /* 第一个可用数据块的块号 */
__le32 s_log_block_size; /* 块大小 */
__le32 s_blocks_per_group; /* 每个块组中的块数 */
__le32 s_inodes_per_group; /* 每个块组中的索引节点个数 */
...
}

每个块组都有自己的组描述符,在 ext2 文件系统中是通过一个名为 ext2_group_desc的结构进行引用的。该结构的定义如下:


清单 3. ext2_group_desc 结构定义
                
/*
* Structure of a blocks group descriptor
*/
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* 数据块位图的块号 */
__le32 bg_inode_bitmap; /* 索引节点位图的块号 */
__le32 bg_inode_table; /* 第一个索引节点表的块号 */
__le16 bg_free_blocks_count; /* 该组中空闲块数 */
__le16 bg_free_inodes_count; /* 该组中空闲索引节点数 */
__le16 bg_used_dirs_count; /* 该组中的目录项 */
__le16 bg_pad;
__le32 bg_reserved[3];
};

数据块位图和索引节点位图分别占用一个块的大小,其每一位描述了对应数据块或索引节点是否空闲,如果该位为0,则表示空闲;如果该位为1,则表示已经使用。

索引节点表存放在一系列连续的数据块中,每个数据块中可以包括若干个索引节点。每个索引节点在 ext2 文件系统中都通过一个名为 ext2_inode 的结构进行引用,该结构大小固定为 128 个字节,其中一些重要的域如下所示:


清单 4. ext2_inode 结构定义
                
/*
* Structure of an inode on the disk
*/
struct ext2_inode {
__le16 i_mode; /* 文件模式 */
__le16 i_uid; /* 文件所有者的 uid */
__le32 i_size; /* 以字节为单位的文件长度 */
__le32 i_atime; /* 最后一次访问该文件的时间 */
__le32 i_ctime; /* 索引节点最后改变的时间 */
__le32 i_mtime; /* 文件内容最后改变的时间 */
__le32 i_dtime; /* 文件删除的时间 */
__le16 i_gid; /* 文件所有者的 gid */
__le16 i_links_count; /* 硬链接数 */
__le32 i_blocks; /* 文件的数据块数 */
...
__le32 i_block[EXT2_N_BLOCKS];/* 指向数据块的指针 */
...
};

第一个索引节点所在的块号保存在该块组描述符的 bg_inode_table 域中。请注意 i_block 域,其中就包含了保存数据的数据块的位置。有关如何对数据块进行寻址,请参看后文“数据块寻址方式”一节的内容。

需要知道的是,在普通的删除文件操作中,操作系统并不会逐一清空保存该文件的数据块的内容,而只会释放该文件所占用的索引节点和数据块,方法是将索引节点位图和数据块位图中的相应标识位设置为空闲状态。因此,如果我们可以找到文件对应的索引节点,由此查到相应的数据块,就可能从磁盘上将已经删除的文件恢复出来。

幸运的是,这一切都是可能的!本文将通过几个实验来了解一下如何从磁盘上恢复删除的文件。


数据块寻址方式

回想一下,ext2_inode 结构的 i_block 域是一个大小为 EXT2_N_BLOCKS 的数组,其中保存的就是真正存放文件数据的数据块的位置。通常来说,EXT2_N_BLOCKS 大小为 15。在 ext2 文件系统,采用了直接寻址和间接寻址两种方式来对数据块进行寻址,原理如图3 所示:


图 3. 数据块寻址方式
恢复被删除的文件-linux篇_第3张图片
  • 对于 i_block 的前 12 个元素(i_block[0]到i_block[11])来说,其中存放的就是实际的数据块号,即对应于文件的 0 到 11 块。这种方式称为直接寻址。
  • 对于第13个元素(i_block[12])来说,其中存放的是另外一个数据块的逻辑块号;这个块中并不存放真正的数据,而是存放真正保存数据的数据块的块号。即 i_block[12] 指向一个二级数组,其每个元素都是对应数据块的逻辑块号。由于每个块号需要使用 4 个字节表示,因此这种寻址方式可以访问的对应文件的块号范围为 12 到 (块大小/4)+11。这种寻址方式称为间接寻址。
  • 对于第14个元素(i_block[13])来说,其中存放也是另外一个数据块的逻辑块号。与间接寻址方式不同的是,i_block[13] 所指向的是一个数据块的逻辑块号的二级数组,而这个二级数组的每个元素又都指向一个三级数组,三级数组的每个元素都是对应数据块的逻辑块号。这种寻址方式称为二次间接寻址,对应文件块号的寻址范围为 (块大小/4)+12 到 (块大小/4)2+(块大小/4)+11。
  • 对于第15个元素(i_block[14])来说,则利用了三级间接索引,其第四级数组中存放的才是逻辑块号对应的文件块号,其寻址范围从 (块大小/4)2+(块大小/4)+12 到 (块大小/4)3+ (块大小/4)2+(块大小/4)+11。

ext2 文件系统可以支持1024、2048和4096字节三种大小的块,对应的寻址能力如下表所示:


表 1. 各种数据块对应的文件寻址范围
块大小 直接寻址 间接寻址 二次间接寻址 三次间接寻址
1024 12KB 268KB 64.26MB 16.06GB
2048 24KB 1.02MB 513.02MB 265.5GB
4096 48KB 4.04MB 4GB ~ 4TB

掌握上面介绍的知识之后,我们就可以开始恢复文件的实验了。


准备文件系统

为了防止破坏已有系统,本文将采用一个新的分区进行恢复删除文件的实验。

首先让我们准备好一个新的分区,并在上面创建 ext2 格式的文件系统。下面的命令可以帮助创建一个 20GB 的分区:


清单 5. 新建磁盘分区
                
# fdisk /dev/sdb << END
n

+20G
p
w
q
END

在笔者的机器上,这个分区是 /dev/sdb6。然后创建文件系统:


清单 6. 在新分区上创建 ext2 文件系统
                
# mke2fs /dev/sdb6

并将其挂载到系统上来:


清单 7. 挂载创建的 ext2 文件系统
                
# mkdir /tmp/test
# mount /dev/sdb6 /tmp/test

在真正使用这个文件系统之前,让我们首先使用系统提供的一个命令 dumpe2fs 来熟悉一下这个文件系统的一些具体参数:


清单 8. 使用 dumpe2fs 熟悉这个文件系统的参数
                
# dumpe2fs /dev/sdb6
dumpe2fs 1.39 (29-May-2006)
Filesystem volume name: <none>
Last mounted on: <not available>
Filesystem UUID: d8b10aa9-c065-4aa5-ab6f-96a9bcda52ce
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: ext_attr resize_inode dir_index filetype sparse_super large_file
Default mount options: (none)
Filesystem state: not clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 2443200
Block count: 4885760
Reserved block count: 244288
Free blocks: 4797829
Free inodes: 2443189
First block: 0
Block size: 4096
Fragment size: 4096
Reserved GDT blocks: 1022
Blocks per group: 32768
Fragments per group: 32768
Inodes per group: 16288
Inode blocks per group: 509
Filesystem created: Mon Oct 29 20:04:16 2007
Last mount time: Mon Oct 29 20:06:52 2007
Last write time: Mon Oct 29 20:08:31 2007
Mount count: 1
Maximum mount count: 39
Last checked: Mon Oct 29 20:04:16 2007
Check interval: 15552000 (6 months)
Next check after: Sat Apr 26 20:04:16 2008
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 128
Default directory hash: tea
Directory Hash Seed: d1432419-2def-4762-954a-1a26fef9d5e8


Group 0: (Blocks 0-32767)
Primary superblock at 0, Group descriptors at 1-2
Reserved GDT blocks at 3-1024
Block bitmap at 1025 (+1025), Inode bitmap at 1026 (+1026)
Inode table at 1027-1535 (+1027)
31224 free blocks, 16276 free inodes, 2 directories
Free blocks: 1543-22535, 22537-32767
Free inodes: 12, 14-16288

...

Group 149: (Blocks 4882432-4885759)
Block bitmap at 4882432 (+0), Inode bitmap at 4882433 (+1)
Inode table at 4882434-4882942 (+2)
2817 free blocks, 16288 free inodes, 0 directories
Free blocks: 4882943-4885759
Free inodes: 2426913-2443200

应用前面介绍的一些知识,我们可以看到,这个文件系统中,块大小(Block size)为4096字节,因此每个块组中的块数应该是4096*8=32768个(Blocks per group),每个块组的大小是 128MB,整个分区被划分成20GB/(4KB*32768)=160个。但是为什么我们只看到 150 个块组(0到149)呢?实际上,在 fdisk 中,我们虽然输入要创建的分区大小为 20GB,但实际上,真正分配的空间并不是严格的20GB,而是只有大约 20*109 个字节,准确地说,应该是 (4885760 * 4096) / (1024*1024*1024) = 18.64GB。这是由于不同程序的计数单位的不同造成的,在使用存储设备时经常遇到这种问题。因此,这个分区被划分成 150 个块组,前 149 个块组分别包含 32768 个块(即 128B),最后一个块组只包含 3328 个块。

另外,我们还可以看出,每个索引节点的大小是 128 字节,每个块组中包含 16288 个索引节点,在磁盘上使用 509 个块来存储(16288*128/4096),在第一个块组中,索引节点表保存在 1027 到 1535 块上。

数据块和索引节点是否空闲,是分别使用块位图和索引节点位图来标识的,在第一个块组中,块位图和索引节点位图分别保存在 1025 和 1026 块上。

dumpe2fs 的输出结果中还包含了其他一些信息,我们暂时先不用详细关心这些信息。


准备测试文件

现在请将附件中的 createfile.sh 文件下载到本地,并将其保存到 /tmp/test 目录中,这个脚本可以帮助我们创建一个特殊的文件,其中每行包含 1KB 字符,最开始的14个字符表示行号。之所以采用这种文件格式,是为了方便地确认所恢复出来的文件与原始文件之间的区别。这个脚本的用法如下:


清单 9. createfile.sh 脚本的用法
                
# ./createfile.sh [size in KB] [filename]

第 1 个参数表示所生成的文件大小,单位是 KB;第 2 个参数表示所生成文件的名字。

下面让我们创建几个测试文件:


清单 10. 准备测试文件
                
# cd /tmp/test
#./createfile.sh 35 testfile.35K
#./createfile.sh 10240 testfile.10M

# cp testfile.35K testfile.35K.orig
# cp testfile.10M testfile.10M.orig

上面的命令新创建了大小为 35 KB 和 9000KB 的两个文件,并为它们各自保存了一个备份,备份文件的目的是为了方便使用 diff 之类的工具验证最终恢复出来的文件与原始文件完全一致。

ls 命令的 –i 选项可以查看有关保存文件使用的索引节点的信息:


清单11. 查看文件的索引节点号
                
# ls -li | sort
11 drwx------ 2 root root 16384 Oct 29 20:08 lost+found
12 -rwxr-xr-x 1 root root 1406 Oct 29 20:09 createfile.sh
13 -rw-r--r-- 1 root root 35840 Oct 29 20:09 testfile.35K
14 -rw-r--r-- 1 root root 10485760 Oct 29 20:10 testfile.10M
15 -rw-r--r-- 1 root root 35840 Oct 29 20:10 testfile.35K.orig
16 -rw-r--r-- 1 root root 10485760 Oct 29 20:11 testfile.10M.orig

第一列中的数字就是索引节点号。从上面的输出结果我们可以看出,索引节点号是按照我们创建文件的顺序而逐渐自增的,我们刚才创建的 35K 大小的文件的索引节点号为 13,10M 大小的文件的索引节点号为 14。debugfs 中提供了很多工具,可以帮助我们了解进一步的信息。现在执行下面的命令:


清单12. 查看索引节点 <13> 的详细信息
                
# echo "stat <13>" | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
Inode: 13 Type: regular Mode: 0644 Flags: 0x0 Generation: 2957086759
User: 0 Group: 0 Size: 35840
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 72
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x47268467 -- Mon Oct 29 20:09:59 2007
atime: 0x4726849d -- Mon Oct 29 20:10:53 2007
mtime: 0x47268467 -- Mon Oct 29 20:09:59 2007
BLOCKS:
(0-8):4096-4104
TOTAL: 9

输出结果显示的就是索引节点 13 的详细信息,从中我们可以看到诸如文件大小(35840=35K)、权限(0644)等信息,尤其需要注意的是最后 3 行的信息,即该文件被保存到磁盘上的 4096 到 4104 总共 9 个数据块中。

下面再看一下索引节点 14 (即 testfile.10M 文件)的详细信息:


清单13. 查看索引节点 <14> 的详细信息
                
# echo "stat <14>" | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
Inode: 14 Type: regular Mode: 0644 Flags: 0x0 Generation: 2957086760
User: 0 Group: 0 Size: 10485760
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 20512
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x47268485 -- Mon Oct 29 20:10:29 2007
atime: 0x472684a5 -- Mon Oct 29 20:11:01 2007
mtime: 0x47268485 -- Mon Oct 29 20:10:29 2007
BLOCKS:
(0-11):24576-24587, (IND):24588, (12-1035):24589-25612, (DIND):25613, (IND):25614,
(1036-2059):25615-26638, (IND):26639, (2060-2559):26640-27139
TOTAL: 2564

和索引节点 13 相比,二者之间最重要的区别在于 BLOCKS 的数据,testfile.10M 在磁盘上总共占用了 2564 个数据块,由于需要采用二级间接寻址模式进行访问,所以使用了4个块来存放间接寻址的信息,分别是24588、25613、25614和26639,其中 25613块中存放的是二级间接寻址的信息。



恢复删除文件

现在将刚才创建的两个文件删除:


清单14. 删除测试文件
                
# rm -f testfile.35K testfile.10M

debugfs 的 lsdel 命令可以查看文件系统中删除的索引节点的信息:


清单15. 使用 lsdel 命令搜索已删除的文件
                
# echo "lsdel" | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
Inode Owner Mode Size Blocks Time deleted
13 0 100644 35840 9/9 Mon Oct 29 20:32:05 2007
14 0 100644 10485760 2564/2564 Mon Oct 29 20:32:05 2007
2 deleted inodes found.

回想一下 inode 结构中有 4 个有关时间的域,分别是 i_atime、i_ctime、i_mtime和i_dtime,分别表示该索引节点的最近访问时间、创建时间、修改时间和删除时间。其中 i_dtime域只有在该索引节点对应的文件或目录被删除时才会被设置。dubugfs 的 lsdel 命令会去扫描磁盘上索引节点表中的所有索引节点,其中 i_dtime 不为空的项就被认为是已经删除的文件所对应的索引节点。

从上面的结果可以看到,刚才删除的两个文件都已经找到了,我们可以通过文件大小区分这两个文件,二者一个大小为35K,另外一个大小为10M,正式我们刚才删除的两个文件。debugfs 的 dump 命令可以帮助恢复文件:


清单16. 使用 dump 命令恢复已删除的文件
                
# echo "dump <13> /tmp/recover/testfile.35K.dump" | debugfs /dev/sdb6
# echo "dump <14> /tmp/recover/testfile.10M.dump" | debugfs /dev/sdb6

执行上面的命令之后,在 /tmp/recover 目录中会生成两个文件,比较这两个文件与我们前面备份的文件的内容就会发现,testfile.35K.dump 与 testfile.35K.orig 的内容完全相同,而 testfile.10M.dump 文件中则仅有前 48K 数据是对的,后面的数据全部为 0 了。这是否意味着删除文件时间已经把数据也同时删除了呢?实际上不是,我们还是有办法把数据全部恢复出来的。记得我们刚才使用 debugfs 的 stat 命令查看索引节点 14 时的 BLOCKS 的数据吗?这些数据记录了整个文件在磁盘上存储的位置,有了这些数据就可以把整个文件恢复出来了,请执行下面的命令:


清单 17. 使用 dd 命令手工恢复已删除的文件
                
# dd if=/dev/sdb6 of=/tmp/recover/testfile.10M.dd.part1 bs=4096 count=12 skip=24576
# dd if=/dev/sdb6 of=/tmp/recover/testfile.10M.dd.part2 bs=4096 count=1024 skip=24589
# dd if=/dev/sdb6 of=/tmp/recover/testfile.10M.dd.part2 bs=4096 count=1024 skip=25615
# dd if=/dev/sdb6 of=/tmp/recover/testfile.10M.dd.part4 bs=4096 count=500 skip=26640

# cat /tmp/recover/testfile.10M.dd.part[1-4] > /tmp/recover/ testfile.10M.dd

比较一下最终的 testfile.10M.dd 文件和已经备份过的 testfile.10M.orig 文件就会发现,二者完全相同:


清单 18. 使用 diff 命令对恢复文件和原文件进行比较
                
# diff /tmp/recover/ testfile.10M.dd /tmp/test/ testfile.10M.orig

数据明明存在,但是刚才我们为什么没法使用 debugfs 的 dump 命令将数据恢复出来呢?现在使用 debugfs 的 stat 命令再次查看一下索引节点 14 的信息:


清单 19. 再次查看索引节点 <14> 的详细信息
                
# echo "stat <14>" | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
Inode: 14 Type: regular Mode: 0644 Flags: 0x0 Generation: 2957086760
User: 0 Group: 0 Size: 10485760
File ACL: 0 Directory ACL: 0
Links: 0 Blockcount: 20512
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x47268995 -- Mon Oct 29 20:32:05 2007
atime: 0x472684a5 -- Mon Oct 29 20:11:01 2007
mtime: 0x47268485 -- Mon Oct 29 20:10:29 2007
dtime: 0x47268995 -- Mon Oct 29 20:32:05 2007
BLOCKS:
(0-11):24576-24587, (IND):24588, (DIND):25613
TOTAL: 14

与前面的结果比较一下不难发现,BLOCKS后面的数据说明总块数为 14,而且也没有整个文件所占据的数据块的详细说明了。既然文件的数据全部都没有发生变化,那么间接寻址所使用的那些索引数据块会不会有问题呢?现在我们来查看一下 24588 这个间接索引块中的内容:


清单 20. 查看间接索引块 24588 中的内容
                
# dd if=/dev/sdb6 of=block. 24588 bs=4096 count=1 skip=24588

# hexdump block. 24588
0000000 0000 0000 0000 0000 0000 0000 0000 0000
*
0001000

显然,这个数据块的内容被全部清零了。 debugfs 的dump 命令按照原来的寻址方式试图恢复文件时,所访问到的实际上都是第0 个数据块(引导块)中的内容。这个分区不是可引导分区,因此这个数据块中没有写入任何数据,因此 dump 恢复出来的数据只有前48K是正确的,其后所有的数据全部为0。

实际上,ext2 是一种非常优秀的文件系统,在磁盘空间足够的情况下,它总是试图将数据写入到磁盘上的连续数据块中,因此我们可以假定数据是连续存放的,跳过间接索引所占据的 24588、25613、25614和26639,将从24576 开始的其余 2500 个数据块读出,就能将整个文件完整地恢复出来。但是在磁盘空间有限的情况下,这种假设并不成立,如果系统中磁盘碎片较多,或者同一个块组中已经没有足够大的空间来保存整个文件,那么文件势必会被保存到一些不连续的数据块中,此时上面的方法就无法正常工作了。

反之,如果在删除文件的时候能够将间接寻址使用的索引数据块中的信息保存下来,那么不管文件在磁盘上是否连续,就都可以将文件完整地恢复出来了,但是这样就需要修改 ext2 文件系统的实现了。在 ext2 的实现中,与之有关的有两个函数:ext2_free_data 和 ext2_free_branches(都在 fs/ext2/inode.c 中)。2.6 版本内核中这两个函数的实现如下:


清单 21. 内核中 ext2_free_data 和 ext2_free_branches 函数的实现
                
814 /**
815 * ext2_free_data - free a list of data blocks
816 * @inode: inode we are dealing with
817 * @p: array of block numbers
818 * @q: points immediately past the end of array
819 *
820 * We are freeing all blocks refered from that array (numbers are
821 * stored as little-endian 32-bit) and updating @inode->i_blocks
822 * appropriately.
823 */
824 static inline void ext2_free_data(struct inode *inode, __le32 *p, __le32 *q)
825 {
826 unsigned long block_to_free = 0, count = 0;
827 unsigned long nr;
828
829 for ( ; p < q ; p++) {
830 nr = le32_to_cpu(*p);
831 if (nr) {
832 *p = 0;
833 /* accumulate blocks to free if they're contiguous */
834 if (count == 0)
835 goto free_this;
836 else if (block_to_free == nr - count)
837 count++;
838 else {
839 mark_inode_dirty(inode);
840 ext2_free_blocks (inode, block_to_free, count);
841 free_this:
842 block_to_free = nr;
843 count = 1;
844 }
845 }
846 }
847 if (count > 0) {
848 mark_inode_dirty(inode);
849 ext2_free_blocks (inode, block_to_free, count);
850 }
851 }
852
853 /**
854 * ext2_free_branches - free an array of branches
855 * @inode: inode we are dealing with
856 * @p: array of block numbers
857 * @q: pointer immediately past the end of array
858 * @depth: depth of the branches to free
859 *
860 * We are freeing all blocks refered from these branches (numbers are
861 * stored as little-endian 32-bit) and updating @inode->i_blocks
862 * appropriately.
863 */
864 static void ext2_free_branches(struct inode *inode, __le32 *p, __le32 *q, int depth)
865 {
866 struct buffer_head * bh;
867 unsigned long nr;
868
869 if (depth--) {
870 int addr_per_block = EXT2_ADDR_PER_BLOCK(inode->i_sb);
871 for ( ; p < q ; p++) {
872 nr = le32_to_cpu(*p);
873 if (!nr)
874 continue;
875 *p = 0;
876 bh = sb_bread(inode->i_sb, nr);
877 /*
878 * A read failure? Report error and clear slot
879 * (should be rare).
880 */
881 if (!bh) {
882 ext2_error(inode->i_sb, "ext2_free_branches",
883 "Read failure, inode=%ld, block=%ld",
884 inode->i_ino, nr);
885 continue;
886 }
887 ext2_free_branches(inode,
888 (__le32*)bh->b_data,
889 (__le32*)bh->b_data + addr_per_block,
890 depth);
891 bforget(bh);
892 ext2_free_blocks(inode, nr, 1);
893 mark_inode_dirty(inode);
894 }
895 } else
896 ext2_free_data(inode, p, q);
897 }

注意第 832 和 875 这两行就是用来将对应的索引项置为 0 的。将这两行代码注释掉(对于最新版本的内核 2.6.23 可以下载本文给的补丁)并重新编译 ext2 模块,然后重新加载新编译出来的模块,并重复上面的实验,就会发现利用 debugfs 的 dump 命令又可以完美地恢复出整个文件来了。

显然,这个补丁并不完善,因为这个补丁中的处理只是保留了索引数据块中的索引节点数据,但是还没有考虑数据块位图的处理,如果对应的数据块没有设置为正在使用的状态,并且刚好这些数据块被重用了,其中的索引节点数据就有可能会被覆盖掉了,这样就彻底没有办法再恢复文件了。感兴趣的读者可以沿用这个思路自行开发一个比较完善的补丁。



除了普通文件之外,UNIX/Linux 中还存在一些特殊的文件,包括目录、字符设备、块设备、命名管道、socket 以及链接;另外还存在一些带有文件洞的文件,这些特殊文件的恢复是和其存储机制紧密联系在一起的,本文将从这些特殊文件的存储原理和机制入手,逐步介绍这些特殊文件的恢复方法。

在本系列文章的第一部分中,我们介绍了 ext2 文件系统中的一些基本概念和重要数据结构,并通过几个实例学习了如何恢复已经删除的文件,最后通过修改 2.6 版本内核中 ext2 文件系统的实现,解决了大文件无法正常恢复的问题。

通过第一部分的介绍,我们已经知道如何恢复系统中删除的普通文件了,但是系统中还存在一些特殊的文件,比如我们熟悉的符号链接等。回想一下在本系列文章的第一部分中,目录项是使用一个名为 ext2_dir_entry_2 的结构来表示的,该结构定义如下:


清单1. ext2_dir_entry_2 结构定义
                
struct ext2_dir_entry_2 {
__le32 inode; /* 索引节点号 */
__le16 rec_len; /* 目录项的长度 */
__u8 name_len; /* 文件名长度 */
__u8 file_type; /* 文件类型 */
char name[EXT2_NAME_LEN]; /* 文件名 */
};

其中 file_type 域就标识了每个文件的类型。ext2 文件系统中支持的文件类型定义如下表所示:


表 1. ext2 文件系统中支持的文件类型
file_type 宏定义 说明
1 EXT2_FT_REG_FILE 普通文件
2 EXT2_FT_DIR 目录
3 EXT2_FT_CHRDEV 字符设备
4 EXT2_FT_BLKDEV 块设备
5 EXT2_FT_FIFO 命名管道
6 EXT2_FT_SOCK socket
7 EXT2_FT_SYMLINK 符号链接

对应的宏定义在 include/linux/ext2_fs.h 文件中。其中,命名管道和 socket 是进程间通信时所使用的两种特殊文件,它们都是在程序运行时创建和使用的;一旦程序退出,就会自动删除。另外,字符设备、块设备、命名管道和 socket 这 4 种类型的文件并不占用数据块,所有的信息全部保存在对应的目录项中。因此,对于数据恢复的目的来说,我们只需要重点关注普通文件、符号链接和目录这三种类型的文件即可。



文件洞

在数据库之类的应用程序中,可能会提前分配一个固定大小的文件,但是并不立即往其中写入数据;数据只有在真正需要的时候才会写入到文件中。如果为这些根本不包含数据的文件立即分配数据块,那就势必会造成磁盘空间的浪费。为了解决这个问题,传统的 Unix 系统中引入了文件洞的概念,文件洞就是普通文件中包含空字符的那部分内容,在磁盘上并不会使用任何数据块来保存这部分数据。也就是说,包含文件洞的普通文件被划分成两部分,一部分是真正包含数据的部分,这部分数据保存在磁盘上的数据块中;另外一部分就是这些文件洞。(在 Windows 操作系统上也存在类似的概念,不过并没有使用文件洞这个概念,而是称之为稀疏文件。)

ext2 文件系统也对文件洞有着很好的支持,其实现是建立在动态数据块分配原则之上的,也就是说,在 ext2 文件系统中,只有当进程需要向文件中写入数据时,才会真正为这个文件分配数据块。

细心的读者可能会发现,在本系列文章第一部分中介绍的 ext2_inode 结构中,有两个与文件大小有关的域:i_size 和 i_blocks,二者分别表示文件的实际大小和存储该文件时真正在磁盘上占用的数据块的个数,其单位分别是字节和块大小(512字节,磁盘每个数据块包含8个块)。通常来说,i_blocks 与块大小的乘积可能会大于或等于 i_size 的值,这是因为文件大小并不都是数据块大小的整数倍,因此分配给该文件的部分数据块可能并没有存满数据。但是在存在文件洞的文件中,i_blocks 与块大小的乘积反而可能会小于 i_size 的值。

下面我们通过几个例子来了解一下包含文件洞的文件在磁盘上究竟是如何存储的,以及这种文件应该如何恢复。

执行下面的命令就可以生成一个带有文件洞的文件:


清单2. 创建带有文件洞的文件
                
# echo -n "X" | dd of=/tmp/test/hole bs=1024 seek=7

# ls -li /tmp/test/hole
15 -rw-r--r-- 1 root root 7169 Nov 26 11:03 /tmp/test/hole

# hexdump /tmp/test/hole
0000000 0000 0000 0000 0000 0000 0000 0000 0000
*
0001c00 0058
0001c01

第一个命令生成的 /tmp/test/hole 文件大小是 7169 字节,其前 7168 字节都为空,第 7169 字节的内容是字母 X。正常来讲,7169 字节的文件需要占用两个数据块来存储,第一个数据块全部为空,第二个数据块的第 3073 字节为字母 X,其余字节都为空。显然,第一个数据块就是一个文件洞,在这个数据块真正被写入数据之前,ext2 并不为其实际分配数据块,而是将 i_block 域的对应位(或间接寻址使用的索引数据块中的对应位)设置为0,表示这是一个文件洞。该文件的内容如下图所示:


图1. /tmp/test/hole 文件的存储方法
恢复被删除的文件-linux篇_第4张图片

file_hole.jpg

现在我们可以使用 debugfs 来查看一下这个文件的详细信息:


清单3. 带有文件洞的文件的 inode 信息
                
# echo "stat <15>" | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
debugfs: Inode: 15 Type: regular Mode: 0644 Flags: 0x0 Generation: 4118330634
User: 0 Group: 0 Size: 7169
File ACL: 1544 Directory ACL: 0
Links: 1 Blockcount: 16
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x474a379c -- Mon Nov 26 11:03:56 2007
atime: 0x474a379c -- Mon Nov 26 11:03:56 2007
mtime: 0x474a379c -- Mon Nov 26 11:03:56 2007
BLOCKS:
(1):20480
TOTAL: 1

从输出结果中我们可以看出,这个文件的大小是 7169 字节(Size 值,即 ext2_inode 结构中 i_size 域的值),占用块数是 16(Blockcount 值,ext2_inode 结构中 i_blocks 域的值,每个块的大小是 512 字节,而每个数据块占据8个块,因此16个块的大小16×512字节相当于 2 个 512字节×8即4096字节的数据块),但是它的数据在磁盘上只是第一个数据块的内容保存在 20480 这个数据块中。使用下面的方法,我们就可以手工恢复整个文件:


清单4. 使用 dd 命令手工恢复带有文件洞的文件
                
# dd if=/dev/zero of=/tmp/recover/hole.part1 bs=4096 count=1
# dd if=/dev/sdb6 of=/tmp/recover/hole.part2 bs=4096 count=1 skip=20480

# cat /tmp/recover/hole.part1 /tmp/recover/hole.part2 > /tmp/recover/hole.full
# split -d -b 7169 hole.full hole
# mv hole00 hole

# diff /tmp/test/hole /tmp/recover/hole

注意第一个 dd 命令就是用来填充这个大小为 4096 字节的文件洞的,这是文件的第一部分;第二个 dd 命令从磁盘上读取出 20480 数据块的内容,其中包含了文件的第二部分。从合并之后的文件中提取出前 7169 字节的数据,就是最终恢复出来的文件。

接下来让我们看一个稍微大一些的带有文件洞的例子,使用下面的命令创建一个大小为57KB 的文件:


清单5. 创建 57K 大小的带有文件洞的文件
                
# echo -n "Y" | dd of=/tmp/test/hole.57K bs=1024 seek=57

# ls -li /tmp/test/hole.57K
17 -rw-r--r-- 1 root root 58369 Nov 26 12:53 /tmp/test/hole.57K

# hexdump /tmp/test/hole.57K
0000000 0000 0000 0000 0000 0000 0000 0000 0000
*
000e400 0059
000e401

与上一个文件类似,这个文件的数据也只有一个字符,是 0x000e400(即第58369字节)为字符“Y”。我们真正关心的是这个文件的数据存储情况:


清单6. 使用间接寻址方式的带有文件洞的文件的 inode 信息
                
# echo "stat <17>" | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
debugfs: Inode: 17 Type: regular Mode: 0644 Flags: 0x0 Generation: 4261347083
User: 0 Group: 0 Size: 58369
File ACL: 1544 Directory ACL: 0
Links: 1 Blockcount: 24
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x474a5166 -- Mon Nov 26 12:53:58 2007
atime: 0x474a5187 -- Mon Nov 26 12:54:31 2007
mtime: 0x474a5166 -- Mon Nov 26 12:53:58 2007
BLOCKS:
(IND):24576, (14):24577
TOTAL: 2

从结果中可以看出,该文件占用了两个数据块来存储数据,一个是间接寻址使用的索引块 24576,一个是真正存放数据的数据块24577。下面让我们来查看一下 24576 这个数据块中的内容:


清单7. 索引数据块中存储的数据
                
# dd if=/dev/sdb6 of=/tmp/recover/block.24576 bs=4096 count=1 skip=24576

# hexdump block.24576
0000000 0000 0000 0000 0000 6001 0000 0000 0000
0000010 0000 0000 0000 0000 0000 0000 0000 0000
*
0001000

正如预期的一样,其中只有第3个 32 位(每个数据块的地址占用32位)表示了真正存储数据的数据块的地址:0x6001,即十进制的 24577。现在恢复这个文件也就便得非常简单了:


清单8. 手工恢复带有文件洞的大文件
                
# dd if=/dev/zero of=/tmp/recover/hole.57K.part1 bs=4096 count=14
# dd if=/dev/sdb6 of=/tmp/recover/hole.57K.part2 bs=4096 count=1 skip=24577

# cat /tmp/recover/hole.57K.part1 /tmp/recover/hole.57K.part2 /
> /tmp/recover/hole.57K.full
# split -d -b 58369 hole.57K.full hole.57K
# mv hole.57K00 hole.57K

# diff /tmp/test/hole.57K /tmp/recover/hole.57K

幸运的是,debugfs 的 dump 命令可以很好地理解文件洞机制,所以可以与普通文件一样完美地恢复整个文件,详细介绍请参看本系列文章的第一部分。



目录

在 ext2 文件系统中,目录是一种特殊的文件,其索引节点的结构与普通文件没什么两样,唯一的区别是目录中的数据都是按照 ext2_dir_entry_2 结构存储在数据块中的(按照 4 字节对齐)。在开始尝试恢复目录之前,首先让我们详细了解一下目录数据块中的数据究竟是如何存储的。现在我们使用 debugfs 来查看一个已经准备好的目录的信息:


清单9. 用来测试的文件系统的信息
                
# debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
debugfs: ls -l
2 40755 (2) 0 0 4096 28-Nov-2007 16:57 .
2 40755 (2) 0 0 4096 28-Nov-2007 16:57 ..
11 40700 (2) 0 0 16384 28-Nov-2007 16:52 lost+found
12 100755 (1) 0 0 1406 28-Nov-2007 16:53 createfile.sh
13 100644 (1) 0 0 35840 28-Nov-2007 16:53 testfile.35K
14 100644 (1) 0 0 10485760 28-Nov-2007 16:54 testfile.10M
32577 40755 (2) 0 0 4096 28-Nov-2007 16:56 dir1
15 100644 (1) 0 0 35840 28-Nov-2007 16:56 testfile.35K.orig
16 100644 (1) 0 0 10485760 28-Nov-2007 16:57 testfile.10M.orig

debugfs: ls -l dir1
32577 40755 (2) 0 0 4096 28-Nov-2007 16:56 .
2 40755 (2) 0 0 4096 28-Nov-2007 16:57 ..
32578 100755 (1) 0 0 1406 28-Nov-2007 16:55 createfile.sh
32579 40755 (2) 0 0 4096 28-Nov-2007 16:55 subdir11
48865 40755 (2) 0 0 4096 28-Nov-2007 16:55 subdir12
32580 100644 (1) 0 0 35840 28-Nov-2007 16:56 testfile.35K
32581 100644 (1) 0 0 10485760 28-Nov-2007 16:56 testfile.10M

从输出结果中可以看出,每个目录结构中至少要包含两项:当前目录(.)和父目录(..)的信息。在一个文件系统中,会有一些特殊的索引节点是保留的,用户创建的文件无法使用这些索引节点。2 就是这样一个特殊的索引节点,表示根目录。结合上面的输出结果,当前目录(.)和父目录(..)对应的索引节点号都是2,表示这是该分区的根目录。特别地,在使用 mke2fs 命令创建一个 ext2 类型的文件系统时,会自动创建一个名为 lost+found 的目录,并为其预留 4 个数据块的大小(16KB),其用途稍后就会介绍。

我们在根目录下面创建了几个文件和一个名为 dir1(索引节点号为 32577)的目录,并在 dir1 中又创建了两个子目录(subdir1 和 subdir2)和几个文件。

要想完美地恢复目录,必须了解清楚在删除目录时究竟对系统进行了哪些操作,其中哪些是可以恢复的,哪些是无法恢复的,这样才能寻找适当的方式去尝试恢复数据。现在先让我们记录下这个文件系统目前的一些状态:


清单10. 根目录(索引节点 <2>)子目录 dir1的 inode 信息
                
debugfs: stat <2>
Inode: 2 Type: directory Mode: 0755 Flags: 0x0 Generation: 0
User: 0 Group: 0 Size: 4096
File ACL: 0 Directory ACL: 0
Links: 4 Blockcount: 8
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x474d2d63 -- Wed Nov 28 16:57:07 2007
atime: 0x474d3203 -- Wed Nov 28 17:16:51 2007
mtime: 0x474d2d63 -- Wed Nov 28 16:57:07 2007
BLOCKS:
(0):1536
TOTAL: 1

debugfs: stat <32577>
Inode: 32577 Type: directory Mode: 0755 Flags: 0x0 Generation: 1695264350
User: 0 Group: 0 Size: 4096
File ACL: 1542 Directory ACL: 0
Links: 4 Blockcount: 16
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x474d2d2a -- Wed Nov 28 16:56:10 2007
atime: 0x474d3203 -- Wed Nov 28 17:16:51 2007
mtime: 0x474d2d2a -- Wed Nov 28 16:56:10 2007
BLOCKS:
(0):88064
TOTAL: 1

以及根目录和 dir1 目录的数据块:


清单11. 备份根目录和子目录 dir1 的数据块
                
# dd if=/dev/sdb6 of=/tmp/recover/block.1536.orig bs=4096 count=1 skip=1536
# dd if=/dev/sdb6 of=/tmp/recover/block.88064.orig bs=4096 count=1 skip=88064

为了方便阅读目录数据块中的数据,我们编写了一个小程序,源代码如下所示:


清单12. read_dir_entry.c 源代码
                
#include <stdio.h>
#include <stdlib.h>
#include <ext2fs/ext2_fs.h>

struct ext2_dir_entry_part {
__u32 inode; /* Inode number */
__u16 rec_len; /* Directory entry length */
__u8 name_len; /* Name length */
__u8 file_type;
} dep;

void usage()
{
printf("read_dir_entry [dir entry filename] [dir entry size]/n");
}

int main(int argc, char **argv)
{

struct ext2_dir_entry_2 de;
char *filename = NULL;
FILE *fp = NULL;
int rtn = 0;
int length = 0;
int de_size = 0;

if (argc < 3)
{
printf("Too few parameters!/n");
usage();
exit(1);
}

filename = argv[1];
de_size = atoi(argv[2]);

fp = fopen(filename, "r");
if (!fp)
{
printf("cannot open file: %s/n", filename);
exit(1);
}

printf(" offset | inode number | rec_len | name_len | file_type | name/n");
printf("=================================================================/n");

while ( rtn = fread(&dep, sizeof(struct ext2_dir_entry_part), 1, fp) )
{
if (dep.rec_len <= 0)
{
fclose(fp);
exit(0);
}

fseek(fp, 0 - sizeof(struct ext2_dir_entry_part), SEEK_CUR);

fread(&de, ((int)(dep.name_len + 3)/4)*4 + sizeof(struct ext2_dir_entry_part), 1, fp);
de.name[de.name_len] = '/0';

printf("%6d: %12d%12d%12d%12d %s/n", length, de.inode, de.rec_len, de.name_len, de.file_type, de.name);
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|

length += dep.rec_len;

if (length >= de_size - sizeof(struct ext2_dir_entry_part))
{
fclose(fp);
exit(0);
}
}

fclose(fp);
}

这段程序的基本想法是要遍历目录对应的数据块的内容,并打印每个目录项的内容(一个 ext2_dir_entry_2 结构)。需要注意的是,在遍历整个文件时,我们并没有采用 rec_length 作为步长,而是采用了 name_length + sizeof(struct ext2_dir_entry_part) 作为步长,这是为了能够读取到其中被标识为删除的目录项的数据,大家稍后就会明白这一点。

将这段程序保存为 read_dir_entry.c,并编译成可执行程序:


清单13. 编译 read_dir_entry.c
                
# gcc –o read_dir_entry read_dir_entry.c

并分析刚才得到的两个数据块的结果:


清单14. 分析原始目录项中的数据
                
# ./read_dir_entry block.1536.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 24 13 1 createfile.sh
68: 13 20 12 1 testfile.35K
88: 14 20 12 1 testfile.10M
108: 32577 12 4 2 dir1
120: 15 28 17 1 testfile.35K.orig
148: 16 3948 17 1 testfile.10M.orig

# ./read_dir_entry block.88064.orig 4096
offset | inode number | rec_len | name_len | file_type | name
=================================================================
0: 32577 12 1 2 .
12: 2 12 2 2 ..
24: 32578 24 13 1 createfile.sh
48: 32579 16 8 2 subdir11
64: 48865 16 8 2 subdir12
80: 32580 20 12 1 testfile.35K
100: 32581 3996 12 1 testfile.10M

这与上面在 debugfs 中使用 ls 命令看到的结果是完全吻合的。

现在删除 dir1 这个目录,然后卸载测试目录(这是为了确保删除文件操作会被同步到磁盘上的数据块中),然后重新读取我们关注的这两个数据块的内容:


清单15. 删除目录并重新备份目录项数据
                
# rm –rf /tmp/test/dir1
# cd /
# umount /tmp/test

# dd if=/dev/sdb6 of=/tmp/recover/block.1536.deleted bs=4096 count=1 skip=1536
# dd if=/dev/sdb6 of=/tmp/recover/block.88064. deleted bs=4096 count=1 skip=88064

现在再来查看一下这两个数据块中内容的变化:


清单16. 分析新目录项中的数据
                
# ./read_dir_entry block.1536.deleted 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 24 13 1 createfile.sh
68: 13 20 12 1 testfile.35K
88: 14 32 12 1 testfile.10M
108: 0 12 4 2 dir1
120: 15 28 17 1 testfile.35K.orig
148: 16 3948 17 1 testfile.10M.orig

# ./read_dir_entry block.88064.deleted 4096
offset | inode number | rec_len | name_len | file_type | name
=================================================================
0: 32577 12 1 2 .
12: 2 12 2 2 ..
24: 32578 24 13 1 createfile.sh
48: 32579 16 8 2 subdir11
64: 48865 16 8 2 subdir12
80: 32580 20 12 1 testfile.35K
100: 32581 3996 12 1 testfile.10M

与前面的结果进行一下对比就会发现,dir1 目录的数据块并没有发生任何变化,而根目录的数据块中 dir1 以及之前的一项则变得不同了。实际上,在删除 dir1 目录时,所执行的操作是将 dir1 项中的索引节点号清空,并将这段空间合并到前一项上(即将 dir1 项的 rec_length 加到前一项的 rec_length上)。这也就是为什么我们编写的 read_dir_entry 程序没有采用 rec_length 作为步长来遍历数据的原因。

除了数据之外,索引节点信息也发生了一些变化,现在我们来了解一下最新的索引节点信息:


清单17. 删除子目录后索引节点信息的变化
                
# debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
debugfs: stat <2>
Inode: 2 Type: directory Mode: 0755 Flags: 0x0 Generation: 0
User: 0 Group: 0 Size: 4096
File ACL: 0 Directory ACL: 0
Links: 3 Blockcount: 8
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x474d3387 -- Wed Nov 28 17:23:19 2007
atime: 0x474d33c2 -- Wed Nov 28 17:24:18 2007
mtime: 0x474d3387 -- Wed Nov 28 17:23:19 2007
BLOCKS:
(0):1536
TOTAL: 1

debugfs: stat <32577>
Inode: 32577 Type: directory Mode: 0755 Flags: 0x0 Generation: 1695264350
User: 0 Group: 0 Size: 0
File ACL: 1542 Directory ACL: 0
Links: 0 Blockcount: 16
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x474d3387 -- Wed Nov 28 17:23:19 2007
atime: 0x474d3387 -- Wed Nov 28 17:23:19 2007
mtime: 0x474d3387 -- Wed Nov 28 17:23:19 2007
dtime: 0x474d3387 -- Wed Nov 28 17:23:19 2007
BLOCKS:
(0):88064
TOTAL: 1

与删除之前的结果进行一下比较就会发现,主要区别包括:

  1. 将父目录的 Links 值减 1。
  2. 设置 dtime 时间,并更新其他时间字段。
  3. 由于目录只有在为空时才会被删除,因此其 Size 值会被设置为 0,Links 字段也被设置为 0。

通过了解数据块和索引节点的相应变化可以为恢复目录提供一个清晰的思路,其具体步骤如下:

  1. 确定删除目录所对应的索引节点号。
  2. 按照恢复文件的方法恢复索引节点对应的数据块。
  3. 遍历数据块内容,恢复其中包含的文件和子目录。
  4. 更新索引节点对应信息。
  5. 修改父目录的索引节点信息和数据块中对应目录项的内容。

实际上,步骤3并不是必须的,因为如果这个目录中包含文件或子目录,使用 debugfs 的 lsdel 命令(遍历索引节点表)也可以找到所删除的索引节点记录,采用本文中介绍的方法也可以将其逐一恢复出来。

debugfs 的 mi 命令可以用来直接修改索引节点的信息,下面我们就使用这个命令来修改 dir1 这个目录对应的索引节点的信息:


清单18. 使用 debugfs 的 mi 命令直接修改索引节点信息
                
# debugfs -w /dev/sdb6
debugfs 1.39 (29-May-2006)
debugfs: lsdel
Inode Owner Mode Size Blocks Time deleted
32577 0 40755 0 1/ 1 Wed Nov 28 17:23:19 2007
32578 0 100755 1406 1/ 1 Wed Nov 28 17:23:19 2007
32579 0 40755 0 1/ 1 Wed Nov 28 17:23:19 2007
32580 0 100644 35840 9/ 9 Wed Nov 28 17:23:19 2007
32581 0 100644 10485760 2564/2564 Wed Nov 28 17:23:19 2007
48865 0 40755 0 1/ 1 Wed Nov 28 17:23:19 2007
6 deleted inodes found.
debugfs: mi <32577>
Mode [040755]
User ID [0]
Group ID [0]
Size [0] 4096
Creation time [1196241799]
Modification time [1196241799]
Access time [1196241799]
Deletion time [1196241799] 0
Link count [0] 4
Block count [16]
File flags [0x0]
Generation [0x650bae5e]
File acl [1542]
Directory acl [0]
Fragment address [0]
Fragment number [0]
Fragment size [0]
Direct Block #0 [88064]
Direct Block #1 [0]
Direct Block #2 [0]
Direct Block #3 [0]
Direct Block #4 [0]
Direct Block #5 [0]
Direct Block #6 [0]
Direct Block #7 [0]
Direct Block #8 [0]
Direct Block #9 [0]
Direct Block #10 [0]
Direct Block #11 [0]
Indirect Block [0]
Double Indirect Block [0]
Triple Indirect Block [0]
debugfs: link <32577> dir1
debugfs: q

注意要使用 mi 命令直接修改索引节点的信息,在执行 debugfs 命令时必须加上 –w 选项,表示以可写方式打开该设备文件。在上面这个例子中,lsdel 命令找到 6 个已经删除的文件,其中 32577 就是 dir1 目录原来对应的索引节点。接下来使用 mi 命令修改这个索引节点的内容,将 Size 设置为 4096(Block count * 512),Deletion Time 设置为 0,Links count 设置为 4。最后又执行了一个 link 命令,为这个索引节点起名为 dir1(这样并不会修改父目录的 Links count 值)。

退出 debugfs 并重新挂载这个设备,就会发现 dir1 目录已经被找回来了,不过尽管该目录下面的目录结构都是正确的,但是这些文件和子目录的数据都是错误的:


清单19. 验证恢复结果
                
# mount /dev/sdb6 /tmp/test
# ls -li /tmp/test
total 20632
12 -rwxr-xr-x 1 root root 1406 Nov 28 16:53 createfile.sh
32577 drwxr-xr-x 4 root root 4096 Nov 28 17:23 dir1
11 drwx------ 2 root root 16384 Nov 28 16:52 lost+found
14 -rw-r--r-- 1 root root 10485760 Nov 28 16:54 testfile.10M
16 -rw-r--r-- 1 root root 10485760 Nov 28 16:57 testfile.10M.orig
13 -rw-r--r-- 1 root root 35840 Nov 28 16:53 testfile.35K
15 -rw-r--r-- 1 root root 35840 Nov 28 16:56 testfile.35K.orig

# ls -li /tmp/test/dir1
total 0
??--------- ? ? ? ? ? /tmp/test/dir1/createfile.sh
??--------- ? ? ? ? ? /tmp/test/dir1/subdir11
??--------- ? ? ? ? ? /tmp/test/dir1/subdir12
??--------- ? ? ? ? ? /tmp/test/dir1/testfile.10M
??--------- ? ? ? ? ? /tmp/test/dir1/testfile.35K

其原因是 dir1 中所包含的另外两个子目录和三个文件都还没有恢复。可以想像,恢复一个删除的目录会是件非常复杂而繁琐的事情。幸运的是,e2fsck 这个工具可以很好地理解 ext2 文件系统的实现,它可以用来对文件系统进行检查,并自动修复诸如链接数不对等问题。现在请按照上面的方法使用 mi 命令将其他 5 个找到的索引节点 Deletion Time 设置为 0,并将 Link count 设置为 1。然后使用下面的命令,强制 e2fsck 对整个文件系统进行检查:


清单20. 使用 e2fsck 强制对文件系统进行一致性检查
                
# e2fsck -f -y /dev/sdb6 > e2fsck.out 2>&1

e2fsck 的结果保存在 e2fsck.out 文件中。查看这个文件就会发现,e2fsck要执行 4 个步骤的检查:

  1. 检查并修复索引节点、数据块和大小。比如已删除子目录的索引节点大小为0,则会根据所占用的块数(每个块为512字节)换算出来。
  2. 检查目录结构的问题。检查索引节点的父目录,如果不存在,就认为父目录就是根目录。对于目录节点,需要检查是否包含当前目录和父目录项。
  3. 检查目录结构的连通性。防止出现按照绝对路径无法访问文件的情况出现,将这些有问题的文件或目录放入 lost+found 目录中。
  4. 检查并修复引用计数。统计对索引节点的引用计数值。
  5. 检查并修复块组信息,包括块位图、索引节点位图,计算块组中的空闲块数、空闲索引节点数等。

现在重新挂载这个文件系统,会发现所有的文件已经全部恢复出来了。



符号链接

我们知道,在 ext2 文件系统中,链接可以分为两种:硬链接和符号链接(或称为软链接)。实际上,目录中的每个文件名都对应一个硬链接。硬链接的出现是为了解决使用不同的文件名来引用同一个文件的问题。如果没有硬链接,只能通过给现有文件新建一份拷贝才能通过另外一个名字来引用这个文件,这样做的问题是在文件内容发生变化的情况下,势必会造成引用这些文件的进程所访问到的数据不一致的情况出现。而虽然每个硬链接在文件目录项中都是作为一个单独的项存在的,但是其索引节点号完全相同,这就是说它们引用的是同一个索引节点,因此对应的文件数据也完全相同。下面让我们通过一个例子来验证一下:


清单21.硬链接采用相同的索引节点号
                
# ln testfile.10M hardlink.10M

# ls -li
total 20592
12 -rwxr-xr-x 1 root root 1406 Nov 29 19:19 createfile.sh
1205313 drwxr-xr-x 2 root root 4096 Nov 29 19:29 dir1
14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 hardlink.10M
11 drwx------ 2 root root 16384 Nov 29 19:19 lost+found
14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 testfile.10M
13 -rw-r--r-- 1 root root 35840 Nov 29 19:20 testfile.35K

我们可以看到,使用 ln 建立的硬链接 hardlink.10M 的索引节点号也是 14,这与 testfile.10M 的索引节点号完全相同,因此通过这两个名字所访问到的数据是完全一致的。

因此,硬链接的恢复与普通文件的恢复非常类似,唯一的区别在于如果索引节点指向的数据已经恢复出来了,现在就无需再恢复数据了,只需要恢复其父目录中的对应目录项即可,这可以通过 debugfs 的 link 命令实现。

硬件链接的出现虽然可以满足通过不同名字来引用相同文件的需要,但是也存在一些问题,包括:

  • 不能对目录建立硬链接,否则就会引起循环引用的问题,从而导致最终正常路径的无法访问。
  • 不能建立跨文件系统的硬链接,这是由于每个文件系统中的索引节点号都是单独进行编号的,跨文件系统就会导致索引节点号变得非常混乱。而这在现代 Linux/Unix 操作系统上恰恰是无法接受的,因为每个文件系统中都可能会有很多挂载点来挂载不同的文件系统。

为了解决上面的问题,符号链接就应运而生了。符号链接与硬链接的区别在于它要占用一个单独的索引节点来存储相关数据,但却并不存储链接指向的文件的数据,而是存储链接的路径名:如果这个路径名小于60个字符,就其存储在符号链接索引节点的 i_block 域中;如果超过 60 个字符,就使用一个单独的数据块来存储。下面让我们来看一个例子:


清单22. 符号链接采用不同的索引节点号
                
# ln -s testfile.10M softlink.10M

# ls -li
total 20596
12 -rwxr-xr-x 1 root root 1406 Nov 29 19:19 createfile.sh
1205313 drwxr-xr-x 2 root root 4096 Nov 29 19:29 dir1
14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 hardlink.10M
11 drwx------ 2 root root 16384 Nov 29 19:19 lost+found
15 lrwxrwxrwx 1 root root 12 Nov 29 19:41 softlink.10M -> testfile.10M
14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 testfile.10M
13 -rw-r--r-- 1 root root 35840 Nov 29 19:20 testfile.35K

# echo "stat <15>" | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
debugfs: Inode: 15 Type: symlink Mode: 0777 Flags: 0x0 Generation: 2344716327
User: 0 Group: 0 Size: 12
File ACL: 1542 Directory ACL: 0
Links: 1 Blockcount: 8
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x474ea56f -- Thu Nov 29 19:41:35 2007
atime: 0x474ea571 -- Thu Nov 29 19:41:37 2007
mtime: 0x474ea56f -- Thu Nov 29 19:41:35 2007
Fast_link_dest: testfile.10M

ln 命令的 –s 参数就用来指定创建一个符号链接。从结果中可以看出,新创建的符号链接使用的索引节点号是 15,索引节点中的 i_block 中存储的值就是这个符号链接所指向的目标:testfile.10M(Fast_link_dest 的值)。

现在再来看一个指向长路径的符号链接的例子:


清单23. 长名符号链接
                
# touch abcdwfghijklmnopqrstuvwxyz0123456789abcdwfghijklmnopqrstuvwxyz0123456789.sh

# ln -s abcdwfghijklmnopqrstuvwxyz0123456789abcdwfghijklmnopqrstuvwxyz0123456789.sh /
longsoftlink.sh

# ls -li
total 20608
16 -rw-r--r-- 1 root root 0 Nov 29 19:52 /
abcdwfghijklmnopqrstuvwxyz0123456789abcdwfghijklmnopqrstuvwxyz0123456789.sh
12 -rwxr-xr-x 1 root root 1406 Nov 29 19:19 createfile.sh
1205313 drwxr-xr-x 2 root root 4096 Nov 29 19:29 dir1
14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 hardlink.10M
17 lrwxrwxrwx 1 root root 75 Nov 29 19:53 longsoftlink.sh -> /
abcdwfghijklmnopqrstuvwxyz0123456789abcdwfghijklmnopqrstuvwxyz0123456789.sh
11 drwx------ 2 root root 16384 Nov 29 19:19 lost+found
15 lrwxrwxrwx 1 root root 12 Nov 29 19:41 softlink.10M -> testfile.10M
14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 testfile.10M
13 -rw-r--r-- 1 root root 35840 Nov 29 19:20 testfile.35K

# echo "stat <17>" | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
debugfs: Inode: 17 Type: symlink Mode: 0777 Flags: 0x0 Generation: 744523175
User: 0 Group: 0 Size: 75
File ACL: 1542 Directory ACL: 0
Links: 1 Blockcount: 16
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x474ea824 -- Thu Nov 29 19:53:08 2007
atime: 0x474ea826 -- Thu Nov 29 19:53:10 2007
mtime: 0x474ea824 -- Thu Nov 29 19:53:08 2007
BLOCKS:
(0):6144
TOTAL: 1

此处我们创建了一个名字长度为 75 个字符的文件,并建立一个符号链接(其索引节点号是 17)指向这个文件。由于链接指向的位置路径名超过了 60 个字符,因此还需要使用一个数据块(6144)来存储这个路径名。手工恢复方法如下:


清单24. 恢复长名符号链接
                
# dd if=/dev/sdb6 of=longsoftlink.6144 bs=4096 count=1 skip=6144

# xxd longsoftlink.6144 | more
0000000: 6162 6364 7766 6768 696a 6b6c 6d6e 6f70 abcdwfghijklmnop
0000010: 7172 7374 7576 7778 797a 3031 3233 3435 qrstuvwxyz012345
0000020: 3637 3839 6162 6364 7766 6768 696a 6b6c 6789abcdwfghijkl
0000030: 6d6e 6f70 7172 7374 7576 7778 797a 3031 mnopqrstuvwxyz01
0000040: 3233 3435 3637 3839 2e73 6800 0000 0000 23456789.sh.....
0000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................

这样符号链接的数据就可以完整地恢复出来了。

需要注意的是,为了保证整个文件系统的完整性,在恢复硬链接时,还需要修改链接指向的索引节点的引用计数值,这可以使用 e2fsck 帮助完成,详细步骤请参看上一节目录的恢复。



在本系列文章的前两部分中,我们介绍了 ext2 文件系统中各种文件在磁盘上的存储结构,以及如何利用 debugfs 工具的辅助,手工恢复这些文件的详细过程。

通过这两部分的学习,我们可以看出恢复系统中删除的文件是一个非常繁琐的过程,需要非常仔细地考虑各种情况,并且要保持足够的细心,才可能把数据准确无误地恢复出来。稍有差错,就会造成数据丢失的情况。聪明的读者肯定会想,如果有一些好工具来自动或辅助完成数据的恢复过程,那简直就太好了。

幸运的是,已经有人开发了这样一些工具,来简化用户的数据恢复工作,e2undel 就是其中功能最为强大的一个。

自动恢复工具 e2undel

回想一下,在 ext2 文件系统中删除一个文件时,该文件本身的数据并没有被真正删除,实际执行的操作如下:

  1. 在块位图中将该文件所占用的数据块标识为可用状态。
  2. 在索引节点位图中将该文件所占用的索引节点标识为可用状态。
  3. 将该文件索引节点中的硬链接数目设置为 0。
  4. 将该文件索引节点中的删除时间设置为当前时间。
  5. 将父目录项中该文件对应项中的索引节点号设置为 0,并扩展前一项,使其包含该项所占用的空间。

而索引节点中的一些关键信息(或称为元数据,包括文件属主、访问权限、文件大小、该文件所占用的数据块等)都并没有发生任何变化。因此只要知道了索引节点号,就完全可以用本系列文章介绍的技术将文件完整地从磁盘上恢复出来了,这正是 e2undel 之类的工具赖以生存的基础。

然而,由于所删除的文件在目录项中对应的项中的索引节点号被清空了,因此我们就无法从索引节点中获得文件名的信息了。不过,由于文件大小、属主和删除时间信息依然能反映文件的原始信息,因此我们可以通过这些信息来帮助判断所删除的文件是哪个。

e2undel 是由Oliver Diedrich 开发的一个用来恢复 ext2 文件系统中已删除文件的工具,它会遍历所检测的文件系统的索引节点表,从中找出所有被标记为删除的索引节点,并按照属主和删除时间列出这些文件。另外,e2undel 还提供了文件大小信息,并试图按照 file 命令的方式来确定文件类型。如果您使用 rm –rf * 之类的命令一次删除了很多文件,这种信息就可以用来非常方便地帮助确定希望恢复的是哪些文件。在选择要恢复的文件之后,e2undel 会从磁盘上读取该文件占用的数据块(这些数据块的信息全部保存在索引节点中),并将其写入到一个新文件中。下面我们来看一下 e2undel 这个工具的详细用法。

首先请从 e2undel 的主页(http://e2undel.sourceforge.net/)上下载最新的源码包(截止到撰写本文为止,最新的版本是 0.82),并将其保存到本地文件系统中。不过这个源码包在最新的 Fedora Core 8 上编译时可能会有些问题,这是由于 ext2 文件系统内部实现中一些数据结构的变化引起来的,读者可以下载本文“下载”部分给出的补丁来修正这个问题(请下载这个补丁文件 e2undel-0.82.patch,并将其保存到与源码包相同的目录中)。要想编译 e2undel,系统中还必须安装 e2fsprogs 和 e2fsprogs-devel 这两个包,其中有编译 e2undel 所需要的一些头文件。Fedora Core 8 中自带的这两个包的版本号是 1.39-7:


清单1. 确认系统中已经安装了 e2fsprogs 和 e2fsprogs-devel
                
# rpm -qa | grep e2fsprogs
e2fsprogs-libs-1.39-7
e2fsprogs-1.39-7
e2fsprogs-devel-1.39-7

现在就可以开始编译 e2undel 了:


清单2. 编译 e2undel
                
# tar -zxf e2undel-0.82.tgz

# patch -p0 < e2undel-0.82.patch
patching file e2undel-0.82/Makefile
patching file e2undel-0.82/e2undel.h
patching file e2undel-0.82.orig/libundel.c

# cd e2undel-0.82
# make all

编译之后会生成一个名为 e2undel 的可执行文件,其用法如下:


清单3. e2undel 的用法
                
# ./e2undel
./e2undel 0.82
usage: ./e2undel -d device -s path [-a] [-t]
usage: ./e2undel -l
'-d': file system where to look for deleted files
'-s': directory where to save undeleted files
'-a': work on all files, not only on those listed in undel log file
'-t': try to determine type of deleted files w/o names, works only with '-a'
'-l': just give a list of valid files in undel log file

e2undel 实际上并没有像前面介绍的使用 e2fsck 那样的方法一样真正将已经删除的文件恢复到原来的文件系统中,因为它并不会修改磁盘上 ext2 使用的内部数据结构(例如索引节点、块位图和索引节点位图)。相反,它仅仅是将所删除文件的数据恢复出来并将这些数据保存到一个新文件中。因此,-s 参数指定是保存恢复出来的文件的目录,最好是在另外一个文件系统上,否则可能会覆盖磁盘上的原有数据。如果指定了 -t 参数,e2undel 会试图读取文件的前 1KB 数据,并试图从中确定该文件的类型,其原理与系统中的 file 命令非常类似,这些信息可以帮助判断正在恢复的是什么文件。

下面让我们来看一个使用 e2undel 恢复文件系统的实例。


清单4. 使用 e2undel 恢复文件的实例
                
# ./e2undel -a -t -d /dev/sda2 -s /tmp/recover/
./e2undel 0.82
Trying to recover files on /dev/sda2, saving them on /tmp/recover/

/dev/sda2 opened for read-only access
/dev/sda2 was not cleanly unmounted.
Do you want wo continue (y/n)? y
489600 inodes (489583 free)
977956 blocks of 4096 bytes (941677 free)
last mounted on Fri Dec 28 16:21:50 2007

reading log file: opening log file: No such file or directory
no entries for /dev/sda2 in log file
searching for deleted inodes on /dev/sda2:
|==================================================|
489600 inodes scanned, 26 deleted files found

   user name | 1 <12 h | 2 <48 h | 3  <7 d | 4 <30 d | 5  <1 y | 6 older
-------------+---------+---------+---------+---------+---------+--------
root | 0 | 0 | 0 | 2 | 0 | 0
phost | 24 | 0 | 0 | 0 | 0 | 0
Select user name from table or press enter to exit: root
Select time interval (1 to 6) or press enter to exit: 4

  inode     size  deleted at        name
-----------------------------------------------------------
13 35840 Dec 19 17:43 2007 * ASCII text
14 10485760 Dec 19 17:43 2007 * ASCII text
Select an inode listed above or press enter to go back: 13
35840 bytes written to /tmp/recover//inode-13-ASCII_text
Select an inode listed above or press enter to go back:

   user name | 1 <12 h | 2 <48 h | 3  <7 d | 4 <30 d | 5  <1 y | 6 older
-------------+---------+---------+---------+---------+---------+--------
root | 0 | 0 | 0 | 2 | 0 | 0
phost | 24 | 0 | 0 | 0 | 0 | 0
Select user name from table or press enter to exit:
#

e2undel 是一个交互式的命令,命令执行过程中需要输入的内容已经使用黑体表示出来了。从输出结果中可以看出,e2undel 一共在这个文件系统中找到了 26 个文件,其中 root 用户删除的有两个。这些文件按照删除时间的先后顺序被划分到几类中。索引节点号 13 对应的是一个 ASCII 正文的文本文件,最终被恢复到 /tmp/recover//inode-13-ASCII_text 文件中。查看一下这个文件就会发现,正是我们在本系列前两部分中删除的那个 35KB 的测试文件。

利用 libundel 库完美恢复文件

尽管 e2undel 可以非常方便地简化恢复文件的过程,但是美中不足的是,其恢复出来的文件的文件名却丢失了,其原因是文件名是保存在父目录的目录项中的,而不是保存在索引节点中的。本系列文章第 2 部分中给出了一种通过遍历父目录的目录项来查找已删除文件的文件名的方法,但是由于索引节点会被重用,因此通过这种方式恢复出来的文件名也许并不总是正确的。另外,如果目录结构的非常复杂,就很难确定某个文件的父目录究竟是哪个,因此查找正确文件名的难度就会变得很大。如果能在删除文件时记录下索引节点号和文件名之间的对应关系,这个问题就能完美地解决了。

这个问题在 e2undel 中得到了完美的解决。实际上,所有删除命令,例如 rm、unlink 都是通过一些底层的系统调用(例如 unlink(2)、rmdir(2))来实现的。基于这一点,e2undel 又利用了Linux 系统中动态链接库加载时提供的一种便利:如果设置了环境变量 LD_PRELOAD,那么在加载动态链接库时,会优先从 $LD_PRELOAD 指向的动态链接库中查找符号表,然后才会在系统使用 ldconfig 配置的动态链接库中继续查找符号表。因此,我们可以在自己编写的库函数中实现一部分系统调用,并将这个库优先于系统库加载,这样就能欺骗系统使用我们自己定义的系统调用来执行原有的操作。具体到 e2undel 上来说,就是要在调用这些系统调用删除文件时,记录下文件名和索引节点号之间的对应关系。

在编译 e2undel 源代码之后,还会生成一个库文件 libundel.so.1.0,其中包含了删除文件时所使用的一些系统调用的钩子函数。e2undel官方主页上下载的源码包中仅仅包括了对 unlink(2) 和 remove(3) 这两个系统调用的钩子函数,但是从 2.6.16 版本的内核开始,引入了一系列新的系统调用,包括 faccessat(2), fchmodat(2), fchownat(2), fstatat(2), futimesat(2), linkat(2), mkdirat(2), mknodat(2), openat(2), readlinkat(2), renameat(2), symlinkat(2), unlinkat(2), mkfifoat(3) 等,尽管这些系统调用目前还没有成为POSIX标准的一部分,但是相信这个过程不会很久了。目前诸如 Fecora Core 8 之类的系统中自带的 rm 命令(属于 coreutils)包已经使用这些新的系统调用进行了改写,另外本文下载部分中的补丁文件中已经提供了对 rmdir 和 unlinkat 的钩子函数。部分源代码如下所示:


清单5. libundel.c 代码片断
                
void _init()
{
f = fopen("/var/e2undel/e2undel", "a");
if (!f)
fprintf(stderr, "libundel: can't open log file, undeletion disabled/n");
}

......

int rmdir(const char *pathname)
{
int err;
struct stat buf;
char pwd[PATH_MAX];
int (*rmdirp)(char *) = dlsym(RTLD_NEXT, "rmdir");

  if (NULL != pathname)
{
if (__lxstat(3, pathname, &buf)) buf.st_ino = 0;
if (!realpath(pathname, pwd)) pwd[0] = '/0';
}
err = (*rmdirp)((char *) pathname);
if (err) return err; /* remove() did not succeed */
if (f)
{
if (!S_ISLNK(buf.st_mode)) /* !!! should we check for other file types? */
{ /* don't log deleted symlinks */
fprintf(f, "%ld,%ld::%ld::%s/n",
(long) (buf.st_dev & 0xff00) / 256,
(long) buf.st_dev & 0xff,
(long) buf.st_ino, pwd[0] ? pwd : pathname);
fflush(f);
}
} /* if (f) */
return err;
} /* rmdir() */

......

void _fini()
{
if (f) fclose(f);
}

_init 和 _fini 这两个函数分别在打开和关闭动态链接库时执行,分别用来打开和关闭日志文件。如果某个命令(例如 rm)执行了 rmdir 系统调用,就会被这个库接管。rmdir 的处理比较简单,它先搜索到真正的 rmdir 系统调用的符号表,然后使用同样的参数来执行这个系统调用,如果成功,就将有关索引节点和文件名之类的信息记录到日志中(默认是 /var/e2undel/ e2undel)。

这个库的使用非常简单,请执行下面的命令:


清单6. libundel 的设置
                
# cp libundel.so.1.0 /usr/local/lib
# cd /usr/local/lib
# ln -s libundel.so.1.0 libundel.so.1
# ln –s libundel.so.1.0 libundel.so

# ldconfig
# mkdir /var/e2undel
# chmod 711 /var/e2undel
# touch /var/e2undel/e2undel
# chmod 622 /var/e2undel/e2undel

上面的设置仅仅允许 root 用户可以恢复文件,如果希望让普通用户也能恢复文件,就需要修改对应文件的权限设置。

现在尝试以另外一个用户的身份来删除些文件:


清单7. 设置libundel之后删除文件
                
$ export LD_PRELOAD=/usr/local/lib/libundel.so
$ rm -rf e2undel-0.82

要想记录所有用户的删除文件的操作,可以将 export LD_PRELOAD=/usr/local/lib/libundel.so 这行内容加入到 /etc/profile 文件中。

现在使用 e2undel 来恢复已删除的文件就变得简单多了,因为已经可以通过文件名来恢复文件了:


清单8. e2undel 利用 libundel 日志恢复删除文件
                
# ./e2undel -a -t -d /dev/sda2 -s /tmp/recover/
./e2undel 0.82
Trying to recover files on /dev/sda2, saving them on /tmp/recover/

/dev/sda2 opened for read-only access
/dev/sda2 was not cleanly unmounted.
Do you want wo continue (y/n)? y
489600 inodes (489531 free)
977956 blocks of 4096 bytes (941559 free)
last mounted on Fri Dec 28 20:45:05 2007

reading log file: 
found 24 entries for /dev/sda2 in log file
searching for deleted inodes on /dev/sda2:
|==================================================|
489600 inodes scanned, 26 deleted files found
checking names from log file for deleted files: 24 deleted files with names

   user name | 1 <12 h | 2 <48 h | 3  <7 d | 4 <30 d | 5  <1 y | 6 older
-------------+---------+---------+---------+---------+---------+--------
root | 0 | 0 | 0 | 2 | 0 | 0
phost | 24 | 0 | 0 | 0 | 0 | 0
Select user name from table or press enter to exit: phost
Select time interval (1 to 6) or press enter to exit: 1

  inode     size  deleted at        name
-----------------------------------------------------------
310083 0 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82
310113 2792 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/BUGS
310115 3268 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/HISTORY
310116 1349 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/INSTALL
310117 1841 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/INSTALL.de
310118 2175 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/Makefile
310119 12247 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/README
310120 9545 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/README.de
310121 13690 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/apprentice.c
310122 19665 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/ascmagic.c
310123 221 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/common.h
310124 1036 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/compactlog.c
310125 30109 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/e2undel.c
310127 2447 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/e2undel.h
310128 1077 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/file.c
310129 2080 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/file.h
310130 4484 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/find_del.c
310131 2141 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/is_tar.c
310132 2373 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/libundel.c
310133 7655 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/log.c
310134 39600 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/magic.h
310135 4591 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/names.h
310136 13117 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/softmagic.c
310137 5183 Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/tar.h

如果对所有用户都打开这个功能,由于日志文件是单向增长的,随着时间的推移,可能会变得很大,不过 e2undel 中还提供了一个 compactlog 工具来删除日志文件中的重复项。

在学习本系列文章介绍的技术之后,利用 e2undel 之类的工具,并使用本系列文章第一部分中提供的补丁,恢复删除文件就变得非常简单了。但是在日常使用过程中,大家可能还会碰到一些意外的情况,比如文件系统发生问题,从而无法正常挂载;使用 mke2fs 之类的工具重做了文件系统;甚至磁盘上出现坏道。此时应该如何恢复系统中的文件呢,下面让我们来逐一看一下如何解决这些问题。



文件系统故障的恢复

回想一下,在超级块中保存了有关文件系统本身的一些数据,在 ext2 文件系统中,还使用块组描述符保存了有关块组的信息;另外,索引节点位图、块位图中分别保存了索引节点和磁盘上数据块的使用情况,而文件本身的索引节点信息(即文件的元数据)则保存在索引节点表中。这些数据对于文件系统来说都是至关重要的,它们是存取文件的基础。如果超级块和块组描述符的信息一旦出错,则会造成文件系统无法正常挂载的情况出现。造成这些信息出错的原因有:

  • 系统管理员操作失误。
  • 设备驱动程序或第三方软件(例如mke2fs之类的)有 bug。
  • 电源意外断电。
  • 内核有 bug。

如果出现这种问题,可能造成的后果有:

  1. 文件系统无法挂载。
  2. 操作系统挂起。
  3. 即使文件系统能够成功挂载,在系统重启时也可能会看到一些错误,或者目录列表中出现乱字符的情况等。

下面让我们来模拟一个出现这种错误的情况。我们知道,超级块信息就保存在分区中的第一个块中,现在我们来试验一下清空这个块中数据的后果:


清单9. 清空超级块信息的后果
                
# dd if=/dev/zero of=/dev/sda2 bs=4096 count=1

# mount /dev/sda2 /tmp/test -t ext2
mount: wrong fs type, bad option, bad superblock on /dev/sda2,
missing codepage or helper program, or other error
In some cases useful info is found in syslog - try
dmesg | tail or so

由于无法从磁盘上读取到有效的超级块信息,mount 命令已经无法挂载 /dev/sda2 设备上的文件系统了。

为了防止这个问题会造成严重的后果,ext2 文件系统会在每个块组中保存一份超级块的拷贝。当然,这会造成一定的空间浪费,因此在最新的 ext2 文件系统中,只是在特定的块组中保存一份超级块的拷贝。具体来说,是在第 0、1 个块组和第 3、5、7 的整数次幂个块组中保存一份超级块的拷贝,而其他块组中的空间都可以节省出来了。下面来看一个 20GB 大小的文件系统的实际例子:


清单10. ext2 文件系统中超级块拷贝的位置
                
# dumpe2fs /dev/sdb6 | grep -i superblock
dumpe2fs 1.39 (29-May-2006)
Primary superblock at 0, Group descriptors at 1-2
Backup superblock at 32768, Group descriptors at 32769-32770
Backup superblock at 98304, Group descriptors at 98305-98306
Backup superblock at 163840, Group descriptors at 163841-163842
Backup superblock at 229376, Group descriptors at 229377-229378
Backup superblock at 294912, Group descriptors at 294913-294914
Backup superblock at 819200, Group descriptors at 819201-819202
Backup superblock at 884736, Group descriptors at 884737-884738
Backup superblock at 1605632, Group descriptors at 1605633-1605634
Backup superblock at 2654208, Group descriptors at 2654209-2654210
Backup superblock at 4096000, Group descriptors at 4096001-4096002

这是一个 20GB 大的 ext2 文件系统,每个块组的大小是 32768 个块,超级块一共有 11 个拷贝,分别存储在第 0、1、3、5、7、9、25、27、49、81 和 125 个块组中。默认情况下,内核只会使用第一个超级块中的信息来对磁盘进行操作。在出现故障的情况下,就可以使用这些超级块的备份来恢复数据了。具体说来,有两种方法:首先 mount 命令可以使用 sb 选项指定备用超级块信息来挂载文件系统:


清单11. 使用超级块拷贝挂载文件系统
                
# mount -o sb=131072 /dev/sda2 /tmp/test -t ext2

需要注意的是,mount 命令中块大小是以 1024 字节为单位计算的,而这个文件系统则采用的是 4096 字节为单位的块,因此 sb 的值应该是 32768*4=131072。

尽管 mount 命令可以使用备用超级块来挂载文件系统,但却无法修复主超级块的问题,这需要使用 e2fsck 这个工具来完成:


清单12. 利用 e2fsck 工具修复 ext2 文件系统中主超级块的问题
                
# e2fsck /dev/sda2
e2fsck 1.40.2 (12-Jul-2007)
Couldn't find ext2 superblock, trying backup blocks...
/dev/sda2 was not cleanly unmounted, check forced.
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information

/dev/sda2: ***** FILE SYSTEM WAS MODIFIED *****
/dev/sda2: 11/489600 files (9.1% non-contiguous), 17286/977956 blocks

# mount /dev/sda2 /tmp/test -t ext2

e2fsck 工具可以检查出主超级块的问题,然后从其他超级块拷贝中读取数据,并使用它来恢复主超级块中的数据(在 ext2 文件系统中,超级块信息保存在一个 ext2_super_block 的数据结构中,详细信息请参考内核源代码)。修复主超级块的问题之后,mount 命令就可以成功挂载原来的文件系统了。



重建文件系统的解决办法

在日常使用过程中,可能碰到的另外一个问题是管理员可能错误地执行了某些命令,例如使用mke2fs 重建了文件系统,从而造成数据的丢失。实际上,在 mke2fs 创建文件系统的过程中,并不会真正去清空原有文件系统中存储的文件的数据,但却会重新生成超级块、块组描述符之类的信息,并清空索引节点位图和块位图中的数据,最为关键的是,它还会清空索引节点表中的数据。因此尽管文件数据依然存储在磁盘上,但是由于索引节点中存储的文件元数据已经丢失了,要想完整地恢复原有文件,已经变得非常困难了。

然而,这个问题也并非完全无法解决。在 e2fsprogs 包中还提供了一个名为 e2image 的工具,可以用来将 ext2 文件系统中的元数据保存到一个文件中,下面是一个例子:


清单13. 使用超级块拷贝挂载文件系统
                
# e2image -r /dev/sda2 sda2.e2image

这会生成一个与文件系统大小相同的文件,其中包含了文件系统的元数据,包括索引节点中的间接块数据以及目录数据。另外,其中所有数据的位置均与磁盘上存储的位置完全相同,因此可以使用 debugfs、dumpe2fs 之类的工具直接查看:


清单14. 使用 debugfs 查看 e2image 映像文件的信息
                
# debugfs sda2.e2image.raw
debugfs 1.40.2 (12-Jul-2007)
debugfs: ls -l
2 40755 (2) 0 0 4096 31-Dec-2007 15:56 .
2 40755 (2) 0 0 4096 31-Dec-2007 15:56 ..
11 40700 (2) 0 0 16384 31-Dec-2007 15:54 lost+found
12 100644 (1) 0 0 10485760 31-Dec-2007 15:56 testfile.10M
13 100644 (1) 0 0 35840 31-Dec-2007 15:56 testfile.35K

为了节省空间,这些映像文件以稀疏文件的形式保存在磁盘上,在一个 4GB 的文件系统中,如果 55 万个索引节点中已经使用了 1 万 5 千个,使用 bizp2 压缩后的文件大概只有 3MB左右。

当然,这些映像文件中并没有包含实际文件的数据,不过文件数据依然保存在磁盘上,因此只要及时备份相关信息,在发生意外的情况下是有可能恢复大部分数据的。



磁盘坏道情况的处理

随着磁盘使用的时间越来越长,难免会出现磁盘上出现一些物理故障,比如产生物理坏道。根据物理损坏的严重程度,可能会造成文件丢失、文件系统无法加载甚至整个磁盘都无法识别的情况出现。因此要想将损失控制在最小范围内,除了经常备份数据,在发现问题的第一时间采取及时地应对措施也非常重要。

物理故障一旦出现,极有可能会有加剧趋势,因此应该在恢复数据的同时,尽量减少对磁盘的使用,dd 命令可以用来创建磁盘的完美映像。应该使用的命令如下:


清单15. 使用 dd 命令创建磁盘映像
                
# dd if=/dev/sdb of=/images/sdb.image bs=4096 conv=noerror,sync

noerror 参数告诉 dd 在碰到读写错(可能是由于坏道引起的)时继续向下操作,而不是停止退出。sync 参数说明对于从源设备无法正常读取的块,就使用NULL填充。默认情况下,dd 使用 512 字节作为一个块的单位来读写 I/O 设备,指定 bs 为 4096 字节可以在一定程度上加速 I/O 操作,但同时也会造成一个问题:如果所读取的这个 4096 字节为单位的数据块中某一部分出现问题,则整个 4096 字节的就全部被清空了,这样会造成数据的丢失。为了解决这种问题,我们可以使用 dd_rescue 这个工具(可以从 http://www.garloff.de/kurt/linux/ddrescue/ 上下载),其用法如下:


清单16. 使用 dd_rescue 命令创建磁盘映像
                
# dd_rescue /dev/sdb /images/sdb.image –b 65536 –B 512

与 dd 相比,dd_rescue 强大之处在于在碰到错误时,可以以更小的数据块为单位重新读取这段数据,从而确保能够读出尽量多的数据。上面命令中的参数指明正常操作时以 64KB 为单位读取磁盘数据,一旦出错,则以 512 字节为单位重新读取这段数据,直至整个硬盘被完整读出为止。

获得磁盘映像之后,就可以将其当作普通磁盘一样进行操作了。应用本系列文章中介绍的技术,应该能从中恢复出尽可能多的数据。当然,对于那些刚好处于坏道位置的数据,那就实在回天乏力了。



恢复文件策略

截至到现在,本系列文章中介绍的都是在删除文件或出现意外情况之后如何恢复文件,实际上,对于保证数据可用性的目的来讲,这些方法都无非是亡羊补牢而已。制定恰当地数据备份策略,并及时备份重要数据才是真正的解决之道。

不过即使有良好的数据备份策略,也难免会出现有部分数据没有备份的情况。因此,一旦出现误删文件的情况,应该立即执行相应的对策,防止文件数据被覆盖:

  • 断开所有对文件系统的访问。fuser 命令可以用来帮助查看和杀死相关进程,详细用法请参看 fuser 的手册。
  • 如果业务无法停顿,就将文件系统以只读方式重新加载,命令格式为:mount -r -n -o remount mountpoint
  • 应用本系列文章介绍的技术恢复文件。

当然,在进行数据备份的同时,也需要考虑本文中介绍的一些技术本身的要求,例如 e2image映像文件、e2undel 的日志文件等,都非常重要,值得及时备份。




本系列文章的前 3 部分详细介绍了 ext2 文件系统中文件的数据存储格式,并讨论了各种情况下数据恢复的问题。作为 ext2 文件系统的后继者,ext3 文件系统可以实现与 ext2 文件系统近乎完美的兼容。但是前文中介绍的各种技术在 ext3 文件系统中是否同样适用呢?ext3 文件系统中删除文件的恢复又有哪些特殊之处呢?本文将逐一探讨这些问题。

ext3:日志文件系统

由于具有很好的文件存取性能,ext2 文件系统自从 1993 年发布之后,已经迅速得到了用户的青睐,成为很多 Linux 发行版中缺省的文件系统,原因之一在于 ext2 文件系统采用了文件系统缓存的概念,可以加速文件的读写速度。然而,如果文件系统缓存中的数据尚未写入磁盘,机器就发生了掉电等意外状况,就会造成磁盘数据不一致的状态,这会损坏磁盘数据的完整性(文件数据与元数据不一致),严重情况下甚至会造成文件系统的崩溃。

为了确保数据的完整性,在系统引导时,会自动检查文件系统上次是否是正常卸载的。如果是非正常卸载,或者已经使用到一定的次数,就会自动运行 fsck 之类的程序强制进行一致性检查(具体例子请参看本系列文章的第 2 部分),并修复存在问题的地方,使 ext2 文件系统恢复到新的一致状态。

然而,随着硬盘技术的发展,磁盘容量变得越来越大,对磁盘进行一致性检查可能会占用很长时间,这对于一些关键应用来说是不可忍受的;于是日志文件系统(Journal File System)的概念也就应运而生了。

所谓日志文件系统,就是在文件系统中借用了数据库中“事务”(transaction)的概念,将文件的更新操作变成原子操作。具体来说,就是在修改文件系统内容的同时(或之前),将修改变化记录到日志中,这样就可以在意外发生的情况下,就可以根据日志将文件系统恢复到一致状态。这些操作完全可以在重新挂载文件系统时来完成,因此在重新启动机器时,并不需要对文件系统再进行一致性检查,这样可以大大提高系统的可用程度。

Linux 系统中目前已经出现了很多日志文件系统,例如 SGI 开发的 XFS、IBM 开发的 JFS 和 ReiserFS 以及 ext3 等。与其他日志文件系统相比,ext3 最大的特性在于它完全兼容 ext2 文件系统。用户可以在 ext2 和 ext3 文件系统之间无缝地进行变换,二者在磁盘上采用完全相同的的数据格式进行存储,因此大部分支持 ext2 文件系统的工具也都可以在 ext3 文件系统上使用。甚至为 ext2 开发的很多特性也都可以非常平滑地移植到 ext3 文件系统上来。ext3 文件系统的另外一个特性在于它提供了 3 种日志模式,可以满足各种不同用户的要求:

  • data=journal:这会记录对所有文件系统数据和元数据的修改。这种模式可以将数据丢失的风险降至最低,但是速度也最慢。
  • data=ordered:仅仅记录对文件系统元数据的修改,但是在修改相关文件系统元数据之前,需要将文件数据同步到磁盘上。
  • data=writeback:仅仅记录对文件系统元数据的修改,对文件数据的修改按照标准文件系统的写操作过程进行处理。这种模式速度最快。

在重新挂载文件系统时,系统会自动检查日志项,将尚未提交到磁盘上的操作重新写入磁盘,从而确保文件系统的状态与最后一次操作的结果保持一致。



ext3 文件系统探索

下面让我们通过一个例子来了解一下 ext3 文件系统中有关日志的一些详细信息。


清单1. 创建 ext3 文件系统
                
# mkfs.ext3 /dev/sdb7
mke2fs 1.39 (29-May-2006)
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
2443200 inodes, 4885760 blocks
244288 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=0
150 block groups
32768 blocks per group, 32768 fragments per group
16288 inodes per group
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000

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

This filesystem will be automatically checked every 27 mounts or
180 days, whichever comes first. Use tune2fs -c or -i to override.

在清单 1 中,我们使用 mkfs.ext3 创建了一个 ext3 类型的文件系统,与 ext2 文件系统相比,mkfs.ext3 命令额外在文件系统中使用 32768 个数据块创建了日志。实际上,ext2 文件系统可以使用 tune2fs 命令平滑地转换成 ext3 文件系统,用法如清单 2 所示。


清单2. 使用 tune2fs 将 ext2 文件系统转换成 ext3 文件系统
                
# tune2fs -j /dev/sdb6
tune2fs 1.39 (29-May-2006)
Creating journal inode: done
This filesystem will be automatically checked every 28 mounts or
180 days, whichever comes first. Use tune2fs -c or -i to override.

类似地,dumpe2fs 命令也可以用来查看有关 ext3 文件系统的信息:


清单3. 使用 dumpe2fs 查看 ext3 文件系统的信息
                
# dumpe2fs /dev/sdb7 | grep "Group 0" -B 10 -A 21
dumpe2fs 1.39 (29-May-2006)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 128
Journal inode: 8
Default directory hash: tea
Directory Hash Seed: 69de4e53-27fc-42db-a9ea-36debd6e68de
Journal backup: inode blocks
Journal size: 128M


Group 0: (Blocks 0-32767)
Primary superblock at 0, Group descriptors at 1-2
Reserved GDT blocks at 3-1024
Block bitmap at 1025 (+1025), Inode bitmap at 1026 (+1026)
Inode table at 1027-1535 (+1027)
0 free blocks, 16277 free inodes, 2 directories
Free blocks:
Free inodes: 12-16288
Group 1: (Blocks 32768-65535)
Backup superblock at 32768, Group descriptors at 32769-32770
Reserved GDT blocks at 32771-33792
Block bitmap at 33793 (+1025), Inode bitmap at 33794 (+1026)
Inode table at 33795-34303 (+1027)
29656 free blocks, 16288 free inodes, 0 directories
Free blocks: 35880-65535
Free inodes: 16289-32576
Group 2: (Blocks 65536-98303)
Block bitmap at 65536 (+0), Inode bitmap at 65537 (+1)
Inode table at 65538-66046 (+2)
32257 free blocks, 16288 free inodes, 0 directories
Free blocks: 66047-98303
Free inodes: 32577-48864

从清单 3 中的输出结果可以看出,这个文件系统上的日志一共占用了 128MB 的空间,日志文件使用索引节点号为 8,块组 0 和块组 1 中空闲块比其他块组明显要少,这是因为日志文件主要就保存在这两个块组中了,这一点可以使用 debugfs 来验证:


清单4. 查看日志文件的信息
                
# debugfs /dev/sdb7
debugfs 1.39 (29-May-2006)
debugfs: stat <8>
Inode: 8 Type: regular Mode: 0600 Flags: 0x0 Generation: 0
User: 0 Group: 0 Size: 134217728
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 262416
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x4795d200 -- Tue Jan 22 19:22:40 2008
atime: 0x00000000 -- Thu Jan 1 08:00:00 1970
mtime: 0x4795d200 -- Tue Jan 22 19:22:40 2008
BLOCKS:
(0-11):1542-1553, (IND):1554, (12-1035):1555-2578, (DIND):2579, /
(IND):2580, (1036-2059):2581-3604, (IND):3605, (2060-3083):3606-4629
, (IND):4630, (3084-4107):4631-5654, (IND):5655, (4108-5131):5656-6679, /
(IND):6680, (5132-6155):6681-7704, (IND):7705, (6156-7179):7
706-8729, (IND):8730, (7180-8203):8731-9754, (IND):9755, (8204-9227):9756-10779, /
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
(IND):10780, (9228-10251):10781-11804, (IND):11805,
(10252-11275):11806-12829, (IND):12830, (11276-12299):12831-13854, /
(IND):13855, (12300-13323):13856-14879, (IND):14880, (13324-1434
7):14881-15904, (IND):15905, (14348-15371):15906-16929, /
(IND):16930, (15372-16395):16931-17954, (IND):17955, (16396-17419):17956-189
79, (IND):18980, (17420-18443):18981-20004, (IND):20005, (18444-19467):20006-21029, /
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
(IND):21030, (19468-20491):21031-22054, (IND):22
055, (20492-21515):22056-23079, (IND):23080, (21516-22539):23081-24104, /
(IND):24105, (22540-23563):24106-25129, (IND):25130, (23564-
24587):25131-26154, (IND):26155, (24588-25611):26156-27179, /
(IND):27180, (25612-26635):27181-28204, (IND):28205, (26636-27659):28206
-29229, (IND):29230, (27660-28683):29231-30254, (IND):30255, /
(28684-29707):30256-31279, (IND):31280, (29708-30731):31281-32304, (IND
):32305, (30732-31193):32306-32767, (31194-31755):34304-34865, /
(IND):34866, (31756-32768):34867-35879
TOTAL: 32802

debugfs:.

另外,ext3 的日志文件也可以单独存储到其他设备上。但是无论如何,这对于用户来说都是透明的,用户根本就觉察不到日志文件的存在,只是内核在挂载文件系统时会检查日志文件的内容,并采取相应的操作,使文件系统恢复到最后一次操作时的一致状态。

对于恢复删除文件的目的来说,我们并不需要关心日志文件,真正应该关心的是文件在磁盘上的存储格式。实际上,ext3 在这方面完全兼容 ext2,以存储目录项和索引节点使用的数据结构为例,ext3 使用的两个数据结构 ext3_dir_entry_2 和 ext3_inode 分别如清单 5 和清单 6 所示。与 ext2 的数据结构对比一下就会发现,二者并没有什么根本的区别,这正是 ext2 和 ext3 文件系统可以实现自由转换的基础。


清单5. ext3_dir_entry_2 结构
                
/*
* The new version of the directory entry. Since EXT3 structures are
* stored in intel byte order, and the name_len field could never be
* bigger than 255 chars, it's safe to reclaim the extra byte for the
* file_type field.
*/
struct ext3_dir_entry_2 {
__le32 inode; /* Inode number */
__le16 rec_len; /* Directory entry length */
__u8 name_len; /* Name length */
__u8 file_type;
char name[EXT3_NAME_LEN]; /* File name */
};


清单6. ext3_inode 结构
                
/*
* Structure of an inode on the disk
*/
struct ext3_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Creation time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks; /* Blocks count */
__le32 i_flags; /* File flags */
...
__le32 i_block[EXT3_N_BLOCKS];/* Pointers to blocks */
...
};

既然 ext3 与 ext2 文件系统有这么好的兼容性和相似性,这是否就意味着本系列文章前 3 部分介绍的各种技术同样也适用于 ext3 文件系统呢?对于大部分情况来说,答案是肯定的。ext3 与 ext2 文件系统存储文件所采用的机制并没有什么不同,第 1 部分中介绍的原理以及后续介绍的 debugfs 等工具也完全适用于 ext3 文件系统。

然而,这并非就说 ext3 与 ext2 文件系统是完全相同的。让我们来看一个例子:清单 7 给出了在 ext3 文件系统中删除一个文件前后索引节点的变化。


清单7. ext3 文件系统中删除文件前后索引节点信息的变化
                
# debugfs /dev/sdb7
debugfs 1.39 (29-May-2006)
debugfs: stat <48865>
Inode: 48865 Type: regular Mode: 0644 Flags: 0x0 Generation: 3736765465
User: 0 Group: 0 Size: 61261
File ACL: 99840 Directory ACL: 0
Links: 1 Blockcount: 136
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x478618e1 -- Thu Jan 10 21:08:49 2008
atime: 0x478618e1 -- Thu Jan 10 21:08:49 2008
mtime: 0x478618e1 -- Thu Jan 10 21:08:49 2008
BLOCKS:
(0-11):129024-129035, (IND):129036, (12-14):129037-129039
TOTAL: 16

debugfs: q

# rm -f Home.html
# sync
# cd ..
# umount test

# debugfs /dev/sdb7
debugfs 1.39 (29-May-2006)
debugfs: lsdel
Inode Owner Mode Size Blocks Time deleted
0 deleted inodes found.
debugfs: stat <48865>
Inode: 48865 Type: regular Mode: 0644 Flags: 0x0 Generation: 3736765465
User: 0 Group: 0 Size: 0
File ACL: 99840 Directory ACL: 0
Links: 0 Blockcount: 0
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x47861900 -- Thu Jan 10 21:09:20 2008
atime: 0x478618e1 -- Thu Jan 10 21:08:49 2008
mtime: 0x47861900 -- Thu Jan 10 21:09:20 2008
dtime: 0x47861900 -- Thu Jan 10 21:09:20 2008
BLOCKS:

debugfs: q

仔细看一下结果就会发现,在删除文件之后,除了设置了删除时间 dtime 之外,还将文件大小(size)设置为 0,占用块数(Blockcount)也设置为 0,并清空了存储文件数据的数据(i_block 数组)。这正是 ext3 文件系统与 ext2 文件系统在删除文件时最重要的一点的区别:在删除文件时,对于 ext2 文件系统来说,操作系统只是简单地修改对应索引节点中的删除时间,并修改索引节点位图和数据块位图中的标志,表明它们已经处于空闲状态,可以被重新使用;而对于 ext3 文件系统来说,还清除了表明数据块存放位置的字段(i_block),并将索引节点中的文件大小信息设置为 0。然而,这点区别对于恢复被删除文件的用途来说却是至关重要的,因为缺少了文件大小和数据块位置的信息,尽管文件数据依然完好地保存在磁盘上,但却没有任何一条清晰的线索能够说明这个文件的数据块被存储到哪些磁盘块中,以及这些数据块的相互顺序如何,文件中间是否存在文件洞等信息,因此要想完整地恢复文件就变得非常困难了。这也正是使用 debugfs 的 dump 命令在 ext3 文件系统中并不能恢复已删除文件的原因。

不过,这是否就意味着 ext3 文件系统中删除的文件就无法恢复了呢?其实不然。基于这样一个事实:“在删除文件时,并不会将文件数据真正从磁盘上删除”,我们可以采用其他一些方法来尝试恢复数据。



ext3 文件系统中恢复删除文件的方法 1:正文匹配

我们知道,磁盘以及磁盘上的某个分区在系统中都以设备文件的形式存在,我们可以将这些设备文件当作普通文件一样来读取数据。不过,既然已经无法通过文件名来定位文件的数据块位置,现在要想恢复文件,就必须了解文件的数据,并通过正文匹配进行检索了。自然,grep 就是这样一个理想的工具:


清单8. 使用 grep 搜索磁盘上的文件
                
# ./creatfile.sh 35 testfile.35K

# rm -f testfile.35K

# cd ..

# umount test

# grep -A 1 -B 1 -a -n " 10:0" /dev/sdb7 > sdb7.testfile.35K
grep: memory exhausted

# cat sdb7.testfile.35K
545- 9:0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
546: 10:0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
547- 11:0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,

在清单 8 中的例子中,我们首先使用本系列文章第 1 部分中提供的脚本创建了一个测试文件,在删除该文件后,通过利用 grep 以“ 10:0”作为关键字对设备文件进行正文匹配,我们顺利地找到了测试文件中的第 10 行数据。需要注意的是,grep 命令中我们使用了几个参数,-a 参数表明将设备文件当作文本文件进行处理,-B 和 –A 参数分别说明同时打印匹配项的前/后几行的数据。同一关键字可能会在很多文件中多次出现,因此如何从中挑选出所需的数据就完全取决于对数据的熟悉程度了。

利用 grep 进行正文匹配也存在一个问题,由于打开的设备文件非常大,grep 会产生内存不足的问题,因此无法完成整个设备的正文匹配工作。解决这个问题的方法是使用 strings。strings 命令可以将文件中所有可打印字符全部打印出来,因此对于正文匹配的目的来说,它可以很好地实现文本数据的提取工作。


清单9. 使用 strings 提取设备文件中的可打印数据
                
# time strings /dev/sdb7 > sdb7.strings

real 12m42.386s
user 10m44.140s
sys 1m42.950s

# grep " 10:0" sdb7.strings
10:0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,

清单 9 中的例子使用 strings 将 /dev/sdb7 中的可打印字符提取出来,并重定向到一个文本文件中,此后就可以使用任何文本编辑工具或正文匹配工具从这个文件中寻找自己的数据了。不过扫描磁盘需要的时间可能会比较长,在上面这个例子中,扫描 20GB 的分区大概需要 13 分钟。


ext3 文件系统中恢复删除文件的方法 2:提前备份元数据

利用 grep 或 strings 对磁盘数据进行正文匹配,这种方法有一定的局限性:它只是对文本文件的恢复比较有用,这还要依赖于用户对数据的熟悉程度;而对于二进制文件来说,除非有其他备份,否则要通过这种正文匹配的方式来恢复文件,几乎是不可能完成的任务。然而,如果没有其他机制的辅助,在 ext3 文件系统中,这却几乎是唯一可以尝试用来恢复数据的方法了。究其原因是因为,这种方法仅仅是一种亡羊补牢的做法,而所需要的一些关键数据已经不存在了,因此恢复文件就变得愈发困难了。不过,索引节点中的这些关键信息在删除文件之前肯定是存在的。

受本系列文章介绍过的 debugfs 和 libundel 的启发,我们可以对 libundel 进行扩充,使其在删除文件的同时,除了记录文件名、磁盘设备、索引节点信息之外,把文件大小、数据块位置等重要信息也同时记录下来,这样就可以根据日志文件中的元数据信息完美地恢复文件了。

为了实现以上想法,我们需要对 libundel 的实现进行很大的调整,下面介绍所使用的几个关键的函数。


清单10. get_bmap 函数的实现
                
static unsigned long get_bmap(int fd, unsigned long block)
{
int ret;
unsigned int b;

b = block;
ret = ioctl(fd, FIBMAP, &b); /* FIBMAP takes a pointer to an integer */
if (ret < 0)
{
if (errno == EPERM)
{
if (f)
{

{ /* don't log deleted symlinks */
fprintf(f, "No permission to use FIBMAP ioctl./n");
fflush(f);
}
} /* if (f) */

return 0;
}
}
return b;
}

get_bmap 函数是整个改进的基础,其作用是返回磁盘上保存指定文件的某块数据使用的数据块的位置,这是通过 ioctl 的 FIBMAP 命令来实现的。ioctl 提供了一种在用户空间与内核进行交互的便捷机制,在内核中会通过调用 f_op->ioctl() 进行处理。对于 FIBMAP 命令来说,会由 VFS 调用 f_op->bmap() 获取指定数据块在磁盘上的块号,并将结果保存到第 3 个参数指向的地址中。


清单11. get_blocks 函数的实现
                
int get_blocks(const char *filename)
{
#ifdef HAVE_FSTAT64
struct stat64 fileinfo;
#else
struct stat fileinfo;
#endif
int bs;
long fd;
unsigned long block, last_block = 0, first_cblock = 0, numblocks, i;
long bpib; /* Blocks per indirect block */
char cblock_list[256];
char pwd[PATH_MAX];

if (NULL != filename)
{
if (__lxstat(3, filename, &fileinfo))
fileinfo.st_ino = 0;

if (!realpath(filename, pwd))
pwd[0] = '/0';
}


#ifdef HAVE_OPEN64
fd = open64(filename, O_RDONLY);
#else
fd = open(filename, O_RDONLY);
#endif
if (fd < 0) {
fprintf(stderr, "cannot open the file of %s/n", filename);
return -1;
}

if (ioctl(fd, FIGETBSZ, &bs) < 0) { /* FIGETBSZ takes an int */
perror("FIGETBSZ");
close(fd);
return -1;
}

bpib = bs / 4;
numblocks = (fileinfo.st_size + (bs-1)) / bs;

sprintf(block_list, "%ld,%ld::%ld::%ld::%ld::",
(long) (fileinfo.st_dev & 0xff00) / 256,
(long) fileinfo.st_dev & 0xff,
(long) fileinfo.st_ino,
(long) fileinfo.st_size, (long)bs);

for (i=0; i < numblocks; i++) {
block = get_bmap(fd, i);

if (last_block == 0) {
first_cblock = block;
}
if (last_block && (block != last_block +1) ) {
sprintf(cblock_list, "(%ld-%ld):%ld-%ld,",
i-(last_block-first_cblock)-1, i-1, first_cblock, last_block);
strcat(block_list, cblock_list);
first_cblock = block;
}

if (i == numblocks - 1 ) {
if (last_block == 0) {
sprintf(cblock_list, "(%ld-%ld):%ld-%ld", i, i, first_cblock, block);
}
else {
sprintf(cblock_list, "(%ld-%ld):%ld-%ld",
i-(last_block-first_cblock)-1, i, first_cblock, block);
}

strcat(block_list, cblock_list);
}

last_block = block;
}

sprintf(cblock_list, "::%s", pwd[0] ? pwd : filename);
strcat(block_list, cblock_list);

close(fd);

return 0;
}

get_blocks 函数的作用是遍历文件包含的每个数据块,获取它们在磁盘上保存的数据块位置。为了保证日志文件中的数据尽量精简,数据块位置会按照本身的连续情况被划分成一个个的连续块域,每个都记录为下面的形式:(文件中的起始块号-文件中的结束块号):磁盘上的起始数据块号-磁盘上的结束数据块号。

自然,get_blocks 函数应该是在用户调用删除文件的系统调用时被调用的,这些系统调用的实现也应该进行修改。清单 12 给出了 remove 库函数修改后的例子。


清单12. remove 库函数的实现
                
int remove(const char *pathname)
{
int err;
int (*removep)(char *) = dlsym(RTLD_NEXT, "remove");

err = get_blocks(pathname);
if (err < 0)
{
fprintf(stderr, "error while reading blocks from %s/n", pathname);
}

err = (*removep)((char *) pathname);
if (err) return err; /* remove() did not succeed */
if (f)
{
fprintf(f, "%s/n", block_list);
fflush(f);
} /* if (f) */
return err;
} /* remove() */

现在,记录元数据信息的日志文件 /var/e2undel/e2undel 如清单 13 所示:


清单13. 修改后的 /var/e2undel/e2undel 样例文件
                
8,23::48865::13690::4096::(0-3):106496-106499::/tmp/test/apprentice.c
8,23::48867::19665::4096::(0-4):106528-106532::/tmp/test/ascmagic.c
8,23::48872::1036::4096::(0-0):106545-106545::/tmp/test/compactlog.c
8,23::48875::31272::4096::(0-7):106596-106603::/tmp/test/e2undel.c
8,23::48878::1077::4096::(0-0):106616-106616::/tmp/test/file.c
8,23::48880::4462::4096::(0-1):106618-106619::/tmp/test/find_del.c
8,23::48885::2141::4096::(0-0):106628-106628::/tmp/test/is_tar.c
8,23::48887::6540::4096::(0-1):106631-106632::/tmp/test/libundel.c
8,23::48890::8983::4096::(0-2):106637-106639::/tmp/test/log.c
8,23::48897::13117::4096::(0-3):106663-106666::/tmp/test/softmagic.c
8,23::48866::10485760::4096::(0-11):108544-108555,(12-1035):108557-109580,/
(1036-2059):109583-110606,(2060-2559):110608-111107::/tmp/test/testfile.10M
8,23::48866::7169::4096::(1-1):129024-129024::/tmp/test/hole
8,23::48865::21505::4096::(1-1):129025-129025,(5-5):129026-129026::/tmp/test/hole2

文件名前所增加的 3 项分别为文件大小、块大小和数据块列表。

当然,为了能够利用 /var/e2undel/e2undel 中记录的信息恢复文件,e2undel 的对应实现也必须相应地进行修改。详细实现请参看本文下载部分给出的补丁,在此不再详述。修改后的 libundel 的用法与原来完全相同,详细介绍请参看本系列文章的第 3 部分。




ext3 文件系统中恢复删除文件的方法 3:修改 ext3 实现

利用 libundel 方法的确可以完美地恢复删除文件,但是这毕竟是一种治标不治本的方法,就像是本系列文章第 3 部分中所介绍的一样,这要依赖于环境变量 LD_PRELOAD 的设置。如果用户在删除文件之前,并没有正确设置这个环境变量,那么对应的删除操作就不会记录到日志文件 /var/e2undel/e2undel 中,因此也就无从恢复文件了。

还记得在本系列文章的第 2 部分中我们是如何通过修改 Linux 内核来支持大文件的删除的 吗?采用同样的思路,我们也可以修改 ext3 的实现,使其支持文件的恢复,这样本系列前面文章中介绍的工具就都可以在 ext3 文件系统上正常使用了。

总结一下,在删除文件时,内核中执行的操作至少包括:

  1. 在块位图中将该文件所占用的数据块标识为可用状态。
  2. 在索引节点位图中将该文件所占用的索引节点标识为可用状态。
  3. 将该文件索引节点中的硬链接数目设置为 0。
  4. 清空间接索引节点中的数据.
  5. 清空 i_block 数组中各个成员中的数据。
  6. 将索引节点中的文件大小(i_size)和占用块数(i_blocks)设置为 0。
  7. 将该文件索引节点中的删除时间设置为当前时间。
  8. 将父目录项中该文件对应项中的索引节点号设置为 0,并扩展前一项,使其包含该项所占用的空间。

其中步骤 5 和 6 只适用于 ext3 文件系统,在 ext2 文件系统中并不需要执行。在 ext3 文件系统的实现中,它们分别是由 fs/ext3/inode.c 中的 ext3_delete_inode 和 ext3_truncate 函数实现的:


清单14. ext3_delete_inode 函数实现
                
182 void ext3_delete_inode (struct inode * inode)
183 {
184 handle_t *handle;
185
186 truncate_inode_pages(&inode->i_data, 0);
187
188 if (is_bad_inode(inode))
189 goto no_delete;
190
191 handle = start_transaction(inode);
192 if (IS_ERR(handle)) {
193 /*
194 * If we're going to skip the normal cleanup, we still need to
195 * make sure that the in-core orphan linked list is properly
196 * cleaned up.
197 */
198 ext3_orphan_del(NULL, inode);
199 goto no_delete;
200 }
201
202 if (IS_SYNC(inode))
203 handle->h_sync = 1;
204 inode->i_size = 0;
205 if (inode->i_blocks)
206 ext3_truncate(inode);
207 /*
208 * Kill off the orphan record which ext3_truncate created.
209 * AKPM: I think this can be inside the above `if'.
210 * Note that ext3_orphan_del() has to be able to cope with the
211 * deletion of a non-existent orphan - this is because we don't
212 * know if ext3_truncate() actually created an orphan record.
213 * (Well, we could do this if we need to, but heck - it works)
214 */
215 ext3_orphan_del(handle, inode);
216 EXT3_I(inode)->i_dtime = get_seconds();
217
218 /*
219 * One subtle ordering requirement: if anything has gone wrong
220 * (transaction abort, IO errors, whatever), then we can still
221 * do these next steps (the fs will already have been marked as
222 * having errors), but we can't free the inode if the mark_dirty
223 * fails.
224 */
225 if (ext3_mark_inode_dirty(handle, inode))
226 /* If that failed, just do the required in-core inode clear. */
227 clear_inode(inode);
228 else
229 ext3_free_inode(handle, inode);
230 ext3_journal_stop(handle);
231 return;
232 no_delete:
233 clear_inode(inode); /* We must guarantee clearing of inode... */
234 }


清单15. ext3_truncate 函数实现
                
2219 void ext3_truncate(struct inode *inode)
2220 {
2221 handle_t *handle;
2222 struct ext3_inode_info *ei = EXT3_I(inode);
2223 __le32 *i_data = ei->i_data;
2224 int addr_per_block = EXT3_ADDR_PER_BLOCK(inode->i_sb);
2225 struct address_space *mapping = inode->i_mapping;
2226 int offsets[4];
2227 Indirect chain[4];
2228 Indirect *partial;
2229 __le32 nr = 0;
2230 int n;
2231 long last_block;
2232 unsigned blocksize = inode->i_sb->s_blocksize;
2233 struct page *page;
2234
...
2247 if ((inode->i_size & (blocksize - 1)) == 0) {
2248 /* Block boundary? Nothing to do */
2249 page = NULL;
2250 } else {
2251 page = grab_cache_page(mapping,
2252 inode->i_size >> PAGE_CACHE_SHIFT);
2253 if (!page)
2254 return;
2255 }
2256
2257 handle = start_transaction(inode);
2258 if (IS_ERR(handle)) {
2259 if (page) {
2260 clear_highpage(page);
2261 flush_dcache_page(page);
2262 unlock_page(page);
2263 page_cache_release(page);
2264 }
2265 return; /* AKPM: return what? */
2266 }
2267
2268 last_block = (inode->i_size + blocksize-1)
2269 >> EXT3_BLOCK_SIZE_BITS(inode->i_sb);
2270
2271 if (page)
2272 ext3_block_truncate_page(handle, page, mapping, inode->i_size);
2273
2274 n = ext3_block_to_path(inode, last_block, offsets, NULL);
2275 if (n == 0)
2276 goto out_stop; /* error */

...

2287 if (ext3_orphan_add(handle, inode))
2288 goto out_stop;
2289

...

2297 ei->i_disksize = inode->i_size;

...

2303 mutex_lock(&ei->truncate_mutex);
2304
2305 if (n == 1) { /* direct blocks */
2306 ext3_free_data(handle, inode, NULL, i_data+offsets[0],
2307 i_data + EXT3_NDIR_BLOCKS);
2308 goto do_indirects;
2309 }
2310
2311 partial = ext3_find_shared(inode, n, offsets, chain, &nr);
2312 /* Kill the top of shared branch (not detached) */
2313 if (nr) {
2314 if (partial == chain) {
2315 /* Shared branch grows from the inode */
2316 ext3_free_branches(handle, inode, NULL,
2317 &nr, &nr+1, (chain+n-1) - partial);
2318 *partial->p = 0;
2319 /*
2320 * We mark the inode dirty prior to restart,
2321 * and prior to stop. No need for it here.
2322 */
2323 } else {
2324 /* Shared branch grows from an indirect block */
2325 BUFFER_TRACE(partial->bh, "get_write_access");
2326 ext3_free_branches(handle, inode, partial->bh,
2327 partial->p,
2328 partial->p+1, (chain+n-1) - partial);
2329 }
2330 }
2331 /* Clear the ends of indirect blocks on the shared branch */
2332 while (partial > chain) {
2333 ext3_free_branches(handle, inode, partial->bh, partial->p + 1,
2334 (__le32*)partial->bh->b_data+addr_per_block,
2335 (chain+n-1) - partial);
2336 BUFFER_TRACE(partial->bh, "call brelse");
2337 brelse (partial->bh);
2338 partial--;
2339 }
2340 do_indirects:
2341 /* Kill the remaining (whole) subtrees */
2342 switch (offsets[0]) {
2343 default:
2344 nr = i_data[EXT3_IND_BLOCK];
2345 if (nr) {
2346 ext3_free_branches(handle, inode, NULL, &nr, &nr+1, 1);
2347 i_data[EXT3_IND_BLOCK] = 0;
2348 }
2349 case EXT3_IND_BLOCK:
2350 nr = i_data[EXT3_DIND_BLOCK];
2351 if (nr) {
2352 ext3_free_branches(handle, inode, NULL, &nr, &nr+1, 2);
2353 i_data[EXT3_DIND_BLOCK] = 0;
2354 }
2355 case EXT3_DIND_BLOCK:
2356 nr = i_data[EXT3_TIND_BLOCK];
2357 if (nr) {
2358 ext3_free_branches(handle, inode, NULL, &nr, &nr+1, 3);
2359 i_data[EXT3_TIND_BLOCK] = 0;
2360 }
2361 case EXT3_TIND_BLOCK:
2362 ;
2363 }
2364
2365 ext3_discard_reservation(inode);
2366
2367 mutex_unlock(&ei->truncate_mutex);
2368 inode->i_mtime = inode->i_ctime = CURRENT_TIME_SEC;
2369 ext3_mark_inode_dirty(handle, inode);
2370
2371 /*
2372 * In a multi-transaction truncate, we only make the final transaction
2373 * synchronous
2374 */
2375 if (IS_SYNC(inode))
2376 handle->h_sync = 1;
2377 out_stop:
2378 /*
2379 * If this was a simple ftruncate(), and the file will remain alive
2380 * then we need to clear up the orphan record which we created above.
2381 * However, if this was a real unlink then we were called by
2382 * ext3_delete_inode(), and we allow that function to clean up the
2383 * orphan info for us.
2384 */
2385 if (inode->i_nlink)
2386 ext3_orphan_del(handle, inode);
2387
2388 ext3_journal_stop(handle);
2389 }

清单 14 和 15 列出的 ext3_delete_inode 和 ext3_truncate 函数实现中,使用黑体标出了与前面提到的问题相关的部分代码。本文下载部分给出的针对 ext3 文件系统的补丁中,包括了对这些问题的一些修改。清单 16 给出了使用这个补丁之后在 ext3 文件系统中删除文件的一个例子。


清单16. 利用 debugfs 工具查看删除文件的信息
                
# ./creatfile.sh 90 testfile.90K

# ls -li
total 116
12 -rwxr-xr-x 1 root root 1407 2008-01-23 07:25 creatfile.sh
11 drwx------ 2 root root 16384 2008-01-23 07:23 lost+found
13 -rw-r--r-- 1 root root 92160 2008-01-23 07:25 testfile.90K

# rm -f testfile.90K
# cd ..
# umount /tmp/test

# debugfs /dev/sda3
debugfs 1.40.2 (12-Jul-2007)
debugfs: lsdel
Inode Owner Mode Size Blocks Time deleted
0 deleted inodes found.

debugfs: stat <13>
Inode: 13 Type: regular Mode: 0644 Flags: 0x0 Generation: 3438957668
User: 0 Group: 0 Size: 92160
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 192
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x47967b69 -- Wed Jan 23 07:25:29 2008
atime: 0x47967b68 -- Wed Jan 23 07:25:28 2008
mtime: 0x47967b69 -- Wed Jan 23 07:25:29 2008
BLOCKS:
(0-11):22528-22539, (IND):22540, (12-22):22541-22551
TOTAL: 24

debugfs:

在清单 16 中,我们首先创建了一个大小为 90KB 的测试文件,删除该文件并使用 debugfs 查看对应的设备文件时,stat <13> 显示的信息说明数据块的位置信息都仍然保存在了索引节点中,不过 lsdel 命令并未找到这个索引节点。因此下载部分中给出的补丁尚不完善,感兴趣的读者可以自行开发出更加完善的补丁。

 

 

下载:

createfile.sh

indirect_index.patch

read_dir_entry.c

e2undel-0.82.patch

 


 

 原文地址 http://www-128.ibm.com/developerworks/search/searchResults.jsp?searchType=1&searchSite=dWChina&pageL

你可能感兴趣的:(恢复被删除的文件-linux篇)