如何恢復 Linux 上刪除的文件,第 1 部分(原理及普通文件的恢復)

 
要想恢復誤刪除的文件,必須清楚數據在磁盤上究竟是如何存儲的,以及如何定位並恢復數據。本文從數據恢復的角度,着重介紹了 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 {
__le32 inode; /* 索引節點號 */
__le16 rec_len; /* 目錄項的長度 */
__u8 name_len; /* 文件名長度 */
__u8 file_type; /* 文件類型 */
char name[EXT2_NAME_LEN]; /* 文件名 */
};

在 Unix/Linux 系統中,目錄只是一種特殊的文件。目錄和文件是通過 file_type 域來區分的,該值爲 1 則表示是普通文件,該值爲 2 則表示是目錄。

對於每個 ext2 分區來說,其在物理磁盤上的佈局如圖 2 所示:


圖 2. ext2 分區的佈局

從圖 2 中可以看到,對於 ext2 文件系統來說,磁盤被劃分成一個個大小相同的數據塊,每個塊的大小可以是1024、2048 或 4096 個字節。其中,第一個塊稱爲引導塊,一般保留做引導扇區使用,因此 ext2 文件系統一般都是從第二個塊開始的。剩餘的塊被劃分爲一個個的塊組,ext2 文件系統會試圖儘量將相同文件的數據塊都保存在同一個塊組中,並且儘量保證文件在磁盤上的連續性,從而提高文件讀寫時的性能。

至於一個分區中到底有多少個塊組,這取決於兩個因素:

  1. 分區大小。
  2. 塊大小。

最終的計算公式如下:

分區中的塊組數=分區大小/(塊大小*8)

這是由於在每個塊組中使用了一個數據塊位圖來標識數據塊是否空閒,因此每個塊組中最多可以有(塊大小*8)個塊;該值除上分區大小就是分區中總的塊組數。

每個塊組都包含以下內容:

  1. 超級塊。存放文件系統超級塊的一個拷貝。
  2. 組描述符。該塊組的組描述符。
  3. 數據塊位圖。標識相應的數據塊是否空閒。
  4. 索引節點位圖。標識相應的索引節點是否空閒。
  5. 索引節點表。存放所有索引節點的數據。
  6. 數據塊。該塊組中用來保存實際數據的數據塊。

在每個塊組中都保存了超級塊的一個拷貝,默認情況下,只有第一個塊組中的超級塊結構纔會被系統內核使用;其他塊組中的超級塊可以在 e2fsck 之類的程序對磁盤上的文件系統進行一致性檢查使用。在 ext2 文件系統中,超級塊的結構會通過一個名爲 ext2_super_block 的結構進行引用。該結構的一些重要域如下所示:


清單 2. ext2_super_block 結構定義

                
struct ext2_super_block {
__le32 s_inodes_count; /* 索引節點總數 */
__le32 s_blocks_count; /* 塊數,即文件系統以塊爲單位的大小 */
__le32 s_r_blocks_count; /* 系統預留的塊數 */
__le32 s_free_blocks_count; /* 空閒塊數 */
__le32 s_free_inodes_count; /* 空閒索引節點數 */
__le32 s_first_data_block; /* 第一個可用數據塊的塊號 */
__le32 s_log_block_size; /* 塊大小 */
__le32 s_blocks_per_group; /* 每個塊組中的塊數 */
__le32 s_inodes_per_group; /* 每個塊組中的索引節點個數 */
...
}

每個塊組都有自己的組描述符,在 ext2 文件系統中是通過一個名爲 ext2_group_desc的結構進行引用的。該結構的定義如下:


清單 3. ext2_group_desc 結構定義

                
/*
* Structure of a blocks group descriptor
*/
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* 數據塊位圖的塊號 */
__le32 bg_inode_bitmap; /* 索引節點位圖的塊號 */
__le32 bg_inode_table; /* 第一個索引節點表的塊號 */
__le16 bg_free_blocks_count; /* 該組中空閒塊數 */
__le16 bg_free_inodes_count; /* 該組中空閒索引節點數 */
__le16 bg_used_dirs_count; /* 該組中的目錄項 */
__le16 bg_pad;
__le32 bg_reserved[3];
};

數據塊位圖和索引節點位圖分別佔用一個塊的大小,其每一位描述了對應數據塊或索引節點是否空閒,如果該位爲0,則表示空閒;如果該位爲1,則表示已經使用。

索引節點表存放在一系列連續的數據塊中,每個數據塊中可以包括若干個索引節點。每個索引節點在 ext2 文件系統中都通過一個名爲 ext2_inode 的結構進行引用,該結構大小固定爲 128 個字節,其中一些重要的域如下所示:


清單 4. ext2_inode 結構定義

                
/*
* Structure of an inode on the disk
*/
struct ext2_inode {
__le16 i_mode; /* 文件模式 */
__le16 i_uid; /* 文件所有者的 uid */
__le32 i_size; /* 以字節爲單位的文件長度 */
__le32 i_atime; /* 最後一次訪問該文件的時間 */
__le32 i_ctime; /* 索引節點最後改變的時間 */
__le32 i_mtime; /* 文件內容最後改變的時間 */
__le32 i_dtime; /* 文件刪除的時間 */
__le16 i_gid; /* 文件所有者的 gid */
__le16 i_links_count; /* 硬鏈接數 */
__le32 i_blocks; /* 文件的數據塊數 */
...
__le32 i_block[EXT2_N_BLOCKS];/* 指向數據塊的指針 */
...
};

第一個索引節點所在的塊號保存在該塊組描述符的 bg_inode_table 域中。請注意 i_block 域,其中就包含了保存數據的數據塊的位置。有關如何對數據塊進行尋址,請參看後文“數據塊尋址方式”一節的內容。

需要知道的是,在普通的刪除文件操作中,操作系統並不會逐一清空保存該文件的數據塊的內容,而只會釋放該文件所佔用的索引節點和數據塊,方法是將索引節點位圖和數據塊位圖中的相應標識位設置爲空閒狀態。因此,如果我們可以找到文件對應的索引節點,由此查到相應的數據塊,就可能從磁盤上將已經刪除的文件恢復出來。

幸運的是,這一切都是可能的!本文將通過幾個實驗來了解一下如何從磁盤上恢復刪除的文件。

數據塊尋址方式

回想一下,ext2_inode 結構的 i_block 域是一個大小爲 EXT2_N_BLOCKS 的數組,其中保存的就是真正存放文件數據的數據塊的位置。通常來說,EXT2_N_BLOCKS 大小爲 15。在 ext2 文件系統,採用了直接尋址和間接尋址兩種方式來對數據塊進行尋址,原理如圖3 所示:


圖 3. 數據塊尋址方式

  • 對於 i_block 的前 12 個元素(i_block[0]到i_block[11])來說,其中存放的就是實際的數據塊號,即對應於文件的 0 到 11 塊。這種方式稱爲直接尋址。
  • 對於第13個元素(i_block[12])來說,其中存放的是另外一個數據塊的邏輯塊號;這個塊中並不存放真正的數據,而是存放真正保存數據的數據塊的塊號。即 i_block[12] 指向一個二級數組,其每個元素都是對應數據塊的邏輯塊號。由於每個塊號需要使用 4 個字節表示,因此這種尋址方式可以訪問的對應文件的塊號範圍爲 12 到 (塊大小/4)+11。這種尋址方式稱爲間接尋址。
  • 對於第14個元素(i_block[13])來說,其中存放也是另外一個數據塊的邏輯塊號。與間接尋址方式不同的是,i_block[13] 所指向的是一個數據塊的邏輯塊號的二級數組,而這個二級數組的每個元素又都指向一個三級數組,三級數組的每個元素都是對應數據塊的邏輯塊號。這種尋址方式稱爲二次間接尋址,對應文件塊號的尋址範圍爲 (塊大小/4)+12 到 (塊大小/4)2+(塊大小/4)+11。
  • 對於第15個元素(i_block[14])來說,則利用了三級間接索引,其第四級數組中存放的纔是邏輯塊號對應的文件塊號,其尋址範圍從 (塊大小/4)2+(塊大小/4)+12 到 (塊大小/4)3+ (塊大小/4)2+(塊大小/4)+11。

ext2 文件系統可以支持1024、2048和4096字節三種大小的塊,對應的尋址能力如下表所示:


表 1. 各種數據塊對應的文件尋址範圍

塊大小 直接尋址 間接尋址 二次間接尋址 三次間接尋址
1024 12KB 268KB 64.26MB 16.06GB
2048 24KB 1.02MB 513.02MB 265.5GB
4096 48KB 4.04MB 4GB ~ 4TB

掌握上面介紹的知識之後,我們就可以開始恢復文件的實驗了。

準備文件系統

爲了防止破壞已有系統,本文將採用一個新的分區進行恢復刪除文件的實驗。

首先讓我們準備好一個新的分區,並在上面創建 ext2 格式的文件系統。下面的命令可以幫助創建一個 20GB 的分區:


清單 5. 新建磁盤分區

                
# fdisk /dev/sdb << END
n

+20G
p
w
q
END

在筆者的機器上,這個分區是 /dev/sdb6。然後創建文件系統:


清單 6. 在新分區上創建 ext2 文件系統

                
# mke2fs /dev/sdb6

並將其掛載到系統上來:


清單 7. 掛載創建的 ext2 文件系統

                
# mkdir /tmp/test
# mount /dev/sdb6 /tmp/test

在真正使用這個文件系統之前,讓我們首先使用系統提供的一個命令 dumpe2fs 來熟悉一下這個文件系統的一些具體參數:


清單 8. 使用 dumpe2fs 熟悉這個文件系統的參數

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


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

...

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

應用前面介紹的一些知識,我們可以看到,這個文件系統中,塊大小(Block size)爲4096字節,因此每個塊組中的塊數應該是4096*8=32768個(Blocks per group),每個塊組的大小是 128MB,整個分區被劃分成20GB/(4KB*32768)=160個。但是爲什麼我們只看到 150 個塊組(0到149)呢?實際上,在 fdisk 中,我們雖然輸入要創建的分區大小爲 20GB,但實際上,真正分配的空間並不是嚴格的20GB,而是隻有大約 20*109 個字節,準確地說,應該是 (4885760 * 4096) / (1024*1024*1024) = 18.64GB。這是由於不同程序的計數單位的不同造成的,在使用存儲設備時經常遇到這種問題。因此,這個分區被劃分成 150 個塊組,前 149 個塊組分別包含 32768 個塊(即 128B),最後一個塊組只包含 3328 個塊。

另外,我們還可以看出,每個索引節點的大小是 128 字節,每個塊組中包含 16288 個索引節點,在磁盤上使用 509 個塊來存儲(16288*128/4096),在第一個塊組中,索引節點表保存在 1027 到 1535 塊上。

數據塊和索引節點是否空閒,是分別使用塊位圖和索引節點位圖來標識的,在第一個塊組中,塊位圖和索引節點位圖分別保存在 1025 和 1026 塊上。

dumpe2fs 的輸出結果中還包含了其他一些信息,我們暫時先不用詳細關心這些信息。

準備測試文件

現在請將附件中的 createfile.sh 文件下載到本地,並將其保存到 /tmp/test 目錄中,這個腳本可以幫助我們創建一個特殊的文件,其中每行包含 1KB 字符,最開始的14個字符表示行號。之所以採用這種文件格式,是爲了方便地確認所恢復出來的文件與原始文件之間的區別。這個腳本的用法如下:


清單 9. createfile.sh 腳本的用法

                
# ./createfile.sh [size in KB] [filename]

第 1 個參數表示所生成的文件大小,單位是 KB;第 2 個參數表示所生成文件的名字。

下面讓我們創建幾個測試文件:


清單 10. 準備測試文件

                
# cd /tmp/test
#./createfile.sh 35 testfile.35K
#./createfile.sh 10240 testfile.10M

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

上面的命令新創建了大小爲 35 KB 和 9000KB 的兩個文件,併爲它們各自保存了一個備份,備份文件的目的是爲了方便使用 diff 之類的工具驗證最終恢復出來的文件與原始文件完全一致。

ls 命令的 –i 選項可以查看有關保存文件使用的索引節點的信息:


清單11. 查看文件的索引節點號

                
# ls -li | sort
11 drwx------ 2 root root 16384 Oct 29 20:08 lost+found
12 -rwxr-xr-x 1 root root 1406 Oct 29 20:09 createfile.sh
13 -rw-r--r-- 1 root root 35840 Oct 29 20:09 testfile.35K
14 -rw-r--r-- 1 root root 10485760 Oct 29 20:10 testfile.10M
15 -rw-r--r-- 1 root root 35840 Oct 29 20:10 testfile.35K.orig
16 -rw-r--r-- 1 root root 10485760 Oct 29 20:11 testfile.10M.orig

第一列中的數字就是索引節點號。從上面的輸出結果我們可以看出,索引節點號是按照我們創建文件的順序而逐漸自增的,我們剛纔創建的 35K 大小的文件的索引節點號爲 13,10M 大小的文件的索引節點號爲 14。debugfs 中提供了很多工具,可以幫助我們瞭解進一步的信息。現在執行下面的命令:


清單12. 查看索引節點 <13> 的詳細信息

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

輸出結果顯示的就是索引節點 13 的詳細信息,從中我們可以看到諸如文件大小(35840=35K)、權限(0644)等信息,尤其需要注意的是最後 3 行的信息,即該文件被保存到磁盤上的 4096 到 4104 總共 9 個數據塊中。

下面再看一下索引節點 14 (即 testfile.10M 文件)的詳細信息:


清單13. 查看索引節點 <14> 的詳細信息

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

和索引節點 13 相比,二者之間最重要的區別在於 BLOCKS 的數據,testfile.10M 在磁盤上總共佔用了 2564 個數據塊,由於需要採用二級間接尋址模式進行訪問,所以使用了4個塊來存放間接尋址的信息,分別是24588、25613、25614和26639,其中25613塊中存放的是二級間接尋址的信息。

恢復刪除文件

現在將剛纔創建的兩個文件刪除:


清單14. 刪除測試文件

                
# rm -f testfile.35K testfile.10M

debugfs 的 lsdel 命令可以查看文件系統中刪除的索引節點的信息:


清單15. 使用 lsdel 命令搜索已刪除的文件

                
# echo "lsdel" | debugfs /dev/sdb6
debugfs 1.39 (29-May-2006)
Inode Owner Mode Size Blocks Time deleted
13 0 100644 35840 9/9 Mon Oct 29 20:32:05 2007
14 0 100644 10485760 2564/2564 Mon Oct 29 20:32:05 2007
2 deleted inodes found.

回想一下 inode 結構中有 4 個有關時間的域,分別是 i_atime、i_ctime、i_mtime和i_dtime,分別表示該索引節點的最近訪問時間、創建時間、修改時間和刪除時間。其中 i_dtime域只有在該索引節點對應的文件或目錄被刪除時纔會被設置。dubugfs 的 lsdel 命令會去掃描磁盤上索引節點表中的所有索引節點,其中 i_dtime 不爲空的項就被認爲是已經刪除的文件所對應的索引節點。

從上面的結果可以看到,剛纔刪除的兩個文件都已經找到了,我們可以通過文件大小區分這兩個文件,二者一個大小爲35K,另外一個大小爲10M,正式我們剛纔刪除的兩個文件。debugfs 的 dump 命令可以幫助恢復文件:


清單16. 使用 dump 命令恢復已刪除的文件

                
# echo "dump <13> /tmp/recover/testfile.35K.dump" | debugfs /dev/sdb6
# echo "dump <14> /tmp/recover/testfile.10M.dump" | debugfs /dev/sdb6

執行上面的命令之後,在 /tmp/recover 目錄中會生成兩個文件,比較這兩個文件與我們前面備份的文件的內容就會發現,testfile.35K.dump 與 testfile.35K.orig 的內容完全相同,而 testfile.10M.dump 文件中則僅有前 48K 數據是對的,後面的數據全部爲 0 了。這是否意味着刪除文件時間已經把數據也同時刪除了呢?實際上不是,我們還是有辦法把數據全部恢復出來的。記得我們剛纔使用 debugfs 的 stat 命令查看索引節點 14 時的 BLOCKS 的數據嗎?這些數據記錄了整個文件在磁盤上存儲的位置,有了這些數據就可以把整個文件恢復出來了,請執行下面的命令:


清單 17. 使用 dd 命令手工恢復已刪除的文件

                
# dd if=/dev/sdb6 of=/tmp/recover/testfile.10M.dd.part1 bs=4096 count=12 skip=24576
# dd if=/dev/sdb6 of=/tmp/recover/testfile.10M.dd.part2 bs=4096 count=1024 skip=24589
# dd if=/dev/sdb6 of=/tmp/recover/testfile.10M.dd.part2 bs=4096 count=1024 skip=25615
# dd if=/dev/sdb6 of=/tmp/recover/testfile.10M.dd.part4 bs=4096 count=500 skip=26640

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

比較一下最終的 testfile.10M.dd 文件和已經備份過的 testfile.10M.orig 文件就會發現,二者完全相同:


清單 18. 使用 diff 命令對恢復文件和原文件進行比較

                
# diff /tmp/recover/ testfile.10M.dd /tmp/test/ testfile.10M.orig

數據明明存在,但是剛纔我們爲什麼沒法使用 debugfs 的 dump 命令將數據恢復出來呢?現在使用 debugfs 的 stat 命令再次查看一下索引節點 14 的信息:


清單 19. 再次查看索引節點 <14> 的詳細信息

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

與前面的結果比較一下不難發現,BLOCKS後面的數據說明總塊數爲 14,而且也沒有整個文件所佔據的數據塊的詳細說明了。既然文件的數據全部都沒有發生變化,那麼間接尋址所使用的那些索引數據塊會不會有問題呢?現在我們來查看一下 24588 這個間接索引塊中的內容:


清單 20. 查看間接索引塊 24588 中的內容

                
# dd if=/dev/sdb6 of=block. 24588 bs=4096 count=1 skip=24588

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

顯然,這個數據塊的內容被全部清零了。debugfs 的dump 命令按照原來的尋址方式試圖恢復文件時,所訪問到的實際上都是第0 個數據塊(引導塊)中的內容。這個分區不是可引導分區,因此這個數據塊中沒有寫入任何數據,因此 dump 恢復出來的數據只有前48K是正確的,其後所有的數據全部爲0。

實際上,ext2 是一種非常優秀的文件系統,在磁盤空間足夠的情況下,它總是試圖將數據寫入到磁盤上的連續數據塊中,因此我們可以假定數據是連續存放的,跳過間接索引所佔據的 24588、25613、25614和26639,將從24576 開始的其餘 2500 個數據塊讀出,就能將整個文件完整地恢復出來。但是在磁盤空間有限的情況下,這種假設並不成立,如果系統中磁盤碎片較多,或者同一個塊組中已經沒有足夠大的空間來保存整個文件,那麼文件勢必會被保存到一些不連續的數據塊中,此時上面的方法就無法正常工作了。

反之,如果在刪除文件的時候能夠將間接尋址使用的索引數據塊中的信息保存下來,那麼不管文件在磁盤上是否連續,就都可以將文件完整地恢復出來了,但是這樣就需要修改 ext2 文件系統的實現了。在 ext2 的實現中,與之有關的有兩個函數:ext2_free_data 和 ext2_free_branches(都在 fs/ext2/inode.c 中)。2.6 版本內核中這兩個函數的實現如下:


清單 21. 內核中 ext2_free_data 和 ext2_free_branches 函數的實現

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

注意第 832 和 875 這兩行就是用來將對應的索引項置爲 0 的。將這兩行代碼註釋掉(對於最新版本的內核 2.6.23 可以下載本文給的補丁)並重新編譯 ext2 模塊,然後重新加載新編譯出來的模塊,並重覆上面的實驗,就會發現利用 debugfs 的 dump 命令又可以完美地恢復出整個文件來了。

顯然,這個補丁並不完善,因爲這個補丁中的處理只是保留了索引數據塊中的索引節點數據,但是還沒有考慮數據塊位圖的處理,如果對應的數據塊沒有設置爲正在使用的狀態,並且剛好這些數據塊被重用了,其中的索引節點數據就有可能會被覆蓋掉了,這樣就徹底沒有辦法再恢復文件了。感興趣的讀者可以沿用這個思路自行開發一個比較完善的補丁。

小結

本文介紹了 ext2 文件系統中的一些基本概念和重要數據結構,並通過幾個實例介紹如何恢復已經刪除的文件,最後通過修改內核中 ext2 文件系統的實現,解決了大文件無法正常恢復的問題。本系列的下一篇文章中,將介紹如何恢復 ext2 文件系統中的一些特殊文件,以及如何恢復整個目錄等方面的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章