1. 程式人生 > >如何恢復 Linux 上刪除的檔案 ext2

如何恢復 Linux 上刪除的檔案 ext2

            <script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"></script>           
要想恢復誤刪除的檔案,必須清楚資料在磁碟上究竟是如何儲存的,以及如何定位並恢復資料。本文從資料恢復的角度,著重介紹了 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 分割槽的佈局ext2 分割槽的佈局

           

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

           

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

           
                   
  1. 分割槽大小。
  2.                
  3. 塊大小。
  4.            
           

最終的計算公式如下:

           

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

           

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

           

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

           
                   
  1. 超級塊。存放檔案系統超級塊的一個拷貝。
  2.                
  3. 組描述符。該塊組的組描述符。
  4.                
  5. 資料塊點陣圖。標識相應的資料塊是否空閒。
  6.                
  7. 索引節點點陣圖。標識相應的索引節點是否空閒。
  8.                
  9. 索引節點表。存放所有索引節點的資料。
  10.                
  11. 資料塊。該塊組中用來儲存實際資料的資料塊。
  12.            
           

在每個塊組中都儲存了超級塊的一個拷貝,預設情況下,只有第一個塊組中的超級塊結構才會被系統核心使用;其他塊組中的超級塊可以在 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 << ENDn+20GpwqEND
                       
                       

在筆者的機器上,這個分割槽是 /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-96a9bcda52ceFilesystem magic number:  0xEF53Filesystem revision #:    1 (dynamic)Filesystem features:      ext_attr resize_inode dir_index filetype sparse_super large_fileDefault mount options:    (none)Filesystem state:         not cleanErrors behavior:          ContinueFilesystem OS type:       LinuxInode count:              2443200Block count:              4885760Reserved block count:     244288Free blocks:              4797829Free inodes:              2443189First block:              0Block size:               4096Fragment size:            4096Reserved GDT blocks:      1022Blocks per group:         32768Fragments per group:      32768Inodes per group:         16288Inode blocks per group:   509Filesystem created:       Mon Oct 29 20:04:16 2007Last mount time:          Mon Oct 29 20:06:52 2007Last write time:          Mon Oct 29 20:08:31 2007Mount count:              1Maximum mount count:      39Last checked:             Mon Oct 29 20:04:16 2007Check interval:           15552000 (6 months)Next check after:         Sat Apr 26 20:04:16 2008Reserved blocks uid:      0 (user root)Reserved blocks gid:      0 (group root)First inode:              11Inode size:               128Default directory hash:   teaDirectory Hash Seed:      d1432419-2def-4762-954a-1a26fef9d5e8Group 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 | sort11 drwx------ 2 root root    16384 Oct 29 20:08 lost+found12 -rwxr-xr-x 1 root root     1406 Oct 29 20:09 createfile.sh13 -rw-r--r-- 1 root root    35840 Oct 29 20:09 testfile.35K14 -rw-r--r-- 1 root root 10485760 Oct 29 20:10 testfile.10M15 -rw-r--r-- 1 root root    35840 Oct 29 20:10 testfile.35K.orig16 -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/sdb6debugfs 1.39 (29-May-2006)Inode: 13  Type: regular    Mode:  0644   Flags: 0x0   Generation: 2957086759User:     0   Group:     0   Size: 35840File ACL: 0    Directory ACL: 0Links: 1   Blockcount: 72Fragment:  Address: 0    Number: 0    Size: 0ctime: 0x47268467 -- Mon Oct 29 20:09:59 2007atime: 0x4726849d -- Mon Oct 29 20:10:53 2007mtime: 0x47268467 -- Mon Oct 29 20:09:59 2007BLOCKS:(0-8):4096-4104TOTAL: 9
                       
                       

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

           

下面再看一下索引節點 14 (即 testfile.10M 檔案)的詳細資訊:

           

清單13. 檢視索引節點 <14> 的詳細資訊           

                                                                                                           
                       
                # echo "stat <14>" | debugfs /dev/sdb6debugfs 1.39 (29-May-2006)Inode: 14  Type: regular  Mode: 0644  Flags: 0x0   Generation: 2957086760User:     0   Group:     0   Size: 10485760File ACL: 0    Directory ACL: 0Links: 1   Blockcount: 20512Fragment:  Address: 0    Number: 0    Size: 0ctime: 0x47268485 -- Mon Oct 29 20:10:29 2007atime: 0x472684a5 -- Mon Oct 29 20:11:01 2007mtime: 0x47268485 -- Mon Oct 29 20:10:29 2007BLOCKS:(0-11):24576-24587, (IND):24588, (12-1035):24589-25612, (DIND):25613, (IND):25614, (1036-2059):25615-26638, (IND):26639, (2060-2559):26640-27139TOTAL: 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/sdb6debugfs 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 20072 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/sdb6debugfs 1.39 (29-May-2006)Inode: 14  Type: regular  Mode:  0644  Flags: 0x0   Generation: 2957086760User:     0   Group:     0   Size: 10485760File ACL: 0    Directory ACL: 0Links: 0   Blockcount: 20512Fragment:  Address: 0    Number: 0    Size: 0ctime: 0x47268995 -- Mon Oct 29 20:32:05 2007atime: 0x472684a5 -- Mon Oct 29 20:11:01 2007mtime: 0x47268485 -- Mon Oct 29 20:10:29 2007dtime: 0x47268995 -- Mon Oct 29 20:32:05 2007BLOCKS:                (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. 245880000000 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 blocks816  *      @inode: inode we are dealing with817  *      @p:     array of block numbers818  *      @q:     points immediately past the end of array819  *820  *      We are freeing all blocks refered from that array (numbers are821  *      stored as little-endian 32-bit) and updating @inode->i_blocks822  *      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 branches855  *      @inode: inode we are dealing with856  *      @p:     array of block numbers857  *      @q:     pointer immediately past the end of array858  *      @depth: depth of the branches to free859  *860  *      We are freeing all blocks refered from these branches (numbers are861  *      stored as little-endian 32-bit) and updating @inode->i_blocks862  *      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 slot879                          * (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         } else896                 ext2_free_data(inode, p, q);897 }
                       
                       

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

           

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

                                                                                                                       
                       

小結

           

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

           

除了普通檔案之外,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/hole15 -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 檔案的儲存方法儲存方法

           

file_hole.jpg

           

現在我們可以使用 debugfs 來檢視一下這個檔案的詳細資訊:

           

清單3. 帶有檔案洞的檔案的 inode 資訊           

                                                                                                           
                       
                # echo "stat <15>" | debugfs /dev/sdb6debugfs 1.39 (29-May-2006)debugfs:  Inode: 15   Type: regular    Mode:  0644   Flags: 0x0   Generation: 4118330634User:     0   Group:     0   Size: 7169File ACL: 1544    Directory ACL: 0Links: 1   Blockcount: 16Fragment:  Address: 0    Number: 0    Size: 0ctime: 0x474a379c -- Mon Nov 26 11:03:56 2007atime: 0x474a379c -- Mon Nov 26 11:03:56 2007mtime: 0x474a379c -- Mon Nov 26 11:03:56 2007BLOCKS:                (1):20480                TOTAL: 1            
                       
                       

從輸出結果中我們可以看出,這個檔案的大小是 7169 位元組(Size 值,即 ext2_inode 結構中 i_size 域的值),佔用塊數是 16(Blockcount 值,ext2_inode 結構中 i_blocks 域的值,每個