如何恢复 Linux 上删除的文件–ext3

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

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 blocksJournal 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-129039TOTAL: 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 命令并未找到这个索引节点。因此下载部分中给出的补丁尚不完善,感兴趣的读者可以自行开发出更加完善的补丁。

 

 


小结

 

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

 


参考资料

 


下载

 

描述 名字 大小 下载方法
样例代码 e2undel-0.82.ext3.patch 17KB HTTP
样例代码 ext3.inode.c.patch 2KB HTTP

 

作者简介

冯锐,软件工程师,目前在 IBM 开发中心从事 AIX 性能测试方面的工作。您可以通过 fengrui@cn.ibm.com 与他联系。

 

沈林峰,软件工程师,目前在 IBM 开发中心从事 Linux on Power 方面的测试工作,您可以通过shenlinf@cn.ibm.com 与他联系。

 

出处:http://www.ibm.com/developerworks/cn/linux/l-cn-filesrc4/

 

发表评论