道底wwWwww.susu83.com63n如何解决,是否www.susu83.com63nCoM能再恢复

1,851被浏览1,155,113分享邀请回答icloud.com/activationlock/ 或百度:苹果官方激活锁查询,之后输入序列号,看看手机是不是处于非激活状态,这样你才可以挂上你的iCloud账号第6步:查看配件是否粗糙、磨损、机器成色等并不是太重要的问题。利益相关:被人骗买到日版有锁翻新机器的伤心人。。。2015.10更新:楼下有补充,硬件也可以改序列号的,那么请试一下是否可以正常从appstore下载app后打开使用。2016.6更新:有人问我怎么辨别6sp的翻新机,楼主我想说,造假技术日新月异,我实在是没法知晓。找靠谱渠道购买或熟人购买吧。实在不行多赚钱才是王道2017.4 评论区 检测app:没有及时看到攻略,本人已受骗。给楼主推荐一个好用的app:lirum device info lite 中文版需要18元购买:利落检测器。楼主可以亲测各种机型,立显原型,希望楼主发布到首页,防止更过的人受骗。601182 条评论分享收藏感谢收起17633 条评论分享收藏感谢收起&script type=&text/javascript&&&!--
google_ad_client = &pub-4985&;
/* 336x280, 文章内页 */
google_ad_slot = &&;
google_ad_width = 336;
google_ad_height = 280;
&script src=&http://pagead2.googlesyndication.com/pagead/show_ads.js& type=&text/javascript&&
要想恢复误删除的文件,必须清楚数据在磁盘上究竟是如何存储的,以及如何定位并恢复数据。本文从数据恢复的角度,着重介绍了 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. 文件数据定位过程
对于 ext2 类型的文件系统来说,目录项是使用一个名为 ext2_dir_entry_2 的结构来表示的,该结构定义如下所示:
清单 1. ext2_dir_entry_2 结构定义
struct ext2_dir_entry_2 {
/* 索引节点号 */
/* 目录项的长度 */
/* 文件名长度 */
/* 文件类型 */
name[EXT2_NAME_LEN];
/* 文件名 */
在 Unix/Linux 系统中,目录只是一种特殊的文件。目录和文件是通过 file_type 域来区分的,该值为 1 则表示是普通文件,该值为 2 则表示是目录。
对于每个 ext2 分区来说,其在物理磁盘上的布局如图 2 所示:
图 2. ext2 分区的布局
从图 2 中可以看到,对于 ext2 文件系统来说,磁盘被划分成一个个大小相同的数据块,每个块的大小可以是 或 4096 个字节。其中,第一个块称为引导块,一般保留做引导扇区使用,因此 ext2 文件系统一般都是从第二个块开始的。剩余的块被划分为一个个的块组,ext2 文件系统会试图尽量将相同文件的数据块都保存在同一个块组中,并且尽量保证文件在磁盘上的连续性,从而提高文件读写时的性能。
至于一个分区中到底有多少个块组,这取决于两个因素:
分区大小。
最终的计算公式如下:
分区中的块组数=分区大小/(块大小*8)
这是由于在每个块组中使用了一个数据块位图来标识数据块是否空闲,因此每个块组中最多可以有(块大小*8)个块;该值除上分区大小就是分区中总的块组数。
每个块组都包含以下内容:
超级块。存放文件系统超级块的一个拷贝。
组描述符。该块组的组描述符。
数据块位图。标识相应的数据块是否空闲。
索引节点位图。标识相应的索引节点是否空闲。
索引节点表。存放所有索引节点的数据。
数据块。该块组中用来保存实际数据的数据块。
在每个块组中都保存了超级块的一个拷贝,默认情况下,只有第一个块组中的超级块结构才会被系统内核使用;其他块组中的超级块可以在 e2fsck 之类的程序对磁盘上的文件系统进行一致性检查使用。在 ext2 文件系统中,超级块的结构会通过一个名为 ext2_super_block 的结构进行引用。该结构的一些重要域如下所示:
清单 2. ext2_super_block 结构定义
struct ext2_super_block {
/* 索引节点总数 */
/* 块数,即文件系统以块为单位的大小 */
s_r_blocks_
/* 系统预留的块数 */
s_free_blocks_
/* 空闲块数 */
s_free_inodes_
/* 空闲索引节点数 */
s_first_data_
/* 第一个可用数据块的块号 */
s_log_block_
/* 块大小 */
s_blocks_per_
/* 每个块组中的块数 */
s_inodes_per_
/* 每个块组中的索引节点个数 */
每个块组都有自己的组描述符,在 ext2 文件系统中是通过一个名为 ext2_group_desc的结构进行引用的。该结构的定义如下:
清单 3. ext2_group_desc 结构定义
* Structure of a blocks group descriptor
struct ext2_group_desc
/* 数据块位图的块号 */
/* 索引节点位图的块号 */
/* 第一个索引节点表的块号 */
bg_free_blocks_
/* 该组中空闲块数 */
bg_free_inodes_
/* 该组中空闲索引节点数 */
bg_used_dirs_
/* 该组中的目录项 */
bg_reserved[3];
数据块位图和索引节点位图分别占用一个块的大小,其每一位描述了对应数据块或索引节点是否空闲,如果该位为0,则表示空闲;如果该位为1,则表示已经使用。
索引节点表存放在一系列连续的数据块中,每个数据块中可以包括若干个索引节点。每个索引节点在 ext2 文件系统中都通过一个名为 ext2_inode 的结构进行引用,该结构大小固定为 128 个字节,其中一些重要的域如下所示:
清单 4. ext2_inode 结构定义
* Structure of an inode on the disk
struct ext2_inode {
/* 文件模式 */
/* 文件所有者的 uid */
/* 以字节为单位的文件长度 */
/* 最后一次访问该文件的时间 */
/* 索引节点最后改变的时间 */
/* 文件内容最后改变的时间 */
/* 文件删除的时间 */
/* 文件所有者的 gid */
/* 硬链接数 */
/* 文件的数据块数 */
i_block[EXT2_N_BLOCKS];/* 指向数据块的指针 */
第一个索引节点所在的块号保存在该块组描述符的 bg_inode_table 域中。请注意 i_block 域,其中就包含了保存数据的数据块的位置。有关如何对数据块进行寻址,请参看后文&数据块寻址方式&一节的内容。
需要知道的是,在普通的删除文件操作中,操作系统并不会逐一清空保存该文件的数据块的内容,而只会释放该文件所占用的索引节点和数据块,方法是将索引节点位图和数据块位图中的相应标识位设置为空闲状态。因此,如果我们可以找到文件对应的索引节点,由此查到相应的数据块,就可能从磁盘上将已经删除的文件恢复出来。
幸运的是,这一切都是可能的!本文将通过几个实验来了解一下如何从磁盘上恢复删除的文件。
数据块寻址方式
回想一下,ext2_inode 结构的 i_block 域是一个大小为 EXT2_N_BLOCKS 的数组,其中保存的就是真正存放文件数据的数据块的位置。通常来说,EXT2_N_BLOCKS 大小为 15。在 ext2 文件系统,采用了直接寻址和间接寻址两种方式来对数据块进行寻址,原理如图3 所示:
图 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 文件系统可以支持和4096字节三种大小的块,对应的寻址能力如下表所示:
表 1. 各种数据块对应的文件寻址范围
二次间接寻址
三次间接寻址
掌握上面介绍的知识之后,我们就可以开始恢复文件的实验了。
准备文件系统
为了防止破坏已有系统,本文将采用一个新的分区进行恢复删除文件的实验。
首先让我们准备好一个新的分区,并在上面创建 ext2 格式的文件系统。下面的命令可以帮助创建一个 20GB 的分区:
清单 5. 新建磁盘分区
# fdisk /dev/sdb && 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:
Last mounted on:
&not available&
Filesystem UUID:
d8b10aa9-c065-4aa5-ab6f-96a9bcda52ce
Filesystem magic number:
Filesystem revision #:
1 (dynamic)
Filesystem features:
ext_attr resize_inode dir_index filetype sparse_super large_file
Default mount options:
Filesystem state:
Errors behavior:
Filesystem OS type:
Inode count:
Block count:
Reserved block count:
Free blocks:
Free inodes:
First block:
Block size:
Fragment size:
Reserved GDT blocks:
Blocks per group:
Fragments per group:
Inodes per group:
Inode blocks per group:
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:
Maximum mount count:
Last checked:
Mon Oct 29 20:04:16 2007
Check interval:
Next check after:
Sat Apr 26 20:04:16 2008
Reserved blocks uid:
0 (user root)
Reserved blocks gid:
0 (group root)
First inode:
Inode size:
Default directory hash:
Directory Hash Seed:
ddef-a26fef9d5e8
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
31224 free blocks, 16276 free inodes, 2 directories
Free blocks: ,
Free inodes: 12, 14-16288
Group 149: (Blocks 5759)
Block bitmap at 4882432 (+0), Inode bitmap at 4882433 (+1)
Inode table at 2942 (+2)
2817 free blocks, 16288 free inodes, 0 directories
Free blocks: 5759
Free inodes: 3200
应用前面介绍的一些知识,我们可以看到,这个文件系统中,块大小(Block size)为4096字节,因此每个块组中的块数应该是68个(Blocks per group),每个块组的大小是 128MB,整个分区被划分成20GB/(4KB*3个。但是为什么我们只看到 150 个块组(0到149)呢?实际上,在 fdisk 中,我们虽然输入要创建的分区大小为 20GB,但实际上,真正分配的空间并不是严格的20GB,而是只有大约 20*109 个字节,准确地说,应该是 (4885760 * 4096) / (24) = 18.64GB。这是由于不同程序的计数单位的不同造成的,在使用存储设备时经常遇到这种问题。因此,这个分区被划分成 150 个块组,前 149 个块组分别包含 32768 个块(即 128B),最后一个块组只包含 3328 个块。
另外,我们还可以看出,每个索引节点的大小是 128 字节,每个块组中包含 16288 个索引节点,在磁盘上使用 509 个块来存储(96),在第一个块组中,索引节点表保存在 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
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
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)
Type: regular
Flags: 0x0
Generation:
Size: 35840
File ACL: 0
Directory ACL: 0
Blockcount: 72
Address: 0
ctime: 0x -- Mon Oct 29 20:09:59 2007
atime: 0x4726849d -- Mon Oct 29 20:10:53 2007
mtime: 0x -- Mon Oct 29 20:09:59 2007
输出结果显示的就是索引节点 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)
Type: regular
Mode: 0644
Flags: 0x0
Generation:
File ACL: 0
Directory ACL: 0
Blockcount: 20512
Address: 0
ctime: 0x -- Mon Oct 29 20:10:29 2007
atime: 0x -- Mon Oct 29 20:11:01 2007
mtime: 0x -- Mon Oct 29 20:10:29 2007
(0-11):, (IND):24588, (12--25612, (DIND):25613, (IND):25614,
():, (IND):26639, ():
TOTAL: 2564
和索引节点 13 相比,二者之间最重要的区别在于 BLOCKS 的数据,testfile.10M 在磁盘上总共占用了 2564 个数据块,由于需要采用二级间接寻址模式进行访问,所以使用了4个块来存放间接寻址的信息,分别是2、2,其中25613块中存放的是二级间接寻址的信息。
恢复删除文件
现在将刚才创建的两个文件删除:
清单14. 删除测试文件
# rm -f testfile.35K testfile.10M
debugfs 的 lsdel 命令可以查看文件系统中删除的索引节点的信息:
清单15. 使用 lsdel 命令搜索已删除的文件
# echo &lsdel& | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
Time deleted
Mon Oct 29 20:32:05 2007
0 /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)
Type: regular
Flags: 0x0
Generation:
File ACL: 0
Directory ACL: 0
Blockcount: 20512
Address: 0
ctime: 0x -- Mon Oct 29 20:32:05 2007
atime: 0x -- Mon Oct 29 20:11:01 2007
mtime: 0x -- Mon Oct 29 20:10:29 2007
dtime: 0x -- Mon Oct 29 20:32:05 2007
(0-11):, (IND):24588, (DIND):25613
与前面的结果比较一下不难发现,BLOCKS后面的数据说明总块数为 14,而且也没有整个文件所占据的数据块的详细说明了。既然文件的数据全部都没有发生变化,那么间接寻址所使用的那些索引数据块会不会有问题呢?现在我们来查看一下 24588 这个间接索引块中的内容:
清单 20. 查看间接索引块 24588 中的内容
# dd if=/dev/sdb6 of=block. 24588 bs=4096 count=1 skip=24588
# hexdump block. 24588
0 00 00 0000
显然,这个数据块的内容被全部清零了。debugfs 的dump 命令按照原来的寻址方式试图恢复文件时,所访问到的实际上都是第0 个数据块(引导块)中的内容。这个分区不是可引导分区,因此这个数据块中没有写入任何数据,因此 dump 恢复出来的数据只有前48K是正确的,其后所有的数据全部为0。
实际上,ext2 是一种非常优秀的文件系统,在磁盘空间足够的情况下,它总是试图将数据写入到磁盘上的连续数据块中,因此我们可以假定数据是连续存放的,跳过间接索引所占据的 2、2,将从24576 开始的其余 2500 个数据块读出,就能将整个文件完整地恢复出来。但是在磁盘空间有限的情况下,这种假设并不成立,如果系统中磁盘碎片较多,或者同一个块组中已经没有足够大的空间来保存整个文件,那么文件势必会被保存到一些不连续的数据块中,此时上面的方法就无法正常工作了。
反之,如果在删除文件的时候能够将间接寻址使用的索引数据块中的信息保存下来,那么不管文件在磁盘上是否连续,就都可以将文件完整地恢复出来了,但是这样就需要修改 ext2 文件系统的实现了。在 ext2 的实现中,与之有关的有两个函数:ext2_free_data 和 ext2_free_branches(都在 fs/ext2/inode.c 中)。2.6 版本内核中这两个函数的实现如下:
清单 21. 内核中 ext2_free_data 和 ext2_free_branches 函数的实现
ext2_free_data - free a list of data blocks
@inode: inode we are dealing with
array of block numbers
points immediately past the end of array
We are freeing all blocks refered from that array (numbers are
stored as little-endian 32-bit) and updating @inode-&i_blocks
appropriately.
824 static inline void ext2_free_data(struct inode *inode, __le32 *p, __le32 *q)
unsigned long block_to_free = 0, count = 0;
for ( ; p & p++) {
nr = le32_to_cpu(*p);
/* accumulate blocks to free if they're contiguous */
if (count == 0)
goto free_
else if (block_to_free == nr - count)
mark_inode_dirty(inode);
ext2_free_blocks (inode, block_to_free, count);
free_this:
block_to_free =
count = 1;
if (count & 0) {
mark_inode_dirty(inode);
ext2_free_blocks (inode, block_to_free, count);
ext2_free_branches - free an array of branches
@inode: inode we are dealing with
array of block numbers
pointer immediately past the end of array
@depth: depth of the branches to free
We are freeing all blocks refered from these branches (numbers are
stored as little-endian 32-bit) and updating @inode-&i_blocks
appropriately.
864 static void ext2_free_branches(struct inode *inode, __le32 *p, __le32 *q, int depth)
struct buffer_head *
if (depth--) {
int addr_per_block = EXT2_ADDR_PER_BLOCK(inode-&i_sb);
for ( ; p & p++) {
nr = le32_to_cpu(*p);
bh = sb_bread(inode-&i_sb, nr);
* A read failure? Report error and clear slot
* (should be rare).
if (!bh) {
ext2_error(inode-&i_sb, &ext2_free_branches&,
&Read failure, inode=%ld, block=%ld&,
inode-&i_ino, nr);
ext2_free_branches(inode,
(__le32*)bh-&b_data,
(__le32*)bh-&b_data + addr_per_block,
bforget(bh);
ext2_free_blocks(inode, nr, 1);
mark_inode_dirty(inode);
ext2_free_data(inode, p, q);
注意第 832 和 875 这两行就是用来将对应的索引项置为 0 的。将这两行代码注释掉(对于最新版本的内核 2.6.23 可以下载本文给的补丁)并重新编译 ext2 模块,然后重新加载新编译出来的模块,并重复上面的实验,就会发现利用 debugfs 的 dump 命令又可以完美地恢复出整个文件来了。
显然,这个补丁并不完善,因为这个补丁中的处理只是保留了索引数据块中的索引节点数据,但是还没有考虑数据块位图的处理,如果对应的数据块没有设置为正在使用的状态,并且刚好这些数据块被重用了,其中的索引节点数据就有可能会被覆盖掉了,这样就彻底没有办法再恢复文件了。感兴趣的读者可以沿用这个思路自行开发一个比较完善的补丁。
本文介绍了 ext2 文件系统中的一些基本概念和重要数据结构,并通过几个实例介绍如何恢复已经删除的文件,最后通过修改内核中 ext2 文件系统的实现,解决了大文件无法正常恢复的问题。本系列的下一篇文章中,将介绍如何恢复 ext2 文件系统中的一些特殊文件,以及如何恢复整个目录等方面的问题。
除了普通文件之外,UNIX/Linux 中还存在一些特殊的文件,包括目录、字符设备、块设备、命名管道、socket 以及链接;另外还存在一些带有文件洞的文件,这些特殊文件的恢复是和其存储机制紧密联系在一起的,本文将从这些特殊文件的存储原理和机制入手,逐步介绍这些特殊文件的恢复方法。
在本系列文章的第一部分中,我们介绍了 ext2 文件系统中的一些基本概念和重要数据结构,并通过几个实例学习了如何恢复已经删除的文件,最后通过修改 2.6 版本内核中 ext2 文件系统的实现,解决了大文件无法正常恢复的问题。
通过第一部分的介绍,我们已经知道如何恢复系统中删除的普通文件了,但是系统中还存在一些特殊的文件,比如我们熟悉的符号链接等。回想一下在本系列文章的第一部分中,目录项是使用一个名为 ext2_dir_entry_2 的结构来表示的,该结构定义如下:
清单1. ext2_dir_entry_2 结构定义
struct ext2_dir_entry_2 {
/* 索引节点号 */
/* 目录项的长度 */
/* 文件名长度 */
/* 文件类型 */
name[EXT2_NAME_LEN];
/* 文件名 */
其中 file_type 域就标识了每个文件的类型。ext2 文件系统中支持的文件类型定义如下表所示:
表 1. ext2 文件系统中支持的文件类型
EXT2_FT_REG_FILE
EXT2_FT_DIR
EXT2_FT_CHRDEV
EXT2_FT_BLKDEV
EXT2_FT_FIFO
EXT2_FT_SOCK
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
0 00 00 0000
第一个命令生成的 /tmp/test/hole 文件大小是 7169 字节,其前 7168 字节都为空,第 7169 字节的内容是字母 X。正常来讲,7169 字节的文件需要占用两个数据块来存储,第一个数据块全部为空,第二个数据块的第 3073 字节为字母 X,其余字节都为空。显然,第一个数据块就是一个文件洞,在这个数据块真正被写入数据之前,ext2 并不为其实际分配数据块,而是将 i_block 域的对应位(或间接寻址使用的索引数据块中的对应位)设置为0,表示这是一个文件洞。该文件的内容如下图所示:
图1. /tmp/test/hole 文件的存储方法
file_hole.jpg
现在我们可以使用 debugfs 来查看一下这个文件的详细信息:
清单3. 带有文件洞的文件的 inode 信息
# echo &stat &15&& | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
Type: regular
Flags: 0x0
Generation:
Size: 7169
File ACL: 1544
Directory ACL: 0
Blockcount: 16
Address: 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
从输出结果中我们可以看出,这个文件的大小是 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
0 00 00 0000
000e400 0059
与上一个文件类似,这个文件的数据也只有一个字符,是 0x000e400(即第58369字节)为字符&Y&。我们真正关心的是这个文件的数据存储情况:
清单6. 使用间接寻址方式的带有文件洞的文件的 inode 信息
# echo &stat &17&& | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
Type: regular
Flags: 0x0
Generation:
Size: 58369
File ACL: 1544
Directory ACL: 0
Blockcount: 24
Address: 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
(IND):24576, (14):24577
从结果中可以看出,该文件占用了两个数据块来存储数据,一个是间接寻址使用的索引块 24576,一个是真正存放数据的数据块24577。下面让我们来查看一下 24576 这个数据块中的内容:
清单7. 索引数据块中存储的数据
# dd if=/dev/sdb6 of=/tmp/recover/block.24576 bs=4096 count=1 skip=24576
# hexdump block.24576
0 00 00 0000
0 00 00 0000
正如预期的一样,其中只有第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)
4096 28-Nov- .
4096 28-Nov- ..
16384 28-Nov- lost+found
1406 28-Nov- createfile.sh
35840 28-Nov- testfile.35K
-Nov- testfile.10M
4096 28-Nov- dir1
35840 28-Nov- testfile.35K.orig
-Nov- testfile.10M.orig
ls -l dir1
4096 28-Nov- .
4096 28-Nov- ..
1406 28-Nov- createfile.sh
4096 28-Nov- subdir11
4096 28-Nov- subdir12
35840 28-Nov- testfile.35K
-Nov- testfile.10M
从输出结果中可以看出,每个目录结构中至少要包含两项:当前目录(.)和父目录(..)的信息。在一个文件系统中,会有一些特殊的索引节点是保留的,用户创建的文件无法使用这些索引节点。2 就是这样一个特殊的索引节点,表示根目录。结合上面的输出结果,当前目录(.)和父目录(..)对应的索引节点号都是2,表示这是该分区的根目录。特别地,在使用 mke2fs 命令创建一个 ext2 类型的文件系统时,会自动创建一个名为 lost+found 的目录,并为其预留 4 个数据块的大小(16KB),其用途稍后就会介绍。
我们在根目录下面创建了几个文件和一个名为 dir1(索引节点号为 32577)的目录,并在 dir1 中又创建了两个子目录(subdir1 和 subdir2)和几个文件。
要想完美地恢复目录,必须了解清楚在删除目录时究竟对系统进行了哪些操作,其中哪些是可以恢复的,哪些是无法恢复的,这样才能寻找适当的方式去尝试恢复数据。现在先让我们记录下这个文件系统目前的一些状态:
清单10. 根目录(索引节点 &2&)子目录 dir1的 inode 信息
Type: directory
Flags: 0x0
Generation: 0
Size: 4096
File ACL: 0
Directory ACL: 0
Blockcount: 8
Address: 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
stat &32577&
Inode: 32577
Type: directory
Flags: 0x0
Generation:
Size: 4096
File ACL: 1542
Directory ACL: 0
Blockcount:
Address: 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
以及根目录和 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 {
/* Inode number */
/* Directory entry length */
/* Name length */
void usage()
printf(&read_dir_entry [dir entry filename] [dir entry size]/n&);
int main(int argc, char **argv)
struct ext2_dir_entry_2
char *filename = NULL;
FILE *fp = NULL;
int rtn = 0;
int length = 0;
int de_size = 0;
if (argc & 3)
printf(&Too few parameters!/n&);
filename = argv[1];
= atoi(argv[2]);
fp = fopen(filename, &r&);
printf(&cannot open file: %s/n&, filename);
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);
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);
|-------- XML error:
The previous line is longer than the max of 90 characters ---------|
length += dep.rec_
if (length &= de_size - sizeof(struct ext2_dir_entry_part))
fclose(fp);
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
=================================================================
lost+found
createfile.sh
testfile.35K
testfile.10M
testfile.35K.orig
testfile.10M.orig
# ./read_dir_entry block.88064.orig 4096
offset | inode number | rec_len | name_len | file_type | name
=================================================================
createfile.sh
testfile.35K
testfile.10M
这与上面在 debugfs 中使用 ls 命令看到的结果是完全吻合的。
现在删除 dir1 这个目录,然后卸载测试目录(这是为了确保删除文件操作会被同步到磁盘上的数据块中),然后重新读取我们关注的这两个数据块的内容:
清单15. 删除目录并重新备份目录项数据
# rm &rf /tmp/test/dir1
# 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
=================================================================
lost+found
createfile.sh
testfile.35K
testfile.10M
testfile.35K.orig
testfile.10M.orig
# ./read_dir_entry block.88064.deleted 4096
offset | inode number | rec_len | name_len | file_type | name
=================================================================
createfile.sh
testfile.35K
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)
Type: directory
Flags: 0x0
Generation: 0
Size: 4096
File ACL: 0
Directory ACL: 0
Blockcount: 8
Address: 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
stat &32577&
Inode: 32577
Type: directory
Flags: 0x0
Generation:
File ACL: 1542
Directory ACL: 0
Blockcount: 16
Address: 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
与删除之前的结果进行一下比较就会发现,主要区别包括:
将父目录的 Links 值减 1。
设置 dtime 时间,并更新其他时间字段。
由于目录只有在为空时才会被删除,因此其 Size 值会被设置为 0,Links 字段也被设置为 0。
通过了解数据块和索引节点的相应变化可以为恢复目录提供一个清晰的思路,其具体步骤如下:
确定删除目录所对应的索引节点号。
按照恢复文件的方法恢复索引节点对应的数据块。
遍历数据块内容,恢复其中包含的文件和子目录。
更新索引节点对应信息。
修改父目录的索引节点信息和数据块中对应目录项的内容。
实际上,步骤3并不是必须的,因为如果这个目录中包含文件或子目录,使用 debugfs 的 lsdel 命令(遍历索引节点表)也可以找到所删除的索引节点记录,采用本文中介绍的方法也可以将其逐一恢复出来。
debugfs 的 mi 命令可以用来直接修改索引节点的信息,下面我们就使用这个命令来修改 dir1 这个目录对应的索引节点的信息:
清单18. 使用 debugfs 的 mi 命令直接修改索引节点信息
# debugfs -w /dev/sdb6
debugfs 1.39 (29-May-2006)
Time deleted
1 Wed Nov 28 17:23:19 2007
1 Wed Nov 28 17:23:19 2007
1 Wed Nov 28 17:23:19 2007
9 Wed Nov 28 17:23:19 2007
0 /2564 Wed Nov 28 17:23:19 2007
1 Wed Nov 28 17:23:19 2007
6 deleted inodes found.
mi &32577&
Creation time
Modification time
Access time
Deletion time
Link count
Block count
File flags
Generation
[0x650bae5e]
Directory acl
Fragment address
Fragment number
Fragment size
Direct Block #0
Direct Block #1
Direct Block #2
Direct Block #3
Direct Block #4
Direct Block #5
Direct Block #6
Direct Block #7
Direct Block #8
Direct Block #9
Direct Block #10
Direct Block #11
Indirect Block
Double Indirect Block
Triple Indirect Block
link &32577& dir1
注意要使用 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
Nov 28 16:54 testfile.10M
16 -rw-r--r-- 1 root root
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
??--------- ? ? ? ?
? /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 个步骤的检查:
检查并修复索引节点、数据块和大小。比如已删除子目录的索引节点大小为0,则会根据所占用的块数(每个块为512字节)换算出来。
检查目录结构的问题。检查索引节点的父目录,如果不存在,就认为父目录就是根目录。对于目录节点,需要检查是否包含当前目录和父目录项。
检查目录结构的连通性。防止出现按照绝对路径无法访问文件的情况出现,将这些有问题的文件或目录放入 lost+found 目录中。
检查并修复引用计数。统计对索引节点的引用计数值。
检查并修复块组信息,包括块位图、索引节点位图,计算块组中的空闲块数、空闲索引节点数等。
现在重新挂载这个文件系统,会发现所有的文件已经全部恢复出来了。
我们知道,在 ext2 文件系统中,链接可以分为两种:硬链接和符号链接(或称为软链接)。实际上,目录中的每个文件名都对应一个硬链接。硬链接的出现是为了解决使用不同的文件名来引用同一个文件的问题。如果没有硬链接,只能通过给现有文件新建一份拷贝才能通过另外一个名字来引用这个文件,这样做的问题是在文件内容发生变化的情况下,势必会造成引用这些文件的进程所访问到的数据不一致的情况出现。而虽然每个硬链接在文件目录项中都是作为一个单独的项存在的,但是其索引节点号完全相同,这就是说它们引用的是同一个索引节点,因此对应的文件数据也完全相同。下面让我们通过一个例子来验证一下:
清单21.硬链接采用相同的索引节点号
# ln testfile.10M hardlink.10M
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
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
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
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
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
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)
Type: symlink
Flags: 0x0
Generation:
File ACL: 1542
Directory ACL: 0
Blockcount: 8
Address: 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 abcdwfghijklmnopqrstuvwxyzabcdwfghijklmnopqrstuvwxyz.sh
# ln -s abcdwfghijklmnopqrstuvwxyzabcdwfghijklmnopqrstuvwxyz.sh /
longsoftlink.sh
total 20608
16 -rw-r--r-- 1 root root
0 Nov 29 19:52 /
abcdwfghijklmnopqrstuvwxyzabcdwfghijklmnopqrstuvwxyz.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
Nov 29 19:21 hardlink.10M
17 lrwxrwxrwx 1 root root
75 Nov 29 19:53 longsoftlink.sh -& /
abcdwfghijklmnopqrstuvwxyzabcdwfghijklmnopqrstuvwxyz.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
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)
Type: symlink
Flags: 0x0
Generation:
File ACL: 1542
Directory ACL: 0
Blockcount: 16
Address: 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
此处我们创建了一个名字长度为 75 个字符的文件,并建立一个符号链接(其索引节点号是 17)指向这个文件。由于链接指向的位置路径名超过了 60 个字符,因此还需要使用一个数据块(6144)来存储这个路径名。手工恢复方法如下:
清单24. 恢复长名符号链接
# dd if=/dev/sdb6 of=longsoftlink.6144 bs=4096 count=1 skip=6144
# xxd longsoftlink.6144 | more
62 68 696a 6b6c 6d6e 6f70
abcdwfghijklmnop
72 78 797a 35
qrstuvwxyz012345
37 64 6a 6b6c
6789abcdwfghijkl
d6e 6f70 76 31
mnopqrstuvwxyz01
33 39 2e73 00
00 00 00 0000
................
这样符号链接的数据就可以完整地恢复出来了。
需要注意的是,为了保证整个文件系统的完整性,在恢复硬链接时,还需要修改链接指向的索引节点的引用计数值,这可以使用 e2fsck 帮助完成,详细步骤请参看上一节目录的恢复。
本文介绍了 ext2 文件系统中比较特殊的一些文件的存储和恢复机制,包括文件洞、目录和链接,并介绍了如何结合使用 debugfs 和 e2fsck 等工具完整恢复 ext2 文件系统的方法。在本系列的后续文章中,我们将介绍几个可以自动恢复 ext2 文件系统中已删除文件的工具,以及对 ext2 文件系统的后继者 ext3 和 ext4 文件系统的一些考虑。
恢复系统中删除的文件是一个非常繁琐的过程,而 e2undel 这个工具可以用来方便地恢复文件系统中已删除的文件。本文将首先讨论 e2undel 的工作原理和用法,并对之进行一些改进。然后讨论了文件系统故障、文件系统重建、磁盘物理损坏等情况下应该如何恢复数据。
在本系列文章的前两部分中,我们介绍了 ext2 文件系统中各种文件在磁盘上的存储结构,以及如何利用 debugfs 工具的辅助,手工恢复这些文件的详细过程。
通过这两部分的学习,我们可以看出恢复系统中删除的文件是一个非常繁琐的过程,需要非常仔细地考虑各种情况,并且要保持足够的细心,才可能把数据准确无误地恢复出来。稍有差错,就会造成数据丢失的情况。聪明的读者肯定会想,如果有一些好工具来自动或辅助完成数据的恢复过程,那简直就太好了。
幸运的是,已经有人开发了这样一些工具,来简化用户的数据恢复工作,e2undel 就是其中功能最为强大的一个。
自动恢复工具 e2undel
回想一下,在 ext2 文件系统中删除一个文件时,该文件本身的数据并没有被真正删除,实际执行的操作如下:
在块位图中将该文件所占用的数据块标识为可用状态。
在索引节点位图中将该文件所占用的索引节点标识为可用状态。
将该文件索引节点中的硬链接数目设置为 0。
将该文件索引节点中的删除时间设置为当前时间。
将父目录项中该文件对应项中的索引节点号设置为 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
-------------+---------+---------+---------+---------+---------+--------
Select user name from table or press enter to exit: root
Select time interval (1 to 6) or press enter to exit: 4
deleted at
-----------------------------------------------------------
Dec 19 17:43 2007 * ASCII text
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
-------------+---------+---------+---------+---------+---------+--------
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&);
fprintf(stderr, &libundel: can't open log file, undeletion disabled/n&);
int rmdir(const char *pathname)
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);
/* remove() did not succeed */
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) */
/* 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
-------------+---------+---------+---------+---------+---------+--------
Select user name from table or press enter to exit: phost
Select time interval (1 to 6) or press enter to exit: 1
deleted at
-----------------------------------------------------------
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/BUGS
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/HISTORY
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/INSTALL
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/INSTALL.de
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/Makefile
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/README
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/README.de
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/apprentice.c
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/ascmagic.c
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/common.h
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/compactlog.c
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/e2undel.c
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/e2undel.h
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/file.c
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/file.h
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/find_del.c
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/is_tar.c
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/libundel.c
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/log.c
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/magic.h
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/names.h
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/softmagic.c
Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/tar.h
如果对所有用户都打开这个功能,由于日志文件是单向增长的,随着时间的推移,可能会变得很大,不过 e2undel 中还提供了一个 compactlog 工具来删除日志文件中的重复项。
在学习本系列文章介绍的技术之后,利用 e2undel 之类的工具,并使用本系列文章第一部分中提供的补丁,恢复删除文件就变得非常简单了。但是在日常使用过程中,大家可能还会碰到一些意外的情况,比如文件系统发生问题,从而无法正常挂载;使用 mke2fs 之类的工具重做了文件系统;甚至磁盘上出现坏道。此时应该如何恢复系统中的文件呢,下面让我们来逐一看一下如何解决这些问题。
文件系统故障的恢复
回想一下,在超级块中保存了有关文件系统本身的一些数据,在 ext2 文件系统中,还使用块组描述符保存了有关块组的信息;另外,索引节点位图、块位图中分别保存了索引节点和磁盘上数据块的使用情况,而文件本身的索引节点信息(即文件的元数据)则保存在索引节点表中。这些数据对于文件系统来说都是至关重要的,它们是存取文件的基础。如果超级块和块组描述符的信息一旦出错,则会造成文件系统无法正常挂载的情况出现。造成这些信息出错的原因有:
系统管理员操作失误。
设备驱动程序或第三方软件(例如mke2fs之类的)有 bug。
电源意外断电。
内核有 bug。
如果出现这种问题,可能造成的后果有:
文件系统无法挂载。
操作系统挂起。
即使文件系统能够成功挂载,在系统重启时也可能会看到一些错误,或者目录列表中出现乱字符的情况等。
下面让我们来模拟一个出现这种错误的情况。我们知道,超级块信息就保存在分区中的第一个块中,现在我们来试验一下清空这个块中数据的后果:
清单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
由于无法从磁盘上读取到有效的超级块信息,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
Backup superblock at 98304, Group descriptors at
Backup superblock at 163840, Group descriptors at 842
Backup superblock at 229376, Group descriptors at 378
Backup superblock at 294912, Group descriptors at 914
Backup superblock at 819200, Group descriptors at 202
Backup superblock at 884736, Group descriptors at 738
Backup superblock at 1605632, Group descriptors at 5634
Backup superblock at 2654208, Group descriptors at 4210
Backup superblock at 4096000, Group descriptors at 6002
这是一个 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 的值应该是 072。
尽管 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),
# 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)
4096 31-Dec- .
4096 31-Dec- ..
16384 31-Dec- lost+found
-Dec- testfile.10M
35840 31-Dec- 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 的日志文件等,都非常重要,值得及时备份。
本文介绍了一个功能非常强大的工具 e2undel,可以用来方便地恢复已删除的文件。然后讨论了文件系统故障、文件系统重建、磁盘物理损坏等情况下应该如何恢复数据。随着文件系统的不断发展,Linux 上常用的文件系统也越来越多,例如 ext3/ext4/reiserfs/jfs 等,这些文件系统上删除的文件能否成功恢复呢?有哪些工具可以用来辅助恢复文件呢?本系列后续文章将继续探讨这个问题。
作为 ext2 文件系统的后继者,ext3 文件系统由于日志的存在,使其可用性大大增加。尽管 ext3 文件系统可以完全兼容 ext2 文件系统,但是由于关键的一点区别却使得在 ext3 上恢复删除文件变得异常困难。本文将逐渐探讨其中的原因,并给出了三种解决方案:正文匹配,元数据备份,以及修改 ext3 的实现。
本系列文章的前 3 部分详细介绍了 ext2 文件系统中文件的数据存储格式,并讨论了各种情况下数据恢复的问题。作为 ext2 文件系统的后继者,ext3 文件系统可以实现与 ext2 文件系统近乎完美的兼容。但是前文中介绍的各种技术在 ext3 文件系统中是否同样适用呢?ext3 文件系统中删除文件的恢复又有哪些特殊之处呢?本文将逐一探讨这些问题。
ext3:日志文件系统
由于具有很好的文件存取性能,ext2 文件系统自从 1993 年发布之后,已经迅速得到了用户的青睐,成为很多 Linux 发行版中缺省的文件系统,原因之一在于 ext2 文件系统采用了文件系统缓存的概念,可以加速文件的读写速度。然而,如果文件系统缓存中的数据尚未写入磁盘,机器就发生了掉电等意外状况,就会造成磁盘数据不一致的状态,这会损坏磁盘数据的完整性(文件数据与元数据不一致),严重情况下甚至会造成文件系统的崩溃。
为了确保数据的完整性,在系统引导时,会自动检查文件系统上次是否是正常卸载的。如果是非正常卸载,或者已经使用到一定的次数,就会自动运行 fsck 之类的程序强制进行一致性检查(具体例子请参看本系列文章的第 2 部分),并修复存在问题的地方,使 ext2 文件系统恢复到新的一致状态。
然而,随着硬盘技术的发展,磁盘容量变得越来越大,对磁盘进行一致性检查可能会占用很长时间,这对于一些关键应用来说是不可忍受的;于是日志文件系统(Journal File System)的概念也就应运而生了。
所谓日志文件系统,就是在文件系统中借用了数据库中&事务&(transaction)的概念,将文件的更新操作变成原子操作。具体来说,就是在修改文件系统内容的同时(}

我要回帖

更多关于 susu 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信