除了普通文件之外,UNIX/Linux 中还存在一些特殊的文件,包括目录、字符设备、块设备、命名管道、socket 以及链接;另外还存在一些带有文件洞的文件,这些特殊文件的恢复是和其存储机制紧密联系在一起的,本文将从这些特殊文件的存储原理和机制入手,逐步介绍这些特殊文件的恢复方法。
在本系列文章的第一部分中,我们介绍了 ext2 文件系统中的一些基本概念和重要数据结构,并通过几个实例学习了如何恢复已经删除的文件,最后通过修改 2.6 版本内核中 ext2 文件系统的实现,解决了大文件无法正常恢复的问题。

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


清单1. ext2_dir_entry_2 结构定义

struct ext2_dir_entry_2 { 

 __le32 inode; /* 索引节点号 */ 

 __le16 rec_len; /* 目录项的长度 */ 

 __u8 name_len; /* 文件名长度 */ 

 __u8 file_type; /* 文件类型 */ 

 char name[EXT2_NAME_LEN]; /* 文件名 */ 

};



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


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

文件洞

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

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

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

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

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


清单2. 创建带有文件洞的文件

# echo -n "X" | dd of=/tmp/test/hole bs=1024 seek=7 


# ls -li /tmp/test/hole 

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


# hexdump /tmp/test/hole 

0000000 0000 0000 0000 0000 0000 0000 0000 0000 

* 

0001c00 0058 

0001c01



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


图1. /tmp/test/hole 文件的存储方法


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


清单3. 带有文件洞的文件的 inode 信息

# echo "stat <15>" | debugfs /dev/sdb6 

debugfs 1.39 (29-May-2006) 

debugfs: Inode: 15 Type: regular Mode: 0644 Flags: 0x0 Generation: 4118330634 

User: 0 Group: 0 Size: 7169 

File ACL: 1544 Directory ACL: 0 

Links: 1 Blockcount: 16 

Fragment: Address: 0 Number: 0 Size: 0 

ctime: 0x474a379c -- Mon Nov 26 11:03:56 2007 

atime: 0x474a379c -- Mon Nov 26 11:03:56 2007 

mtime: 0x474a379c -- Mon Nov 26 11:03:56 2007 

BLOCKS: 

 (1):20480 

 TOTAL: 1




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


清单4. 使用 dd 命令手工恢复带有文件洞的文件

# dd if=/dev/zero of=/tmp/recover/hole.part1 bs=4096 count=1 

# dd if=/dev/sdb6 of=/tmp/recover/hole.part2 bs=4096 count=1 skip=20480 


# cat /tmp/recover/hole.part1 /tmp/recover/hole.part2 > /tmp/recover/hole.full 

# split -d -b 7169 hole.full hole 

# mv hole00 hole 


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



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

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


清单5. 创建 57K 大小的带有文件洞的文件

# echo -n "Y" | dd of=/tmp/test/hole.57K bs=1024 seek=57 


# ls -li /tmp/test/hole.57K 

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


# hexdump /tmp/test/hole.57K 

0000000 0000 0000 0000 0000 0000 0000 0000 0000 

* 

000e400 0059 

000e401



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


清单6. 使用间接寻址方式的带有文件洞的文件的 inode 信息

# echo "stat <17>" | debugfs /dev/sdb6 

debugfs 1.39 (29-May-2006) 

debugfs: Inode: 17 Type: regular Mode: 0644 Flags: 0x0 Generation: 4261347083 

User: 0 Group: 0 Size: 58369 

File ACL: 1544 Directory ACL: 0 

Links: 1 Blockcount: 24 

Fragment: Address: 0 Number: 0 Size: 0 

ctime: 0x474a5166 -- Mon Nov 26 12:53:58 2007 

atime: 0x474a5187 -- Mon Nov 26 12:54:31 2007 

mtime: 0x474a5166 -- Mon Nov 26 12:53:58 2007 

BLOCKS: 

(IND):24576, (14):24577 

TOTAL: 2



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


清单7. 索引数据块中存储的数据

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


# hexdump block.24576 

0000000 0000 0000 0000 0000 6001 0000 0000 0000 

0000010 0000 0000 0000 0000 0000 0000 0000 0000 

* 

0001000



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


清单8. 手工恢复带有文件洞的大文件

# dd if=/dev/zero of=/tmp/recover/hole.57K.part1 bs=4096 count=14 

# dd if=/dev/sdb6 of=/tmp/recover/hole.57K.part2 bs=4096 count=1 skip=24577 


# cat /tmp/recover/hole.57K.part1 /tmp/recover/hole.57K.part2 / 

> /tmp/recover/hole.57K.full 

# split -d -b 58369 hole.57K.full hole.57K 

# mv hole.57K00 hole.57K 


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



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

目录

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


清单9. 用来测试的文件系统的信息

# debugfs /dev/sdb6 
debugfs 1.39 (29-May-2006) 

debugfs: ls -l 

 2 40755 (2) 0 0 4096 28-Nov-2007 16:57 . 

 2 40755 (2) 0 0 4096 28-Nov-2007 16:57 .. 

 11 40700 (2) 0 0 16384 28-Nov-2007 16:52 lost+found 

 12 100755 (1) 0 0 1406 28-Nov-2007 16:53 createfile.sh 

 13 100644 (1) 0 0 35840 28-Nov-2007 16:53 testfile.35K 

 14 100644 (1) 0 0 10485760 28-Nov-2007 16:54 testfile.10M 

 32577 40755 (2) 0 0 4096 28-Nov-2007 16:56 dir1 

 15 100644 (1) 0 0 35840 28-Nov-2007 16:56 testfile.35K.orig 

 16 100644 (1) 0 0 10485760 28-Nov-2007 16:57 testfile.10M.orig 


debugfs: ls -l dir1 

 32577 40755 (2) 0 0 4096 28-Nov-2007 16:56 . 

 2 40755 (2) 0 0 4096 28-Nov-2007 16:57 .. 

 32578 100755 (1) 0 0 1406 28-Nov-2007 16:55 createfile.sh 

 32579 40755 (2) 0 0 4096 28-Nov-2007 16:55 subdir11 

 48865 40755 (2) 0 0 4096 28-Nov-2007 16:55 subdir12 

 32580 100644 (1) 0 0 35840 28-Nov-2007 16:56 testfile.35K 

 32581 100644 (1) 0 0 10485760 28-Nov-2007 16:56 testfile.10M



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

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

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


清单10. 根目录(索引节点 <2>)子目录 dir1的 inode 信息

debugfs: stat <2> 

Inode: 2 Type: directory Mode: 0755 Flags: 0x0 Generation: 0 

User: 0 Group: 0 Size: 4096 

File ACL: 0 Directory ACL: 0 

Links: 4 Blockcount: 8 

Fragment: Address: 0 Number: 0 Size: 0 

ctime: 0x474d2d63 -- Wed Nov 28 16:57:07 2007 

atime: 0x474d3203 -- Wed Nov 28 17:16:51 2007 

mtime: 0x474d2d63 -- Wed Nov 28 16:57:07 2007 

BLOCKS: 

(0):1536 

TOTAL: 1 


debugfs: stat <32577> 

Inode: 32577 Type: directory Mode: 0755 Flags: 0x0 Generation: 1695264350 

User: 0 Group: 0 Size: 4096 

File ACL: 1542 Directory ACL: 0 

Links: 4 

 Blockcount: 

16 

Fragment: Address: 0 Number: 0 Size: 0 

ctime: 0x474d2d2a -- Wed Nov 28 16:56:10 2007 

atime: 0x474d3203 -- Wed Nov 28 17:16:51 2007 

mtime: 0x474d2d2a -- Wed Nov 28 16:56:10 2007 

BLOCKS: 

(0):88064 

TOTAL: 1


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


清单11. 备份根目录和子目录 dir1 的数据块

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

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


清单12. read_dir_entry.c 源代码

#include <stdio.h> 

#include <stdlib.h> 

#include <ext2fs/ext2_fs.h> 


struct ext2_dir_entry_part { 

 __u32 inode; /* Inode number */ 

 __u16 rec_len; /* Directory entry length */ 

 __u8 name_len; /* Name length */ 

 __u8 file_type; 

} dep; 


void usage() 

{ 

 printf("read_dir_entry [dir entry filename] [dir entry size]/n"); 

} 


int main(int argc, char **argv) 

{ 


 struct ext2_dir_entry_2 de; 

 char *filename = NULL; 

 FILE *fp = NULL; 

 int rtn = 0; 

 int length = 0; 

 int de_size = 0; 


 if (argc < 3) 

 { 

 printf("Too few parameters!/n"); 

 usage(); 

 exit(1); 

 } 


 filename = argv[1]; 

 de_size = atoi(argv[2]); 


 fp = fopen(filename, "r"); 

 if (!fp) 

 { 

 printf("cannot open file: %s/n", filename); 

 exit(1); 

 } 


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

 printf("=================================================================/n"); 


 while ( rtn = fread(&dep, sizeof(struct ext2_dir_entry_part), 1, fp) ) 

 { 

 if (dep.rec_len <= 0) 

 { 

 fclose(fp); 

 exit(0); 

 } 


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


 fread(&de, ((int)(dep.name_len + 3)/4)*4 + sizeof(struct ext2_dir_entry_part), 1, fp); 

 de.name[de.name_len] = '/0'; 


 printf("%6d: %12d%12d%12d%12d %s/n", / 

 length, de.inode, de.rec_len, de.name_len, de.file_type, de.name); 


 length += dep.rec_len; 


 if (length >= de_size - sizeof(struct ext2_dir_entry_part)) 

 { 

 fclose(fp); 

 exit(0); 

 } 

 } 


 fclose(fp); 

}


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

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


清单13. 编译 read_dir_entry.c

# gcc –o read_dir_entry read_dir_entry.c

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


清单14. 分析原始目录项中的数据

# ./read_dir_entry block.1536.orig 4096 

 offset | inode number | rec_len | name_len | file_type | name 

================================================================= 

 0: 2 12 1 2 . 

 12: 2 12 2 2 .. 

 24: 11 20 10 2 lost+found 

 44: 12 24 13 1 createfile.sh 

 68: 13 20 12 1 testfile.35K 

 88: 14 20 12 1 testfile.10M 

 108: 32577 12 4 2 dir1 

 120: 15 28 17 1 testfile.35K.orig 

 148: 16 3948 17 1 testfile.10M.orig 


# ./read_dir_entry block.88064.orig 4096 

 offset | inode number | rec_len | name_len | file_type | name 

================================================================= 

 0: 32577 12 1 2 . 

 12: 2 12 2 2 .. 

 24: 32578 24 13 1 createfile.sh 

 48: 32579 16 8 2 subdir11 

 64: 48865 16 8 2 subdir12 

 80: 32580 20 12 1 testfile.35K 

 100: 32581 3996 12 1 testfile.10M



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

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


清单15. 删除目录并重新备份目录项数据

# rm –rf /tmp/test/dir1 

# cd / 

# umount /tmp/test 


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

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



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


清单16. 分析新目录项中的数据

# ./read_dir_entry block.1536.deleted 4096 

 offset | inode number | rec_len | name_len | file_type | name 

================================================================= 

 0: 2 12 1 2 . 

 12: 2 12 2 2 .. 

 24: 11 20 10 2 lost+found 

 44: 12 24 13 1 createfile.sh 

 68: 13 20 12 1 testfile.35K 

 88: 14 32 12 1 testfile.10M 

 120: 0 12 4 2 dir1 

 132: 15 28 17 1 testfile.35K.orig 

 160: 16 3948 17 1 testfile.10M.orig 


# ./read_dir_entry block.88064.deleted 4096 

 offset | inode number | rec_len | name_len | file_type | name 

================================================================= 

 0: 32577 12 1 2 . 

 12: 2 12 2 2 .. 

 24: 32578 24 13 1 createfile.sh 

 48: 32579 16 8 2 subdir11 

 64: 48865 16 8 2 subdir12 

 80: 32580 20 12 1 testfile.35K 

 100: 32581 3996 12 1 testfile.10M



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

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


清单17. 删除子目录后索引节点信息的变化

# debugfs /dev/sdb6 

debugfs 1.39 (29-May-2006) 

debugfs: stat <2> 

Inode: 2 Type: directory Mode: 0755 Flags: 0x0 Generation: 0 

User: 0 Group: 0 Size: 4096 

File ACL: 0 Directory ACL: 0 

Links: 3 Blockcount: 8 

Fragment: Address: 0 Number: 0 Size: 0 

ctime: 0x474d3387 -- Wed Nov 28 17:23:19 2007 

atime: 0x474d33c2 -- Wed Nov 28 17:24:18 2007 

mtime: 0x474d3387 -- Wed Nov 28 17:23:19 2007 

BLOCKS: 

(0):1536 

TOTAL: 1 


debugfs: stat <32577> 

Inode: 32577 Type: directory Mode: 0755 Flags: 0x0 Generation: 1695264350 

User: 0 Group: 0 Size: 0 

File ACL: 1542 Directory ACL: 0 

Links: 0 Blockcount: 16 

Fragment: Address: 0 Number: 0 Size: 0 

ctime: 0x474d3387 -- Wed Nov 28 17:23:19 2007 

atime: 0x474d3387 -- Wed Nov 28 17:23:19 2007 

mtime: 0x474d3387 -- Wed Nov 28 17:23:19 2007 

dtime: 0x474d3387 -- Wed Nov 28 17:23:19 2007 

BLOCKS: 

(0):88064 

TOTAL: 1


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

将父目录的 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) 

debugfs: lsdel 

 Inode Owner Mode Size Blocks Time deleted 

 32577 0 40755 0 1/ 1 Wed Nov 28 17:23:19 2007 

 32578 0 100755 1406 1/ 1 Wed Nov 28 17:23:19 2007 

 32579 0 40755 0 1/ 1 Wed Nov 28 17:23:19 2007 

 32580 0 100644 35840 9/ 9 Wed Nov 28 17:23:19 2007 

 32581 0 100644 10485760 2564/2564 Wed Nov 28 17:23:19 2007 

 48865 0 40755 0 1/ 1 Wed Nov 28 17:23:19 2007 

6 deleted inodes found. 

debugfs: mi <32577> 

 Mode [040755] 

 User ID [0] 

 Group ID [0] 

 Size [0] 4096 

 Creation time [1196241799] 

 Modification time [1196241799] 

 Access time [1196241799] 

 Deletion time [1196241799] 0 

 Link count [0] 4 

 Block count [16] 

 File flags [0x0] 

 Generation [0x650bae5e] 

 File acl [1542] 

 Directory acl [0] 

 Fragment address [0] 

 Fragment number [0] 

 Fragment size [0] 

 Direct Block #0 [88064] 

 Direct Block #1 [0] 

 Direct Block #2 [0] 

 Direct Block #3 [0] 

 Direct Block #4 [0] 

 Direct Block #5 [0] 

 Direct Block #6 [0] 

 Direct Block #7 [0] 

 Direct Block #8 [0] 

 Direct Block #9 [0] 

 Direct Block #10 [0] 

 Direct Block #11 [0] 

 Indirect Block [0] 

 Double Indirect Block [0] 

 Triple Indirect Block [0] 

debugfs: link <32577> dir1 

debugfs: q



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

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


清单19. 验证恢复结果

# mount /dev/sdb6 /tmp/test 

# ls -li /tmp/test 

total 20632 

 12 -rwxr-xr-x 1 root root 1406 Nov 28 16:53 createfile.sh 

32577 drwxr-xr-x 4 root root 4096 Nov 28 17:23 dir1 

 11 drwx------ 2 root root 16384 Nov 28 16:52 lost+found 

 14 -rw-r--r-- 1 root root 10485760 Nov 28 16:54 testfile.10M 

 16 -rw-r--r-- 1 root root 10485760 Nov 28 16:57 testfile.10M.orig 

 13 -rw-r--r-- 1 root root 35840 Nov 28 16:53 testfile.35K 

 15 -rw-r--r-- 1 root root 35840 Nov 28 16:56 testfile.35K.orig 


# ls -li /tmp/test/dir1 

total 0 

??--------- ? ? ? ? ? /tmp/test/dir1/createfile.sh 

??--------- ? ? ? ? ? /tmp/test/dir1/subdir11 

??--------- ? ? ? ? ? /tmp/test/dir1/subdir12 

??--------- ? ? ? ? ? /tmp/test/dir1/testfile.10M 

??--------- ? ? ? ? ? /tmp/test/dir1/testfile.35K



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


清单20. 使用 e2fsck 强制对文件系统进行一致性检查

# e2fsck -f -y /dev/sdb6 > e2fsck.out 2>&1

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

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

符号链接

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


清单21.硬链接采用相同的索引节点号

# ln testfile.10M hardlink.10M 


# ls -li 

total 20592 

 12 -rwxr-xr-x 1 root root 1406 Nov 29 19:19 createfile.sh 

1205313 drwxr-xr-x 2 root root 4096 Nov 29 19:29 dir1 

 14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 hardlink.10M 

 11 drwx------ 2 root root 16384 Nov 29 19:19 lost+found 

 14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 testfile.10M 

 13 -rw-r--r-- 1 root root 35840 Nov 29 19:20 testfile.35K



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

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

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

不能对目录建立硬链接,否则就会引起循环引用的问题,从而导致最终正常路径的无法访问。
不能建立跨文件系统的硬链接,这是由于每个文件系统中的索引节点号都是单独进行编号的,跨文件系统就会导致索引节点号变得非常混乱。而这在现代 Linux/Unix 操作系统上恰恰是无法接受的,因为每个文件系统中都可能会有很多挂载点来挂载不同的文件系统。
为了解决上面的问题,符号链接就应运而生了。符号链接与硬链接的区别在于它要占用一个单独的索引节点来存储相关数据,但却并不存储链接指向的文件的数据,而是存储链接的路径名:如果这个路径名小于60个字符,就其存储在符号链接索引节点的 i_block 域中;如果超过 60 个字符,就使用一个单独的数据块来存储。下面让我们来看一个例子:


清单22. 符号链接采用不同的索引节点号

# ln -s testfile.10M softlink.10M 


# ls -li 

total 20596 

 12 -rwxr-xr-x 1 root root 1406 Nov 29 19:19 createfile.sh 

1205313 drwxr-xr-x 2 root root 4096 Nov 29 19:29 dir1 

 14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 hardlink.10M 

 11 drwx------ 2 root root 16384 Nov 29 19:19 lost+found 

 15 lrwxrwxrwx 1 root root 12 Nov 29 19:41 softlink.10M -> testfile.10M 

 14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 testfile.10M 

 13 -rw-r--r-- 1 root root 35840 Nov 29 19:20 testfile.35K 


# echo "stat <15>" | debugfs /dev/sdb6 

debugfs 1.39 (29-May-2006) 

debugfs: Inode: 15 Type: symlink Mode: 0777 Flags: 0x0 Generation: 2344716327 

User: 0 Group: 0 Size: 12 

File ACL: 1542 Directory ACL: 0 

Links: 1 Blockcount: 8 

Fragment: Address: 0 Number: 0 Size: 0 

ctime: 0x474ea56f -- Thu Nov 29 19:41:35 2007 

atime: 0x474ea571 -- Thu Nov 29 19:41:37 2007 

mtime: 0x474ea56f -- Thu Nov 29 19:41:35 2007 

Fast_link_dest: testfile.10M




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

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


清单23. 长名符号链接

# touch abcdwfghijklmnopqrstuvwxyz0123456789abcdwfghijklmnopqrstuvwxyz0123456789.sh 


# ln -s abcdwfghijklmnopqrstuvwxyz0123456789abcdwfghijklmnopqrstuvwxyz0123456789.sh / 

longsoftlink.sh 


# ls -li 

total 20608 

 16 -rw-r--r-- 1 root root 0 Nov 29 19:52 / 

 abcdwfghijklmnopqrstuvwxyz0123456789abcdwfghijklmnopqrstuvwxyz0123456789.sh 

 12 -rwxr-xr-x 1 root root 1406 Nov 29 19:19 createfile.sh 

1205313 drwxr-xr-x 2 root root 4096 Nov 29 19:29 dir1 

 14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 hardlink.10M 

 17 lrwxrwxrwx 1 root root 75 Nov 29 19:53 longsoftlink.sh -> / 

 abcdwfghijklmnopqrstuvwxyz0123456789abcdwfghijklmnopqrstuvwxyz0123456789.sh 

 11 drwx------ 2 root root 16384 Nov 29 19:19 lost+found 

 15 lrwxrwxrwx 1 root root 12 Nov 29 19:41 softlink.10M -> testfile.10M 

 14 -rw-r--r-- 2 root root 10485760 Nov 29 19:21 testfile.10M 

 13 -rw-r--r-- 1 root root 35840 Nov 29 19:20 testfile.35K 


# echo "stat <17>" | debugfs /dev/sdb6 

debugfs 1.39 (29-May-2006) 

debugfs: Inode: 17 Type: symlink Mode: 0777 Flags: 0x0 Generation: 744523175 

User: 0 Group: 0 Size: 75 

File ACL: 1542 Directory ACL: 0 

Links: 1 Blockcount: 16 

Fragment: Address: 0 Number: 0 Size: 0 

ctime: 0x474ea824 -- Thu Nov 29 19:53:08 2007 

atime: 0x474ea826 -- Thu Nov 29 19:53:10 2007 

mtime: 0x474ea824 -- Thu Nov 29 19:53:08 2007 

BLOCKS: 

 (0):6144 

 TOTAL: 1




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


清单24. 恢复长名符号链接

# dd if=/dev/sdb6 of=longsoftlink.6144 bs=4096 count=1 skip=6144 


# xxd longsoftlink.6144 | more 

0000000: 6162 6364 7766 6768 696a 6b6c 6d6e 6f70 abcdwfghijklmnop 

0000010: 7172 7374 7576 7778 797a 3031 3233 3435 qrstuvwxyz012345 

0000020: 3637 3839 6162 6364 7766 6768 696a 6b6c 6789abcdwfghijkl 

0000030: 6d6e 6f70 7172 7374 7576 7778 797a 3031 mnopqrstuvwxyz01 

0000040: 3233 3435 3637 3839 2e73 6800 0000 0000 23456789.sh..... 

0000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................



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

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

小结

本文介绍了 ext2 文件系统中比较特殊的一些文件的存储和恢复机制,包括文件洞、目录和链接,并介绍了如何结合使用 debugfs 和 e2fsck 等工具完整恢复 ext2 文件系统的方法。在本系列的后续文章中,我们将介绍几个可以自动恢复 ext2 文件系统中已删除文件的工具,以及对 ext2 文件系统的后继者 ext3 和 ext4 文件系统的一些考虑。