天天看點

如何恢複 Linux删除的檔案 - AlanTu

如何恢複 Linux删除的檔案

原理及普通檔案的恢複

要想恢複誤删除的檔案,必須清楚資料在磁盤上究竟是如何存儲的,以及如何定位并恢複資料。本文從資料恢複的角度,着重介紹了 ext2 檔案系統中使用的一些基本概念和重要資料結構,并通過幾個執行個體介紹了如何手工恢複已經删除的檔案。最後針對 ext2 現有實作存在的大檔案無法正常恢複的問題,通過修改核心中的實作,給出了一種解決方案。

  • 如何恢複 Linux删除的檔案 - AlanTu
    内容

對于很多 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):25613TOTAL: 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 檔案系統中的一些特殊檔案,以及如何恢複整個目錄等方面的問題。

回頁首

下載下傳

描述 名字 大小
建立測試檔案使用的腳本 createfile.sh 2KB
針對 2.6.23 版本核心解決大檔案無法恢複問題的更新檔 indirect_index.patch 2KB