设为首页收藏本站

LinuxTone | 运维专家网论坛 - 最棒的Linux运维与开源架构技术交流社区!

 找回密码
 注册

QQ登录

只需一步,快速开始

#公告#抱歉,网站将关闭,不再开放。由于PC时代已远逝 。在这个后移动互联网时代,我们继续携手前行,保持对技术的热情。共同构建linuxtone知识星球欢迎加入,一起讨论技术、招聘人才、分享资源。请新老linuxtone人 扫码移步到 知识星球:linuxtone

学习是一种信仰!分享是一种快乐!能力= 心态 * 沟通 * 知识 (你的每一天需要正能量!)

 网站的发展需要你贡献一份力量!希望你能每天坚持看贴1小时,并回答网友的问题!祝你在浏览论坛的过程中取得进步!谢谢!linuxtone加油!大家加油! 友情提示: 你今天学习了吗?你今天进步了吗?少一点抱怨!多一点进步!Life is short ! Why not linuxtone ?  

网站的发展、感谢每位坛友的努力!

查看: 3493|回复: 0

如何恢复 Linux 上删除的文件 [复制链接]

Rank: 8Rank: 8

签到
1
注册时间
2008-5-26
最后登录
2014-4-6
在线时间
36 小时
阅读权限
150
积分
409
帖子
87
主题
33
精华
5
UID
7

LT粉丝

发表于 2008-7-2 09:13:27 |显示全部楼层
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, \
(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, \
(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 文件系统上正常使用了。

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

在块位图中将该文件所占用的数据块标识为可用状态。
在索引节点位图中将该文件所占用的索引节点标识为可用状态。
将该文件索引节点中的硬链接数目设置为 0。
清空间接索引节点中的数据.
清空 i_block 数组中各个成员中的数据。
将索引节点中的文件大小(i_size)和占用块数(i_blocks)设置为 0。
将该文件索引节点中的删除时间设置为当前时间。
将父目录项中该文件对应项中的索引节点号设置为 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 命令并未找到这个索引节点。因此下载部分中给出的补丁尚不完善,感兴趣的读者可以自行开发出更加完善的补丁。


小结

本文首先介绍了 ext3 文件系统作为一种日志文件系统的一些特性,然后针对这些特性介绍了 3 种恢复删除文件的方法:正文匹配、利用 libundel 和修改内核中 ext3 文件系统的实现,并给出了对 libundel 和 ext3 文件系统实现的补丁程序。应用本文介绍的方法,读者可以最大程度地恢复 ext3 文件系统中删除的文件。本系列的后续文章将继续探讨在其他文件系统上如何恢复已删除的文件,并着手设计更加通用的文件备份方法。

例:

用第一个方法,匹配分区中所有的文本来找回丢失的脚本内容。

急救措施第一步,当然是先umount掉你的分区,或者将其设置为readonly的,以免误删文件的block被覆盖,那就完蛋了。

mount -o remount,ro /dev/sda9
可以用fueser命令看当前目录有什么进程在用,然后kill掉它。

fuser -v -m /home
这个方法是使用grep或者strings命令对分区进行操作。

使用grep的话,可以用下面的命令:

grep -A 1 -B 1 -a -n "Your script content" /dev/sdb9 > sdb9.testfile
-A 和 -B分别是同时显示匹配行的下一行和上一行的内容,"Your script content"里面包含脚本的一些关键字,最好是同一分区下面其它文本文件没有的关键字啦。

不过,一般的脚本都会有几十行以上,不然直接自己写出来就好了,所以要显示脚本的全部内容的话,"-A 1"和"-B 1"是不够的。我试过,使用"-A 30"和"-B 30",结果是内存很快就用光,不能再分配。

那么还是使用"strings"命令好一点。"strings"命令的方式是将分区中所有文本文件的内容都print到一个文件中,如:

strings /dev/sda9 |gzip -c > /usr/sda9.gz
这样就将/dev/sda9中所有文本内容都print到/usr/sda9.gz中并且压缩了。我进行压缩的原因是sda9是我最大的分区,我怕直接print成一个文件的话,/usr分区会撑爆。如果要直接print成一个文件,那么:

strings /dev/sda9  > /usr/sda9.file
我的/dev/sda9分区总共60G左右,已用空间是8G,strings+gzip的时间是30分钟左右,压缩以后的文件是1.6G。而IBM那篇文章中的例子,扫描 20GB 的分区大概需要13分钟。

然后直接从导出的文件里面找你要的内容就可以了,我的方法是:

gunzip sda9.tgz -c - |grep -A 20 -B 20 -a "Your script content"
Yeah!我找回了自己的脚本

[ 本帖最后由 zzxia 于 2008-7-2 09:22 编辑 ]
您需要登录后才可以回帖 登录 | 注册

IT运维专家网感谢您的支持

合作联系: QQ:67888954/MSN:cnseek@msn.com/mail:netseek@linuxtone.org

Archiver|手机版|感谢所有关心和支持过LinuxTone的朋友们 转载本站内容请注明原作者名及出处 ( 京ICP备08103151 )   |

GMT+8, 2020-1-29 04:01 , Processed in 0.021722 second(s), 10 queries , Apc On.

Powered by Discuz! X2 Licensed

© 2001-2011 Comsenz Inc.

回顶部