如何恢復 Linux 上刪除的文件

火星人 @ 2014-03-12 , reply:0


  
要想恢復誤刪除的文件,必須清楚數據在磁碟上究竟是如何存儲的,以及如何定位並恢複數據。本文從數據恢復的角度,著重介紹了 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 文件系統中的一些特殊文件,以及如何恢復整個目錄等方面的問題。

除了普通文件之外,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 文件的存儲方法

file_hole.jpg

現在我們可以使用 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);  |-------- XML error:  The previous line is longer than the max of 90 characters ---------|        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     108:            0          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.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  

與刪除之前的結果進行一下比較就會發現,主要區別包括:

  1. 將父目錄的 Links 值減 1。
  2. 設置 dtime 時間,並更新其他時間欄位。
  3. 由於目錄只有在為空時才會被刪除,因此其 Size 值會被設置為 0,Links 欄位也被設置為 0。

通過了解數據塊和索引節點的相應變化可以為恢複目錄提供一個清晰的思路,其具體步驟如下:

  1. 確定刪除目錄所對應的索引節點號。
  2. 按照恢復文件的方法恢復索引節點對應的數據塊。
  3. 遍曆數據塊內容,恢復其中包含的文件和子目錄。
  4. 更新索引節點對應信息。
  5. 修改父目錄的索引節點信息和數據塊中對應目錄項的內容。

實際上,步驟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 個步驟的檢查:

  1. 檢查並修復索引節點、數據塊和大小。比如已刪除子目錄的索引節點大小為0,則會根據所佔用的塊數(每個塊為512位元組)換算出來。
  2. 檢查目錄結構的問題。檢查索引節點的父目錄,如果不存在,就認為父目錄就是根目錄。對於目錄節點,需要檢查是否包含當前目錄和父目錄項。
  3. 檢查目錄結構的連通性。防止出現按照絕對路徑無法訪問文件的情況出現,將這些有問題的文件或目錄放入 lost+found 目錄中。
  4. 檢查並修復引用計數。統計對索引節點的引用計數值。
  5. 檢查並修復塊組信息,包括塊點陣圖、索引節點點陣圖,計算塊組中的空閑塊數、空閑索引節點數等。

現在重新掛載這個文件系統,會發現所有的文件已經全部恢復出來了。





符號鏈接

我們知道,在 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 文件系統的一些考慮。

恢復系統中刪除的文件是一個非常繁瑣的過程,而 e2undel 這個工具可以用來方便地恢復文件系統中已刪除的文件。本文將首先討論 e2undel 的工作原理和用法,並對之進行一些改進。然後討論了文件系統故障、文件系統重建、磁碟物理損壞等情況下應該如何恢複數據。

在本系列文章的前兩部分中,我們介紹了 ext2 文件系統中各種文件在磁碟上的存儲結構,以及如何利用 debugfs 工具的輔助,手工恢復這些文件的詳細過程。

通過這兩部分的學習,我們可以看出恢復系統中刪除的文件是一個非常繁瑣的過程,需要非常仔細地考慮各種情況,並且要保持足夠的細心,才可能把數據準確無誤地恢復出來。稍有差錯,就會造成數據丟失的情況。聰明的讀者肯定會想,如果有一些好工具來自動或輔助完成數據的恢復過程,那簡直就太好了。

幸運的是,已經有人開發了這樣一些工具,來簡化用戶的數據恢復工作,e2undel 就是其中功能最為強大的一個。

自動恢復工具 e2undel

回想一下,在 ext2 文件系統中刪除一個文件時,該文件本身的數據並沒有被真正刪除,實際執行的操作如下:

  1. 在塊點陣圖中將該文件所佔用的數據塊標識為可用狀態。
  2. 在索引節點點陣圖中將該文件所佔用的索引節點標識為可用狀態。
  3. 將該文件索引節點中的硬鏈接數目設置為 0。
  4. 將該文件索引節點中的刪除時間設置為當前時間。
  5. 將父目錄項中該文件對應項中的索引節點號設置為 0,並擴展前一項,使其包含該項所佔用的空間。

而索引節點中的一些關鍵信息(或稱為元數據,包括文件屬主、訪問許可權、文件大小、該文件所佔用的數據塊等)都並沒有發生任何變化。因此只要知道了索引節點號,就完全可以用本系列文章介紹的技術將文件完整地從磁碟上恢復出來了,這正是 e2undel 之類的工具賴以生存的基礎。

然而,由於所刪除的文件在目錄項中對應的項中的索引節點號被清空了,因此我們就無法從索引節點中獲得文件名的信息了。不過,由於文件大小、屬主和刪除時間信息依然能反映文件的原始信息,因此我們可以通過這些信息來幫助判斷所刪除的文件是哪個。

e2undel 是由Oliver Diedrich 開發的一個用來恢復 ext2 文件系統中已刪除文件的工具,它會遍歷所檢測的文件系統的索引節點表,從中找出所有被標記為刪除的索引節點,並按照屬主和刪除時間列出這些文件。另外,e2undel 還提供了文件大小信息,並試圖按照 file 命令的方式來確定文件類型。如果您使用 rm –rf * 之類的命令一次刪除了很多文件,這種信息就可以用來非常方便地幫助確定希望恢復的是哪些文件。在選擇要恢復的文件之後,e2undel 會從磁碟上讀取該文件佔用的數據塊(這些數據塊的信息全部保存在索引節點中),並將其寫入到一個新文件中。下面我們來看一下 e2undel 這個工具的詳細用法。

首先請從 e2undel 的主頁(http://e2undel.sourceforge.net/)上下載最新的源碼包(截止到撰寫本文為止,最新的版本是 0.82),並將其保存到本地文件系統中。不過這個源碼包在最新的 Fedora Core 8 上編譯時可能會有些問題,這是由於 ext2 文件系統內部實現中一些數據結構的變化引起來的,讀者可以下載本文“下載”部分給出的補丁來修正這個問題(請下載這個補丁文件 e2undel-0.82.patch,並將其保存到與源碼包相同的目錄中)。要想編譯 e2undel,系統中還必須安裝 e2fsprogs 和 e2fsprogs-devel 這兩個包,其中有編譯 e2undel 所需要的一些頭文件。Fedora Core 8 中自帶的這兩個包的版本號是 1.39-7:


清單1. 確認系統中已經安裝了 e2fsprogs 和 e2fsprogs-devel

                  # rpm -qa | grep e2fsprogs  e2fsprogs-libs-1.39-7  e2fsprogs-1.39-7  e2fsprogs-devel-1.39-7  

現在就可以開始編譯 e2undel 了:


清單2. 編譯 e2undel

                  # tar -zxf e2undel-0.82.tgz          # patch -p0 < e2undel-0.82.patch   patching file e2undel-0.82/Makefile  patching file e2undel-0.82/e2undel.h  patching file e2undel-0.82.orig/libundel.c    # cd e2undel-0.82  # make all  

編譯之後會生成一個名為 e2undel 的可執行文件,其用法如下:


清單3. e2undel 的用法

                  # ./e2undel   ./e2undel 0.82  usage: ./e2undel -d device -s path [-a] [-t]  usage: ./e2undel -l  '-d': file system where to look for deleted files  '-s': directory where to save undeleted files  '-a': work on all files, not only on those listed in undel log file  '-t': try to determine type of deleted files w/o names, works only with '-a'  '-l': just give a list of valid files in undel log file  

e2undel 實際上並沒有像前面介紹的使用 e2fsck 那樣的方法一樣真正將已經刪除的文件恢復到原來的文件系統中,因為它並不會修改磁碟上 ext2 使用的內部數據結構(例如索引節點、塊點陣圖和索引節點點陣圖)。相反,它僅僅是將所刪除文件的數據恢復出來並將這些數據保存到一個新文件中。因此,-s 參數指定是保存恢復出來的文件的目錄,最好是在另外一個文件系統上,否則可能會覆蓋磁碟上的原有數據。如果指定了 -t 參數,e2undel 會試圖讀取文件的前 1KB 數據,並試圖從中確定該文件的類型,其原理與系統中的 file 命令非常類似,這些信息可以幫助判斷正在恢復的是什麼文件。

下面讓我們來看一個使用 e2undel 恢復文件系統的實例。


清單4. 使用 e2undel 恢復文件的實例

                  # ./e2undel -a -t -d /dev/sda2 -s /tmp/recover/  ./e2undel 0.82  Trying to recover files on /dev/sda2, saving them on /tmp/recover/  

/dev/sda2 opened for read-only access  /dev/sda2 was not cleanly unmounted.  Do you want wo continue (y/n)? y  489600 inodes (489583 free)  977956 blocks of 4096 bytes (941677 free)  last mounted on Fri Dec 28 16:21:50 2007  

reading log file: opening log file: No such file or directory  no entries for /dev/sda2 in log file  searching for deleted inodes on /dev/sda2:  |==================================================|  489600 inodes scanned, 26 deleted files found  

   user name | 1 <12 h | 2 <48 h | 3  <7 d | 4 <30 d | 5  <1 y | 6 older  -------------+---------+---------+---------+---------+---------+--------          root |       0 |       0 |       0 |       2 |       0 |       0         phost |      24 |       0 |       0 |       0 |       0 |       0  Select user name from table or press enter to exit: root  Select time interval (1 to 6) or press enter to exit: 4              

  inode     size  deleted at        name  -----------------------------------------------------------       13    35840  Dec 19 17:43 2007 * ASCII text       14 10485760  Dec 19 17:43 2007 * ASCII text  Select an inode listed above or press enter to go back: 13    35840 bytes written to /tmp/recover//inode-13-ASCII_text  Select an inode listed above or press enter to go back:   

   user name | 1 <12 h | 2 <48 h | 3  <7 d | 4 <30 d | 5  <1 y | 6 older  -------------+---------+---------+---------+---------+---------+--------          root |       0 |       0 |       0 |       2 |       0 |       0         phost |      24 |       0 |       0 |       0 |       0 |       0  Select user name from table or press enter to exit:   #  

e2undel 是一個互動式的命令,命令執行過程中需要輸入的內容已經使用黑體表示出來了。從輸出結果中可以看出,e2undel 一共在這個文件系統中找到了 26 個文件,其中 root 用戶刪除的有兩個。這些文件按照刪除時間的先後順序被劃分到幾類中。索引節點號 13 對應的是一個 ASCII 正文的文本文件,最終被恢復到 /tmp/recover//inode-13-ASCII_text 文件中。查看一下這個文件就會發現,正是我們在本系列前兩部分中刪除的那個 35KB 的測試文件。

利用 libundel 庫完美恢復文件

儘管 e2undel 可以非常方便地簡化恢復文件的過程,但是美中不足的是,其恢復出來的文件的文件名卻丟失了,其原因是文件名是保存在父目錄的目錄項中的,而不是保存在索引節點中的。本系列文章第 2 部分中給出了一種通過遍歷父目錄的目錄項來查找已刪除文件的文件名的方法,但是由於索引節點會被重用,因此通過這種方式恢復出來的文件名也許並不總是正確的。另外,如果目錄結構的非常複雜,就很難確定某個文件的父目錄究竟是哪個,因此查找正確文件名的難度就會變得很大。如果能在刪除文件時記錄下索引節點號和文件名之間的對應關係,這個問題就能完美地解決了。

這個問題在 e2undel 中得到了完美的解決。實際上,所有刪除命令,例如 rm、unlink 都是通過一些底層的系統調用(例如 unlink(2)、rmdir(2))來實現的。基於這一點,e2undel 又利用了Linux 系統中動態鏈接庫載入時提供的一種便利:如果設置了環境變數 LD_PRELOAD,那麼在載入動態鏈接庫時,會優先從 $LD_PRELOAD 指向的動態鏈接庫中查找符號表,然後才會在系統使用 ldconfig 配置的動態鏈接庫中繼續查找符號表。因此,我們可以在自己編寫的庫函數中實現一部分系統調用,並將這個庫優先於系統庫載入,這樣就能欺騙系統使用我們自己定義的系統調用來執行原有的操作。具體到 e2undel 上來說,就是要在調用這些系統調用刪除文件時,記錄下文件名和索引節點號之間的對應關係。

在編譯 e2undel 源代碼之後,還會生成一個庫文件 libundel.so.1.0,其中包含了刪除文件時所使用的一些系統調用的鉤子函數。e2undel官方主頁上下載的源碼包中僅僅包括了對 unlink(2) 和 remove(3) 這兩個系統調用的鉤子函數,但是從 2.6.16 版本的內核開始,引入了一系列新的系統調用,包括 faccessat(2), fchmodat(2), fchownat(2), fstatat(2), futimesat(2), linkat(2), mkdirat(2), mknodat(2), openat(2), readlinkat(2), renameat(2), symlinkat(2), unlinkat(2), mkfifoat(3) 等,儘管這些系統調用目前還沒有成為POSIX標準的一部分,但是相信這個過程不會很久了。目前諸如 Fecora Core 8 之類的系統中自帶的 rm 命令(屬於 coreutils)包已經使用這些新的系統調用進行了改寫,另外本文下載部分中的補丁文件中已經提供了對 rmdir 和 unlinkat 的鉤子函數。部分源代碼如下所示:


清單5. libundel.c 代碼片斷

                  void _init()  {    f = fopen("/var/e2undel/e2undel", "a");    if (!f)      fprintf(stderr, "libundel: can't open log file, undeletion disabled\n");  }  

......  

int rmdir(const char *pathname)  {    int err;    struct stat buf;    char pwd[PATH_MAX];    int (*rmdirp)(char *) = dlsym(RTLD_NEXT, "rmdir");  

  if (NULL != pathname)    {      if (__lxstat(3, pathname, &buf)) buf.st_ino = 0;      if (!realpath(pathname, pwd)) pwd[0] = '\0';    }    err = (*rmdirp)((char *) pathname);    if (err) return err;      /* remove() did not succeed */    if (f)    {      if (!S_ISLNK(buf.st_mode))  /* !!! should we check for other file types? */      {                         /* don't log deleted symlinks */        fprintf(f, "%ld,%ld::%ld::%s\n",                (long) (buf.st_dev & 0xff00) / 256,                (long) buf.st_dev & 0xff,                (long) buf.st_ino, pwd[0] ? pwd : pathname);        fflush(f);      }    }   /* if (f) */      return err;  }   /* rmdir() */  

......  

void _fini()  {    if (f) fclose(f);  }  

_init 和 _fini 這兩個函數分別在打開和關閉動態鏈接庫時執行,分別用來打開和關閉日誌文件。如果某個命令(例如 rm)執行了 rmdir 系統調用,就會被這個庫接管。rmdir 的處理比較簡單,它先搜索到真正的 rmdir 系統調用的符號表,然後使用同樣的參數來執行這個系統調用,如果成功,就將有關索引節點和文件名之類的信息記錄到日誌中(默認是 /var/e2undel/ e2undel)。

這個庫的使用非常簡單,請執行下面的命令:


清單6. libundel 的設置

                  # cp libundel.so.1.0 /usr/local/lib  # cd /usr/local/lib  # ln -s libundel.so.1.0 libundel.so.1  # ln –s libundel.so.1.0 libundel.so  

# ldconfig  # mkdir /var/e2undel  # chmod 711 /var/e2undel  # touch /var/e2undel/e2undel  # chmod 622 /var/e2undel/e2undel  

上面的設置僅僅允許 root 用戶可以恢復文件,如果希望讓普通用戶也能恢復文件,就需要修改對應文件的許可權設置。

現在嘗試以另外一個用戶的身份來刪除些文件:


清單7. 設置libundel之後刪除文件

                  $ export LD_PRELOAD=/usr/local/lib/libundel.so  $ rm -rf e2undel-0.82  

要想記錄所有用戶的刪除文件的操作,可以將 export LD_PRELOAD=/usr/local/lib/libundel.so 這行內容加入到 /etc/profile 文件中。

現在使用 e2undel 來恢復已刪除的文件就變得簡單多了,因為已經可以通過文件名來恢復文件了:


清單8. e2undel 利用 libundel 日誌恢復刪除文件

                  # ./e2undel -a -t -d /dev/sda2 -s /tmp/recover/   ./e2undel 0.82  Trying to recover files on /dev/sda2, saving them on /tmp/recover/  

/dev/sda2 opened for read-only access  /dev/sda2 was not cleanly unmounted.  Do you want wo continue (y/n)? y  489600 inodes (489531 free)  977956 blocks of 4096 bytes (941559 free)  last mounted on Fri Dec 28 20:45:05 2007  

reading log file:   found 24 entries for /dev/sda2 in log file  searching for deleted inodes on /dev/sda2:  |==================================================|  489600 inodes scanned, 26 deleted files found  checking names from log file for deleted files: 24 deleted files with names  

   user name | 1 <12 h | 2 <48 h | 3  <7 d | 4 <30 d | 5  <1 y | 6 older  -------------+---------+---------+---------+---------+---------+--------          root |       0 |       0 |       0 |       2 |       0 |       0         phost |      24 |       0 |       0 |       0 |       0 |       0  Select user name from table or press enter to exit: phost  Select time interval (1 to 6) or press enter to exit: 1              

  inode     size  deleted at        name  -----------------------------------------------------------   310083        0  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82   310113     2792  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/BUGS   310115     3268  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/HISTORY   310116     1349  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/INSTALL   310117     1841  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/INSTALL.de   310118     2175  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/Makefile   310119    12247  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/README   310120     9545  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/README.de   310121    13690  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/apprentice.c   310122    19665  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/ascmagic.c   310123      221  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/common.h   310124     1036  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/compactlog.c   310125    30109  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/e2undel.c   310127     2447  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/e2undel.h   310128     1077  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/file.c   310129     2080  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/file.h   310130     4484  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/find_del.c   310131     2141  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/is_tar.c   310132     2373  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/libundel.c   310133     7655  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/log.c   310134    39600  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/magic.h   310135     4591  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/names.h   310136    13117  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/softmagic.c   310137     5183  Dec 29 17:23 2007 /tmp/test/undel/e2undel-0.82/tar.h  

如果對所有用戶都打開這個功能,由於日誌文件是單向增長的,隨著時間的推移,可能會變得很大,不過 e2undel 中還提供了一個 compactlog 工具來刪除日誌文件中的重複項。

在學習本系列文章介紹的技術之後,利用 e2undel 之類的工具,並使用本系列文章第一部分中提供的補丁,恢復刪除文件就變得非常簡單了。但是在日常使用過程中,大家可能還會碰到一些意外的情況,比如文件系統發生問題,從而無法正常掛載;使用 mke2fs 之類的工具重做了文件系統;甚至磁碟上出現壞道。此時應該如何恢復系統中的文件呢,下面讓我們來逐一看一下如何解決這些問題。





文件系統故障的恢復

回想一下,在超級塊中保存了有關文件系統本身的一些數據,在 ext2 文件系統中,還使用塊組描述符保存了有關塊組的信息;另外,索引節點點陣圖、塊點陣圖中分別保存了索引節點和磁碟上數據塊的使用情況,而文件本身的索引節點信息(即文件的元數據)則保存在索引節點表中。這些數據對於文件系統來說都是至關重要的,它們是存取文件的基礎。如果超級塊和塊組描述符的信息一旦出錯,則會造成文件系統無法正常掛載的情況出現。造成這些信息出錯的原因有:

  • 系統管理員操作失誤。
  • 設備驅動程序或第三方軟體(例如mke2fs之類的)有 bug。
  • 電源意外斷電。
  • 內核有 bug。

如果出現這種問題,可能造成的後果有:

  1. 文件系統無法掛載。
  2. 操作系統掛起。
  3. 即使文件系統能夠成功掛載,在系統重啟時也可能會看到一些錯誤,或者目錄列表中出現亂字元的情況等。

下面讓我們來模擬一個出現這種錯誤的情況。我們知道,超級塊信息就保存在分區中的第一個塊中,現在我們來試驗一下清空這個塊中數據的後果:


清單9. 清空超級塊信息的後果

                  # dd if=/dev/zero of=/dev/sda2 bs=4096 count=1  

# mount /dev/sda2 /tmp/test -t ext2  mount: wrong fs type, bad option, bad superblock on /dev/sda2,         missing codepage or helper program, or other error         In some cases useful info is found in syslog - try         dmesg | tail  or so  

由於無法從磁碟上讀取到有效的超級塊信息,mount 命令已經無法掛載 /dev/sda2 設備上的文件系統了。

為了防止這個問題會造成嚴重的後果,ext2 文件系統會在每個塊組中保存一份超級塊的拷貝。當然,這會造成一定的空間浪費,因此在最新的 ext2 文件系統中,只是在特定的塊組中保存一份超級塊的拷貝。具體來說,是在第 0、1 個塊組和第 3、5、7 的整數次冪個塊組中保存一份超級塊的拷貝,而其他塊組中的空間都可以節省出來了。下面來看一個 20GB 大小的文件系統的實際例子:


清單10. ext2 文件系統中超級塊拷貝的位置

                  # dumpe2fs /dev/sdb6 | grep -i superblock  dumpe2fs 1.39 (29-May-2006)    Primary superblock at 0, Group descriptors at 1-2    Backup superblock at 32768, Group descriptors at 32769-32770    Backup superblock at 98304, Group descriptors at 98305-98306    Backup superblock at 163840, Group descriptors at 163841-163842    Backup superblock at 229376, Group descriptors at 229377-229378    Backup superblock at 294912, Group descriptors at 294913-294914    Backup superblock at 819200, Group descriptors at 819201-819202    Backup superblock at 884736, Group descriptors at 884737-884738    Backup superblock at 1605632, Group descriptors at 1605633-1605634    Backup superblock at 2654208, Group descriptors at 2654209-2654210    Backup superblock at 4096000, Group descriptors at 4096001-4096002  

這是一個 20GB 大的 ext2 文件系統,每個塊組的大小是 32768 個塊,超級塊一共有 11 個拷貝,分別存儲在第 0、1、3、5、7、9、25、27、49、81 和 125 個塊組中。默認情況下,內核只會使用第一個超級塊中的信息來對磁碟進行操作。在出現故障的情況下,就可以使用這些超級塊的備份來恢複數據了。具體說來,有兩種方法:首先 mount 命令可以使用 sb 選項指定備用超級塊信息來掛載文件系統:


清單11. 使用超級塊拷貝掛載文件系統

                  # mount -o sb=131072 /dev/sda2 /tmp/test -t ext2  

需要注意的是,mount 命令中塊大小是以 1024 位元組為單位計算的,而這個文件系統則採用的是 4096 位元組為單位的塊,因此 sb 的值應該是 32768*4=131072。

儘管 mount 命令可以使用備用超級塊來掛載文件系統,但卻無法修復主超級塊的問題,這需要使用 e2fsck 這個工具來完成:


清單12. 利用 e2fsck 工具修復 ext2 文件系統中主超級塊的問題

                  # e2fsck /dev/sda2  e2fsck 1.40.2 (12-Jul-2007)  Couldn't find ext2 superblock, trying backup blocks...  /dev/sda2 was not cleanly unmounted, check forced.  Pass 1: Checking inodes, blocks, and sizes  Pass 2: Checking directory structure  Pass 3: Checking directory connectivity  Pass 4: Checking reference counts  Pass 5: Checking group summary information    /dev/sda2: ***** FILE SYSTEM WAS MODIFIED *****  /dev/sda2: 11/489600 files (9.1% non-contiguous), 17286/977956 blocks    # mount /dev/sda2 /tmp/test -t ext2  

e2fsck 工具可以檢查出主超級塊的問題,然後從其他超級塊拷貝中讀取數據,並使用它來恢復主超級塊中的數據(在 ext2 文件系統中,超級塊信息保存在一個 ext2_super_block 的數據結構中,詳細信息請參考內核源代碼)。修復主超級塊的問題之後,mount 命令就可以成功掛載原來的文件系統了。





重建文件系統的解決辦法

在日常使用過程中,可能碰到的另外一個問題是管理員可能錯誤地執行了某些命令,例如使用mke2fs 重建了文件系統,從而造成數據的丟失。實際上,在 mke2fs 創建文件系統的過程中,並不會真正去清空原有文件系統中存儲的文件的數據,但卻會重新生成超級塊、塊組描述符之類的信息,並清空索引節點點陣圖和塊點陣圖中的數據,最為關鍵的是,它還會清空索引節點表中的數據。因此儘管文件數據依然存儲在磁碟上,但是由於索引節點中存儲的文件元數據已經丟失了,要想完整地恢復原有文件,已經變得非常困難了。

然而,這個問題也並非完全無法解決。在 e2fsprogs 包中還提供了一個名為 e2image 的工具,可以用來將 ext2 文件系統中的元數據保存到一個文件中,下面是一個例子:


清單13. 使用超級塊拷貝掛載文件系統

                  # e2image -r /dev/sda2 sda2.e2image  

這會生成一個與文件系統大小相同的文件,其中包含了文件系統的元數據,包括索引節點中的間接塊數據以及目錄數據。另外,其中所有數據的位置均與磁碟上存儲的位置完全相同,因此可以使用 debugfs、dumpe2fs 之類的工具直接查看:


清單14. 使用 debugfs 查看 e2image 映像文件的信息

                  # debugfs sda2.e2image.raw   debugfs 1.40.2 (12-Jul-2007)  debugfs:  ls -l        2   40755 (2)      0      0    4096 31-Dec-2007 15:56 .        2   40755 (2)      0      0    4096 31-Dec-2007 15:56 ..       11   40700 (2)      0      0   16384 31-Dec-2007 15:54 lost+found       12  100644 (1)      0      0   10485760 31-Dec-2007 15:56 testfile.10M       13  100644 (1)      0      0   35840 31-Dec-2007 15:56 testfile.35K  

為了節省空間,這些映像文件以稀疏文件的形式保存在磁碟上,在一個 4GB 的文件系統中,如果 55 萬個索引節點中已經使用了 1 萬 5 千個,使用 bizp2 壓縮后的文件大概只有 3MB左右。

當然,這些映像文件中並沒有包含實際文件的數據,不過文件數據依然保存在磁碟上,因此只要及時備份相關信息,在發生意外的情況下是有可能恢復大部分數據的。





磁碟壞道情況的處理

隨著磁碟使用的時間越來越長,難免會出現磁碟上出現一些物理故障,比如產生物理壞道。根據物理損壞的嚴重程度,可能會造成文件丟失、文件系統無法載入甚至整個磁碟都無法識別的情況出現。因此要想將損失控制在最小範圍內,除了經常備份數據,在發現問題的第一時間採取及時地應對措施也非常重要。

物理故障一旦出現,極有可能會有加劇趨勢,因此應該在恢複數據的同時,盡量減少對磁碟的使用,dd 命令可以用來創建磁碟的完美映像。應該使用的命令如下:


清單15. 使用 dd 命令創建磁碟映像

                  # dd if=/dev/sdb of=/images/sdb.image bs=4096 conv=noerror,sync  

noerror 參數告訴 dd 在碰到讀寫錯(可能是由於壞道引起的)時繼續向下操作,而不是停止退出。sync 參數說明對於從源設備無法正常讀取的塊,就使用NULL填充。默認情況下,dd 使用 512 位元組作為一個塊的單位來讀寫 I/O 設備,指定 bs 為 4096 位元組可以在一定程度上加速 I/O 操作,但同時也會造成一個問題:如果所讀取的這個 4096 位元組為單位的數據塊中某一部分出現問題,則整個 4096 位元組的就全部被清空了,這樣會造成數據的丟失。為了解決這種問題,我們可以使用 dd_rescue 這個工具(可以從 http://www.garloff.de/kurt/linux/ddrescue/ 上下載),其用法如下:


清單16. 使用 dd_rescue 命令創建磁碟映像

                  # dd_rescue /dev/sdb /images/sdb.image –b 65536 –B 512  

與 dd 相比,dd_rescue 強大之處在於在碰到錯誤時,可以以更小的數據塊為單位重新讀取這段數據,從而確保能夠讀出盡量多的數據。上面命令中的參數指明正常操作時以 64KB 為單位讀取磁碟數據,一旦出錯,則以 512 位元組為單位重新讀取這段數據,直至整個硬碟被完整讀出為止。

獲得磁碟映像之後,就可以將其當作普通磁碟一樣進行操作了。應用本系列文章中介紹的技術,應該能從中恢復出儘可能多的數據。當然,對於那些剛好處於坏道位置的數據,那就實在回天乏力了。





恢復文件策略

截至到現在,本系列文章中介紹的都是在刪除文件或出現意外情況之後如何恢復文件,實際上,對於保證數據可用性的目的來講,這些方法都無非是亡羊補牢而已。制定恰當地數據備份策略,並及時備份重要數據才是真正的解決之道。

不過即使有良好的數據備份策略,也難免會出現有部分數據沒有備份的情況。因此,一旦出現誤刪文件的情況,應該立即執行相應的對策,防止文件數據被覆蓋:

  • 斷開所有對文件系統的訪問。fuser 命令可以用來幫助查看和殺死相關進程,詳細用法請參看 fuser 的手冊。
  • 如果業務無法停頓,就將文件系統以只讀方式重新載入,命令格式為:mount -r -n -o remount mountpoint
  • 應用本系列文章介紹的技術恢復文件。

當然,在進行數據備份的同時,也需要考慮本文中介紹的一些技術本身的要求,例如 e2image映像文件、e2undel 的日誌文件等,都非常重要,值得及時備份。





小結

本文介紹了一個功能非常強大的工具 e2undel,可以用來方便地恢復已刪除的文件。然後討論了文件系統故障、文件系統重建、磁碟物理損壞等情況下應該如何恢複數據。隨著文件系統的不斷發展,Linux 上常用的文件系統也越來越多,例如 ext3/ext4/reiserfs/jfs 等,這些文件系統上刪除的文件能否成功恢復呢?有哪些工具可以用來輔助恢復文件呢?本系列後續文章將繼續探討這個問題。

作為 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 blocks                  Journal 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, \  (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, \  (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-129039                  TOTAL: 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 文件系統中刪除的文件。本系列的後續文章將繼續探討在其他文件系統上如何恢復已刪除的文件,並著手設計更加通用的文件備份方法。

為了支持更大的文件系統,ext4 對 ext3 的現有實現進行了一系列擴充,使用 48 位的塊號來增大塊號定址範圍,並採用 extent 的設計來簡化對數據塊的索引,這勢必會影響到磁碟數據結構的變化,以及刪除文件的恢復。本文將逐一介紹 ext4 在對大文件系統支持方面所採用的全新設計,並探討 ext4 文件系統中文件的刪除和恢復的相關技術。

ext3 自從誕生之日起,就由於其可靠性好、特性豐富、性能高、版本間兼容性好等優勢而迅速成為 Linux 上非常流行的文件系統,諸如 Redhat 等發行版都將 ext3 作為默認的文件系統格式。為了盡量保持與 ext2 文件系統實現更好的兼容性,ext3 在設計時採用了很多保守的做法,這些保守的設計為 ext3 贏得了穩定、健壯的聲譽,迅速得到了 Linux 用戶(尤其是原有的 ext2 文件系統的用戶)的青睞,但同時這也限制了它的可擴展能力,無法支持特別大的文件系統。

隨著硬碟存儲容量越來越大(硬碟容量每年幾乎都會翻一倍,現在市面上已經有 1TB 的硬碟出售,很快桌面用戶也可以享用這麼大容量的存儲空間了),企業應用所需要和產生的數據越來越多(Lawrence Livermore National Labs 使用的 BlueGene/L 系統上所使用的數據早已超過了 1PB),以及在線重新調整大小特性的支持,ext3 所面臨的可擴充性問題和性能方面的壓力也越來越大。在 ext3 文件系統中,如果使用 4KB 大小的數據塊,所支持的最大文件系統上限為16TB,這是由於它使用了 32 位的塊號所決定的(232 * 212 B = 244 B = 16 TB)。為了解決這些限制,從 2006 年 8 月開始,陸續有很多為 ext3 設計的補丁發布出來,這些補丁主要是擴充了兩個特性:針對大文件系統支持的設計和 extent 映射技術。不過要想支持更大的文件系統,就必須對磁碟上的存儲格式進行修改,這會破壞向前兼容性。因此為了為龐大的 ext3 用戶群維護更好的穩定性,設計人員決定從 ext3 中另闢一支,設計下一代 Linux 上的文件系統,即 ext4。

ext4 的主要目標是解決 ext3 所面臨的可擴展性、性能和可靠性問題。從 2.6.19 版本的內核開始,ext4 已經正式進入內核源代碼中,不過它被標記為正在開發過程中,即 ext4dev。本文將介紹 ext4 為了支持更好的可擴展性方面所採用的設計,並探討由此而引起的磁碟數據格式的變化,以及對恢復刪除文件所帶來的影響。

可擴展性

為了支持更大的文件系統,ext4 決定採用 48 位的塊號取代 ext3 原來的 32 位塊號,並採用 extent 映射來取代 ext3 所採用的間接數據塊映射的方法。這樣既可以增大文件系統的容量,又可以改進大文件的訪問效率。在使用 4KB 大小的數據塊時,ext4 可以支持最大 248 * 212 = 260 B(1 EB)的文件系統。之所以採用 48 位的塊號而不是直接將其擴展到 64 位是因為,ext4 的開發者認為 1 EB 大小的文件系統對未來很多年都足夠了(實際上,按照目前的速度,要對 1 EB 大小的文件系統執行一次完整的 fsck 檢查,大約需要 119 年的時間),與其耗費心機去完全支持 64 位的文件系統,還不如先花些精力來解決更加棘手的可靠性問題。

將塊號從 32 位修改為 48 位之後,存儲元數據的結構都必須相應地發生變化,主要包括超級塊、組描述符和日誌。下面給出了 ext4 中所使用的新結構的部分代碼。


清單1. ext4_super_block 結構定義

                  520 /*   521  * Structure of the super block   522  */   523 struct ext4_super_block {   524 /*00*/  __le32  s_inodes_count;         /* Inodes count */   525         __le32  s_blocks_count;         /* Blocks count */   526         __le32  s_r_blocks_count;       /* Reserved blocks count */   527         __le32  s_free_blocks_count;    /* Free blocks count */   528 /*10*/  __le32  s_free_inodes_count;    /* Free inodes count */   529         __le32  s_first_data_block;     /* First Data Block */   530         __le32  s_log_block_size;       /* Block size */  …  594         /* 64bit support valid if EXT4_FEATURE_COMPAT_64BIT */   595 /*150*/ __le32  s_blocks_count_hi;      /* Blocks count */   596         __le32  s_r_blocks_count_hi;    /* Reserved blocks count */   597         __le32  s_free_blocks_count_hi; /* Free blocks count */  …   606 };  

在 ext4_super_block 結構中,增加了 3 個與此相關的欄位:s_blocks_count_hi、s_r_blocks_count_hi、s_free_blocks_count_hi,它們分別表示 s_blocks_count、s_r_blocks_count、s_free_blocks_count 高 32 位的值,將它們擴充到 64 位。


清單2. ext4_group_desc 結構定義

                  121 /*   122  * Structure of a blocks group descriptor   123  */   124 struct ext4_group_desc   125 {   126         __le32  bg_block_bitmap;                /* Blocks bitmap block */   127         __le32  bg_inode_bitmap;                /* Inodes bitmap block */   128         __le32  bg_inode_table;         /* Inodes table block */   129         __le16  bg_free_blocks_count;   /* Free blocks count */   130         __le16  bg_free_inodes_count;   /* Free inodes count */   131         __le16  bg_used_dirs_count;     /* Directories count */   132         __u16   bg_flags;   133         __u32   bg_reserved[3];   134         __le32  bg_block_bitmap_hi;     /* Blocks bitmap block MSB */   135         __le32  bg_inode_bitmap_hi;     /* Inodes bitmap block MSB */   136         __le32  bg_inode_table_hi;      /* Inodes table block MSB */   137 };  

類似地,在 ext4_group_desc 中引入了另外 3 個欄位:bg_block_bitmap_hi、bg_inode_bitmap_hi、bg_inode_table_hi,分別表示 bg_block_bitmap、bg_inode_bitmap、bg_inode_table 的高 32 位。

另外,由於日誌中要記錄所修改數據塊的塊號,因此 JBD也需要相應地支持 48 位的塊號。同樣是為了為 ext3 廣大的用戶群維護更好的穩定性,JBD2 也從 JBD 中分離出來,詳細實現請參看內核源代碼。

採用 48 位塊號取代原有的 32 位塊號之後,文件系統的最大值還受文件系統中最多塊數的制約,這是由於 ext3 原來採用的結構決定的。回想一下,對於 ext3 類型的分區來說,在每個分區的開頭,都有一個引導塊,用來保存引導信息;文件系統的數據一般從第 2 個數據塊開始(更確切地說,文件系統數據都是從 1KB 之後開始的,對於 1024 位元組大小的數據塊來說,就是從第 2 個數據塊開始;對於超過 1KB 大小的數據塊,引導塊與後面的超級塊等信息共同保存在第 1 個數據塊中,超級塊從 1KB 之後的位置開始)。為了管理方便,文件系統將剩餘磁碟劃分為一個個塊組。塊組前面存儲了超級塊、塊組描述符、數據塊點陣圖、索引節點點陣圖、索引節點表,然後才是數據塊。通過有效的管理,ext2/ext3 可以盡量將文件的數據放入同一個塊組中,從而實現文件數據在磁碟上的最大連續性。

在 ext3 中,為了安全性方面的考慮,所有的塊描述符信息全部被保存到第一個塊組中,因此以預設的 128MB (227 B)大小的塊組為例,最多能夠支持 227 / 32 = 222 個塊組,最大支持的文件系統大小為 222 * 227 = 249 B= 512 TB。而ext4_group_desc 目前的大小為 44 位元組,以後會擴充到 64 位元組,所能夠支持的文件系統最大隻有 256 TB。

為了解決這個問題,ext4 中採用了元塊組(metablock group)的概念。所謂元塊組就是指塊組描述符可以存儲在一個數據塊中的一些連續塊組。仍然以 128MB 的塊組(數據塊為 4KB)為例,ext4 中每個元塊組可以包括 4096 / 64 = 64 個塊組,即每個元塊組的大小是 64 * 128 MB = 8 GB。

採用元塊組的概念之後,每個元塊組中的塊組描述符都變成定長的,這對於文件系統的擴展非常有利。原來在 ext3 中,要想擴大文件系統的大小,只能在第一個塊組中增加更多塊描述符,通常這都需要重新格式化文件系統,無法實現在線擴容;另外一種可能的解決方案是為塊組描述符預留一部分空間,在增加數據塊時,使用這部分空間來存儲對應的塊組描述符;但是這樣也會受到前面介紹的最大容量的限制。而採用元塊組概念之後,如果需要擴充文件系統的大小,可以在現有數據塊之後新添加磁碟數據塊,並將這些數據塊也按照元塊組的方式進行管理即可,這樣就可以突破文件系統大小原有的限制了。當然,為了使用這些新增加的空間,在 superblock 結構中需要增加一些欄位來記錄相關信息。(ext4_super_block 結構中增加了一個 s_first_meta_bg 欄位用來引用第一個元塊組的位置,這樣還可以解決原有塊組和新的元塊組共存的問題。)下圖給出了 ext3 為塊組描述符預留空間和在 ext4 中採用元塊組后的磁碟布局。


圖 1. ext3 與 ext4 磁碟布局對比





extent

ext2/ext3 文件系統與大部分經典的 UNIX/Linux 文件系統一樣,都使用了直接、間接、二級間接和三級間接塊的形式來定位磁碟中的數據塊。對於小文件或稀疏文件來說,這非常有效(以 4KB 大小的數據塊為例,小於 48KB 的文件只需要通過索引節點中 i_block 數組的前 12 個元素一次定位即可),但是對於大文件來說,需要經過幾級間接索引,這會導致在這些文件系統上大文件的性能較差。

測試表明,在生產環境中,數據不連續的情況不會超過10%。因此,在 ext4 中引入了 extent 的概念來表示文件數據所在的位置。所謂 extent 就是描述保存文件數據使用的連續物理塊的一段範圍。每個 extent 都是一個 ext4_extent 類型的結構,大小為 12 位元組。定義如下所示:


清單3. ext4 文件系統中有關 extent 的結構定義

                  69 /*   70  * This is the extent on-disk structure.   71  * It's used at the bottom of the tree.   72  */   73 struct ext4_extent {   74         __le32  ee_block;       /* first logical block extent covers */   75         __le16  ee_len;         /* number of blocks covered by extent */   76         __le16  ee_start_hi;    /* high 16 bits of physical block */   77         __le32  ee_start;       /* low 32 bits of physical block */   78 };   79    80 /*   81  * This is index on-disk structure.   82  * It's used at all the levels except the bottom.   83  */   84 struct ext4_extent_idx {   85         __le32  ei_block;       /* index covers logical blocks from 'block' */   86         __le32  ei_leaf;        /* pointer to the physical block of the next *   87                                  * level. leaf or next index could be there */   88         __le16  ei_leaf_hi;     /* high 16 bits of physical block */   89         __u16   ei_unused;   90 };   91    92 /*   93  * Each block (leaves and indexes), even inode-stored has header.   94  */   95 struct ext4_extent_header {   96         __le16  eh_magic;       /* probably will support different formats */   97         __le16  eh_entries;     /* number of valid entries */   98         __le16  eh_max;         /* capacity of store in entries */   99         __le16  eh_depth;       /* has tree real underlying blocks? */  100         __le32  eh_generation;  /* generation of the tree */  101 };  102   103 #define EXT4_EXT_MAGIC          cpu_to_le16(0xf30a)  

每個 ext4_extent 結構可以表示該文件從 ee_block 開始的 ee_len 個數據塊,它們在磁碟上的位置是從 ee_start_hi<<32 + ee_start 開始,到 ee_start_hi<<32 + ee_start + ee_len – 1 結束,全部都是連續的。儘管 ee_len 是一個 16 位的無符號整數,但是其最高位被在預分配特性中用來標識這個 extent 是否被初始化過了,因此可以一個 extent 可以表示 215 個連續的數據塊,如果採用 4KB 大小的數據塊,就相當於 128MB。

如果文件大小超過了一個 ext4_extent 結構能夠表示的範圍,或者其中有不連續的數據塊,就需要使用多個 ext4_extent 結構來表示了。為了解決這個問題,ext4 文件系統的設計者們採用了一棵 extent 樹結構,它是一棵高度固定的樹,其布局如下圖所示:


圖 2. ext4 中 extent 樹的布局結構

在 extent 樹中,節點一共有兩類:葉子節點和索引節點。保存文件數據的磁碟塊信息全部記錄在葉子節點中;而索引節點中則存儲了葉子節點的位置和相對順序。不管是葉子節點還是索引節點,最開始的 12 個位元組總是一個 ext4_extent_header 結構,用來標識該數據塊中有效項(ext4_extent 或 ext4_extent_idx 結構)的個數(eh_entries 域的值),其中 eh_depth 域用來表示它在 extent 樹中的位置:對於葉子節點來說,該值為 0,之上每層索引節點依次加 1。extent 樹的根節點保存在索引節點結構中的 i_block 域中,我們知道它是一個大小為 60 位元組的數組,最多可以保存一個 ext4_extent_header 結構以及 4 個 ext4_extent 結構。對於小文件來說,只需要一次定址就可以獲得保存文件數據塊的位置;而超出此限制的文件(例如很大的文件、碎片非常多的文件以及稀疏文件)只能通過遍歷 extent 樹來獲得數據塊的位置。





索引節點

索引節點是 ext2/ext3/ext4 文件系統中最為基本的一個概念,它是文件語義與數據之間關聯的橋樑。為了最大程度地實現向後兼容性,ext4 盡量保持索引節點不會發生太大變化。ext4_inode 結構定義如下所示:


清單4. ext4_inode 結構定義

                  284 /*   285  * Structure of an inode on the disk   286  */   287 struct ext4_inode {   288         __le16  i_mode;         /* File mode */   289         __le16  i_uid;          /* Low 16 bits of Owner Uid */   290         __le32  i_size;         /* Size in bytes */   291         __le32  i_atime;        /* Access time */   292         __le32  i_ctime;        /* Inode Change time */   293         __le32  i_mtime;        /* Modification time */   294         __le32  i_dtime;        /* Deletion Time */   295         __le16  i_gid;          /* Low 16 bits of Group Id */   296         __le16  i_links_count;  /* Links count */   297         __le32  i_blocks;       /* Blocks count */   298         __le32  i_flags;        /* File flags */  …   310         __le32  i_block[EXT4_N_BLOCKS];/* Pointers to blocks */   311         __le32  i_generation;   /* File version (for NFS) */   312         __le32  i_file_acl;     /* File ACL */   313         __le32  i_dir_acl;      /* Directory ACL */   314         __le32  i_faddr;        /* Fragment address */  …   339         __le16  i_extra_isize;   340         __le16  i_pad1;   341         __le32  i_ctime_extra;  /* extra Change time      (nsec << 2 | epoch) */   342         __le32  i_mtime_extra;  /* extra Modification time(nsec << 2 | epoch) */   343         __le32  i_atime_extra;  /* extra Access time      (nsec << 2 | epoch) */   344         __le32  i_crtime;       /* File Creation time */   345         __le32  i_crtime_extra; /* extra FileCreationtime (nsec << 2 | epoch) */   346 };  

與 ext3 文件系統中使用的 ext3_inode 結構對比一下可知,索引節點結構並沒有發生太大變化,不同之處在於最後添加了 5 個與時間有關的欄位,這是為了提高時間戳的精度。在 ext2/ext3 文件系統中,時間戳的精度只能達到秒級。隨著硬體性能的提升,這種精度已經無法區分在同一秒中創建的文件的時間戳差異,這對於對精度要求很高的程序來說是無法接受的。在 ext4 文件系統中,通過擴充索引節點結構解決了這個問題,可以實現納秒級的精度。最後兩個新增欄位 i_crtime 和 i_crtime_extra 用來表示文件的創建時間,這可以用來滿足某些應用程序的需求。

前面已經介紹過,儘管索引節點中的 i_block 欄位保持不變,但是由於 extent 概念的引入,對於這個數組的使用方式已經改變了,其前 3 個元素一定是一個 ext4_extent_header 結構,後續每 3 個元素可能是一個 ext4_extent 或 ext4_extent_idx 結構,這取決於所表示的文件的大小。這種設計可以有效地表示連續存放的大文件,但是對於包含碎片非常多的文件或者稀疏文件來說,就不是那麼有效了。為了解決這個問題,ext4 的設計者們正在討論設計一種新型的 extent 來表示這種特殊文件,它將在葉子節點中採用類似於 ext3 所採用的間接索引塊的形式來保存為該文件分配的數據塊位置。該類型的 ext4_extent_header 結構中的 eh_magic 欄位將採用一個新值,以便與目前的 extent 區別開來。

採用這種結構的索引節點還存在一個問題:我們知道,在 ext3 中 i_blocks 是以扇區(即 512 位元組)為單位的,因此單個文件的最大限制是 232 * 512 B = 2 TB。為了支持更大的文件,ext4 的 i_blocks 可以以數據塊大小為單位(這需要 HUGE_FILE 特性的支持),因此文件上限可以擴充到 16TB(數據塊大小為 4KB)。同時為了避免需要對整個文件系統都需要進行類似轉換,還引入了一個 EXT4_HUGE_FILE_FL 標誌,i_flags 中不包含這個標誌的索引節點的 i_blocks 依然以 512 位元組為單位。當文件所佔用的磁碟空間大小增大到不能夠用以512位元組為單位的i_blocks來表示時,ext4自動激活EXT4_HUGE_FILE_FL標誌,以數據塊為單位重新計算i_blocks的值。該轉換是自動進行的,對用戶透明。





目錄項

ext4 文件系統中使用的目錄項與 ext2/ext3 並沒有太大的區別。所使用的結構定義如下所示:


清單5. 目錄項結構定義

                  737 /*   738  * Structure of a directory entry   739  */   740 #define EXT4_NAME_LEN 255   741    742 struct ext4_dir_entry {   743         __le32  inode;                  /* Inode number */   744         __le16  rec_len;                /* Directory entry length */   745         __le16  name_len;               /* Name length */   746         char    name[EXT4_NAME_LEN];    /* File name */   747 };         748            749 /*         750  * The new version of the directory entry.  Since EXT4 structures are   751  * stored in intel byte order, and the name_len field could never be   752  * bigger than 255 chars, it's safe to reclaim the extra byte for the   753  * file_type field.   754  */   755 struct ext4_dir_entry_2 {   756         __le32  inode;                  /* Inode number */   757         __le16  rec_len;                /* Directory entry length */   758         __u8    name_len;               /* Name length */   759         __u8    file_type;   760         char    name[EXT4_NAME_LEN];    /* File name */   761 };  

與 ext2/ext3 類似,當目錄項被刪除時,也會將該目錄項的空間合併到上一個目錄項中。與 ext2/ext3 不同的地方在於,在刪除目錄項時,該目錄項中的索引節點號並沒有被清空,而是得以保留了下來,這使得在恢復刪除文件時,文件名就可以通過查找目錄項中匹配的索引節點號得以正確恢復。詳細數據如下所示:


清單6. 刪除文件前後目錄項的變化

                  [root@vmfc8 ext4]# ./read_dir_entry root.block.547.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          16           5           1  hello      60:           13          32          12           1  testfile.35K      80:           14          12           4           1  hole      92:           15        4004           4           1  home  [root@vmfc8 ext4]# ./read_dir_entry root.block.547.deleted 4096    offset | inode number | rec_len | name_len | file_type | name  =================================================================       0:            2          12           1           2  .      12:            2          12           2           2  ..      24:           11          36          10           2  lost+found      44:           12          16           5           1  hello      60:           13          32          12           1  testfile.35K      80:           14          12           4           1  hole      92:           15        4004           4           1  home  

上面給出了保存根目錄的數據塊(547)在刪除 hello 文件前後的變化,從中我們可以看出,唯一的區別在於 hello 所使用的 16 個位元組的空間後來被合併到 lost+found 目錄項所使用的空間中了,而索引節點號等信息都得以完整地保留了下來。清單中使用的 read_dir_entry 程序用來顯示目錄項中的數據,其源碼可以在本文下載部分中獲得。有關如何抓取保存目錄數據的數據塊的方法,請參看本系列文章第 2 部分的介紹。

在 ext2/3 文件系統中,一個目錄下面最多可以包含 32,000 個子目錄,這對於大型的企業應用來說顯然是不夠的。ext4 決定將其上限擴充到可以支持任意多個子目錄。然而對於這種鏈表式的存儲結構來說,目錄項的查找和刪除需要遍歷整個目錄的所有目錄項,效率顯然是相當低的。實際上,從 ext2 開始,文件系統的設計者引入了一棵 H-樹來對目錄項的 hash 值進行索引,速度可以提高 50 - 100 倍。相關內容已經超出了本文的範圍,感興趣的讀者可自行參考 Linux 內核源代碼中的相關實現。





ext4 文件系統的使用

目前,ext4 文件系統仍然處於非常活躍的狀態,因此內核在相應的地方都加上了 DEV 標誌。在編譯內核時,需要在內核的 .config 文件中啟用 EXT4DEV_FS 選項才能編譯出最終使用的內核模塊 ext4dev.ko。

由於 ext4 內部採用的關鍵數據結構與 ext3 並沒有什麼關鍵區別,因此在創建文件系統時依然是使用 mkfs.ext3 命令,如下所示:


清單7. 創建 ext4 文件系統,目前與創建 ext3 文件系統沒什麼兩樣

                  [root@vmfc8 ~]# mkfs.ext3 /dev/sda3  

為了保持向前兼容性,現有的 ext3 文件系統也可以當作 ext4 文件系統進行載入,命令如下所示:

清單8. 掛載 ext4 文件系統

[root@vmfc8 ~]# mount -t ext4dev -o extents /dev/sda3 /tmp/test

-o extents 選項就是指定要啟用 extent 特性。如果不在這個文件系統中執行任何寫入操作,以後這個文件系統也依然可以按照 ext3 或 ext4 格式正常掛載。但是一旦在這個文件系統中寫入文件之後,文件系統所使用的特性中就包含了 extent 特性,因此以後再也不能按照 ext3 格式進行掛載了,如下所示:


清單9. 寫入文件前後 ext4 文件系統特性的變化據

                  [root@vmfc8 ext4]# umount /tmp/test; mount -t ext4dev -o extents /dev/sda3 /tmp/test; \  dumpe2fs /dev/sda3 > sda3.ext4_1  [root@vmfc8 ext4]# umount /tmp/test; mount -t ext4dev -o extents /dev/sda3 /tmp/test; \  echo hello > /tmp/test/hello; dumpe2fs /dev/sda3 > sda3.ext4_2  [root@vmfc8 ext4]# diff sda3.ext4_1 sda3.ext4_2  6c6  < Filesystem features:      has_journal resize_inode dir_index filetype \  needs_recovery sparse_super large_file  ---  > Filesystem features:      has_journal resize_inode dir_index filetype \  needs_recovery extents sparse_super large_file  …  [root@vmfc8 ext4]# umount /tmp/test; mount -t ext3 /dev/sda3 /tmp/test  mount: wrong fs type, bad option, bad superblock on /dev/sda3,         missing codepage or helper program, or other error         In some cases useful info is found in syslog - try         dmesg | tail  or so  





e2fsprogs 工具的支持

在本系列前面的文章中,我們已經初步體驗了 e2fsprogs 包中提供的諸如 debugfs、dumpe2fs 之類的工具對於深入理解文件系統和磁碟數據來說是如何方便。作為一種新生的文件系統,ext4 文件系統要想得到廣泛應用,相關工具的支持也非常重要。從 1.39 版本開始,e2fsprogs 已經逐漸開始加入對 ext4 文件系統的支持,例如創建文件系統使用的 mkfs.ext3 命令以後會被一個新的命令 mkfs.ext4 所取代。但是截止到本文撰寫時為止,e2fsprogs 的最新版本(1.40.7)對於 ext4 的支持尚不完善,下面的例子給出了 debugfs 查看 hello 文件時的結果:


清單10. debugfs 命令對 ext4 文件系統的支持尚不完善

                  [root@vmfc8 ext4]# echo "hello world" > /tmp/test/hello  [root@vmfc8 ext4]# debugfs /dev/sda3  debugfs 1.40.2 (12-Jul-2007)  debugfs:  stat hello  Inode: 12   Type: regular    Mode:  0644   Flags: 0x80000   Generation: 827135866  User:     0   Group:     0   Size: 12  File ACL: 0    Directory ACL: 0  Links: 1   Blockcount: 8  Fragment:  Address: 0    Number: 0    Size: 0  ctime: 0x47ced460 -- Thu Mar  6 01:12:00 2008  atime: 0x47ced460 -- Thu Mar  6 01:12:00 2008  mtime: 0x47ced460 -- Thu Mar  6 01:12:00 2008  BLOCKS:                  (0):127754, (1):4, (4):1, (5):28672  TOTAL: 4  

從上面的輸出結果中我們可以看出,儘管這個索引節點的 i_flags 欄位值為 0x80000,表示使用 extent 方式來存儲數據,而不是原有的直接/間接索引模式來存儲數據(此時 i_flags 欄位值為 0),但是對 i_block 數組中內容的顯示卻依然沿用了原有的模式。如果文件佔用多個 extent 進行存儲,會發現 debugfs 依然嘗試將 i_block[12]、i_block[13]、i_block[14] 分別作為一級、二級和三級間接索引使用,顯然從中讀出的數據也是毫無意義的。

索引節點中使用的 i_flags 值是在內核源代碼的 /include/linux/ext4_fs.h 中定義的,如下所示:


清單11. i_flags 值定義節選

                  #define EXT4_EXTENTS_FL                 0x00080000 /* Inode uses extents */  





ext4 文件系統中文件的刪除與恢復

在 ext4 文件系統中刪除文件時,所執行的操作與在 ext2/ext3 文件系統中非常類似,也不會真正修改存儲文件數據所使用的磁碟數據塊的內容,而是僅僅刪除或修改了相關的元數據信息,使文件數據無法正常索引,從而實現刪除文件的目的。因此,在 ext4 文件系統中恢復刪除文件也完全是可能的。

前文中已經介紹過,在 ext4 文件系統中刪除文件時,並沒有將目錄項中的索引節點號清空,因此通過遍歷目錄項的方式完全可以完整地恢復出文件名來。

對於文件數據來說,實際數據塊中的數據在文件刪除前後也沒有任何變化,這可以利用本系列文章第一部分中介紹的直接比較數據塊的方法進行驗證。然而由於 extent 的引入,在 ext4 中刪除文件與 ext3 也有所區別。下面讓我們通過一個實例來驗證一下。

在下面的例子中,我們要創建一個非常特殊的文件,它每 7KB 之後的都是一個數字(7 的倍數),其他地方數據全部為 0。


清單12. 創建測試文件

                  [root@vmfc8 ext4]# cat -n create_extents.sh        1  #!/bin/bash       2       3  if [ $# -ne 2 ]       4  then       5          echo "$0 [filename] [size in kb]"       6          exit 1       7  fi       8       9  filename=$1      10  size=$2      11  i=0      12      13  while [ $i -lt $size ]      14  do      15          i=`expr $i + 7`      16          echo -n "$i" | dd of=$1 bs=1024 seek=$i      17  dones  [root@vmfc8 ext4]# ./create_extents.sh /tmp/test/sparsefile.70K 70  [root@vmfc8 ext4]# ls -li /tmp/test/sparsefile.70K   13 -rw-r--r-- 1 root root 71682 2008-03-06 10:49 /tmp/test/sparsefile.70K  [root@vmfc8 ext4]# hexdump -C /tmp/test/sparsefile.70K   00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  *  00001c00  37 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |7...............|  00001c10  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  *  00003800  31 34 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |14..............|  00003810  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  *  00005400  32 31 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |21..............|  00005410  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  *  00007000  32 38 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |28..............|  00007010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  *  00008c00  33 35 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |35..............|  00008c10  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  *  0000a800  34 32 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |42..............|  0000a810  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  *  0000c400  34 39 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |49..............|  0000c410  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  *  0000e000  35 36 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |56..............|  0000e010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  *  0000fc00  36 33 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |63..............|  0000fc10  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  *  00011800  37 30                                             |70|  00011802  

之所以要使用這個文件當作測試文件,完全為了迴避 extent 的優點,否則在 4KB 大小數據塊的 ext4 文件系統中,一個 extent 就可以表示 128MB 的空間,因此要想測試 extent 樹的變化情況就必須創建非常大的文件才行。

由於 debugfs 對 ext4 的支持尚不完善,我們自己編寫了一個小程序(list_extents)來遍歷 extent 樹的內容,並顯示索引節點和葉子節點的數據塊的位置。該程序的源代碼可以在本文下載部分中獲得,其用法如下:


清單13. 查看測試文件使用的 extent 樹信息

                  [root@vmfc8 ext4]# ./list_extents /dev/sda3 13  root node: depth of the tree: 1, 1 entries in root level   idx: logical block: 1, block: 20491  - logical block: 1 - 1, physical block: 20481 - 20481  - logical block: 3 - 3, physical block: 20483 - 20483  - logical block: 5 - 5, physical block: 20485 - 20485  - logical block: 7 - 8, physical block: 20487 - 20488  - logical block: 10 - 10, physical block: 20490 - 20490  - logical block: 12 - 12, physical block: 20492 - 20492  - logical block: 14 - 15, physical block: 20494 - 20495  - logical block: 17 - 17, physical block: 20497 - 20497  

list_extents 程序會對指定磁碟進行搜索,從其中的索引節點表中尋找搜索指定的索引節點號(13)所對應的項,並將其 i_block 數組當作一棵 extent 樹進行遍歷。從輸出結果中我們可以看出,這棵 extent 樹包括 1 個索引節點和 1個包含了8個 ext4_extent 結構的葉子節點,其中索引節點保存 i_block 數組中,而葉子節點則保存在20491 這個數據塊中。下面讓我們來看一下該文件的索引節點在刪除文件前後的變化:


清單14. ext4 文件系統中刪除文件前後文件索引節點的變化

                  [root@vmfc8 ext4]# echo "stat <13>" | debugfs /dev/sda3  debugfs 1.40.2 (12-Jul-2007)  debugfs:  Inode: 13   Type: regular    Mode:  0644   Flags: 0x80000     Generation: 2866260918  User:     0   Group:     0   Size: 71682  File ACL: 0    Directory ACL: 0  Links: 1   Blockcount: 88  Fragment:  Address: 0    Number: 0    Size: 0  ctime: 0x47cf5bb1 -- Thu Mar  6 10:49:21 2008  atime: 0x47cf5bb0 -- Thu Mar  6 10:49:20 2008  mtime: 0x47cf5bb1 -- Thu Mar  6 10:49:21 2008  BLOCKS:  (0):127754, (1):65540, (3):1, (4):20491, (6):3, (7):1,   (8):20483, (9):5, (10):1, (11):20485, (IND):7, (12):32775,   (13):98311, (14):163847, (15):229383, (DIND):2, (IND):32770,   (IND):98306, (IND):163842, (IND):229378, (TIND):20487, (DIND):14386  TOTAL: 22    [root@vmfc8 ext4]# rm -f /tmp/test/sparsefile.70K   [root@vmfc8 ext4]# sync  [root@vmfc8 ext4]# echo "stat <13>" | debugfs /dev/sda3  debugfs 1.40.2 (12-Jul-2007)  debugfs:  Inode: 13   Type: regular    Mode:  0644   Flags: 0x80000     Generation: 2866260918  User:     0   Group:     0   Size: 0  File ACL: 0    Directory ACL: 0  Links: 0   Blockcount: 0  Fragment:  Address: 0    Number: 0    Size: 0  ctime: 0x47cf5ebc -- Thu Mar  6 11:02:20 2008  atime: 0x47cf5bb0 -- Thu Mar  6 10:49:20 2008  mtime: 0x47cf5bb1 -- Thu Mar  6 10:49:21 2008  dtime: 0x47cf5ebc -- Thu Mar  6 11:02:20 2008  BLOCKS:  (0):62218, (1):4, (3):1, (4):20491, (6):3, (7):1,   (8):20483, (9):5, (10):1, (11):20485, (IND):7, (12):32775,   (13):98311, (14):163847, (15):229383, (DIND):2, (IND):32770,   (IND):98306, (IND):163842, (IND):229378, (TIND):20487, (DIND):14386  TOTAL: 22  

首先需要注意的一點是,對於上面這棵 extent 樹來說,只需要使用 i_block 數組的前 6 個元素就可以存儲一個 ext4_extent_header 和一個 ext4_extent_idx 結構了,而 i_block 數組中的所有元素卻都是有數據的。之所以出現這種情況是因為在創建這個特殊的測試文件的過程中,我們是不斷創建一個文件並在此文件尾部追加數據從而生成新文件的。當該文件使用的 extent 超過 4 個時,便擴充成一棵 extent 樹,但是剩餘 3 個 extent 的內容(i_block 數組的后 9 個元素)並沒有被清空。

對比刪除文件前後的變化會發現,ext4 與 ext3 非常類似,也都將文件大小設置為 0,這使得 debugfs 的 dump 命令也無從正常工作了。不過與 ext3 不同的是,ext4 並沒有將 i_block 數組的元素全部清空,而是將 ext4_extent_header 結構中有效項數設置為 0,這樣就將 extent 樹破壞掉了。另外,比較葉子節點(數據塊 20491)中的數據變化會發現,下面這些域在刪除文件時也都被清除了:

  • ext4_extent_header 結構中的 eh_entries。
  • ext4_extent 結構中的 ee_len、ee_start_hi 以及 ee_start。


圖3. 刪除文件前後 extent 樹中葉子節點數據塊的變化

了解清楚這些變化之後,我們會發現在 ext4 中恢復刪除文件的方法與 ext3 基本類似,也可以使用全文匹配、提前備份元數據和修改內核實現 3 種方法。

正如前面介紹的一樣,由於 ext4 文件系統中採用了 extent 的設計,試圖最大程度地確保文件數據會被保存到連續的數據塊中,因此在 ext2/ext3 恢復刪除文件時所介紹的正文匹配方法也完全適用,正常情況下採用這種方式恢復出來的數據會比 ext2/ext3 中更多。詳細內容請參看本系列文章第 4 部分的介紹,本文中不再贅述。

儘管 ext4 是基於 extent 來管理空間的,但是在 ext3 中備份數據塊位置的方法依然完全適用,下面給出了一個例子。


清單15. 備份文件數據塊位置

                  [root@vmfc8 ext4]# export LD_PRELOAD=/usr/local/lib/libundel.so  [root@vmfc8 ext4]# rm -f /tmp/test/sparsefile.70K  [root@vmfc8 ext4]# tail -n 1 /var/e2undel/e2undel   8,3::13::71682::4096::(1-1): 20481-20481,(3-3): 20483-20483,  (5-5): 20485-20485,(7-8): 20487-20488,(10-10): 20490-20490,  (12-12): 20492-20492,(14-15): 20494-20495,  (17-17): 20497-20497::/tmp/test/sparsefile.70K  

當然,如果內核實現中可以在刪除文件時,extent樹(其中包括i_block數組,其他extent索引節點和葉子節點)中的數據保留下來,那自然恢復起來就更加容易了。由於 ext4 的開發依然正在非常活躍地進行中,相關代碼可能會頻繁地發生變化,本文就不再深入探討這個話題了,感興趣的讀者可以自行嘗試。





小結

本文從 ext3 的在支持大文件系統方面的缺陷入手,逐漸介紹了 ext4 為了支持大文件系統而引入的一些設計特性,並探討了這些特性對磁碟數據格式引起的變化,以及對恢復刪除文件所帶來的影響,最終討論了在 ext4 文件系統中如何恢復刪除文件的問題。在本系列的下一篇文章中,我們將開始討論另外一個設計非常精巧的文件系統 reiserfs 上的相關問題。

reiserfs 對於小文件的存取速度非常高,這取決於它所採用的精美的設計:reiserfs 文件系統就是一棵動態的 B+ 樹,小文件和大文件的尾部數據都可以通過保存到葉子節點中而加快存取速度。本文將探討 reiserfs 的設計和實現內幕,並從中探討恢復刪除文件的可能性。

reiserfs 是由 namesys 公司的 Hans Reiser 設計並開發的一種通用日誌文件系統,它是第一個進入 Linux 標準內核日誌文件系統。從誕生之日起,reiserfs 就由於其諸多非常有吸引力的特性而受到很多用戶的青睞,迅速成為 Slackware 等發行版的默認文件系統。它也一度也是 SUSE Linux Enterprise 發行版上的默認文件系統,直到 2006 年 10 月 12 日 Novell 公司決定將默認文件系統轉換到 ext3 為止。儘管其主要設計人員 Hans Reiser 由於涉嫌殺害妻子遭到指控而入獄,從而導致他不得不試圖出售 namesys 公司來支付龐大的訴訟費用,但是 reiserfs 已經受到廣大社區開發人員和用戶的極大關注,有很多志願者已經投入到新的 reiserfs 4 的開發工作中來。本文中的介紹都是基於最新的穩定版本 3.6 版本的,所引用的代碼都基於 2.6.23 版本的內核。

reiserfs 最初的設計目標是為了改進 ext2 文件系統的性能,提高文件系統的利用率,並增強對包含大量文件的目錄的處理能力(ext2/ext3 文件系統中一個目錄下可以包含的子目錄最多只能有 31998 個)。傳統的 ext2 和 ufs 文件系統都採用了將文件數據和文件元數據分離開保存的形式,將元數據保存到索引節點中,將文件數據保存到單獨的磁碟塊中,並通過索引節點中的 i_block 數組利用直接索引和間接索引的形式在磁碟上定位文件數據。這種設計非常適合存儲較大的文件(比如20KB以上),但是對於具有大量小文件的系統來說就存在一些問題。首先在於文件系統的利用率,由於 ext2 會將文件數據以數據塊為單位(默認為 4KB)進行存儲,因此對於存儲只有幾十個位元組的文件來說,會造成空間的極大浪費。另外由於在讀取文件時需要分別讀取文件元數據和文件數據,加上多讀取數據塊的開銷,ext2 文件系統在處理大量小文件時,性能會比較差。為了獲取最好的性能和最大程度地利用磁碟空間,很多用戶會在文件系統之上採用資料庫之類的解決方案來存儲這些小文件,因此會導致上層應用程序的介面極不統一。

為了解決上面提到的問題,reiserfs 為每個文件系統採用一棵經過專門優化的 B+ 樹來組織所有的文件數據,並實現了很多新特性,例如元數據日誌。為了提高文件系統的利用率,reiserfs 中採用了所謂的尾部封裝(tail packing)設計,可以充分利用已分配磁碟塊中的剩餘空間來存儲小文件。實際上,reiserfs 文件系統中存儲的文件會比 ext2/ext3 大 5% - 6% 以上。下面讓我們來探索一下 reiserfs 文件系統中數據在磁碟上究竟是如何存儲的。

磁碟布局

與 ext2/ext3 類似,reiserfs 文件系統在創建時,也會將磁碟空間劃分成固定大小的數據塊。數據塊從 0 開始編號,最多可以有 232 個數據塊。因此如果採用默認的 4KB 大小的數據塊,單個 reiserfs 文件系統的上限是 16TB。reiserfs 分區的前 64KB 保留給引導扇區、磁碟標籤等使用。超級塊(super block)從 64KB 開始,會佔用一個數據塊;之後是一個數據塊點陣圖,用來標識對應的數據塊是否處於空閑狀態。如果一個數據塊點陣圖可以標識 n 個數據塊,那麼 reiserfs 分區中的第 n 個數據塊也都是這樣一個數據塊,用來標識此後(包括自己)n 的數據塊的狀態。reiserfs 文件系統的磁碟結構如圖 1 所示。


圖 1. reiserfs 分區磁碟布局

與 ext2/ext3 類似,reiserfs 文件系統的一些關鍵信息也保存超級塊中。reiserfs 的超級塊使用一個 reiserfs_super_block 結構來表示,其定義如清單1 所示:


清單1. reiserfs_super_block 結構定義

                  135 struct reiserfs_super_block_v1 {   136         __le32 s_block_count;   /* blocks count         */   137         __le32 s_free_blocks;   /* free blocks count    */   138         __le32 s_root_block;    /* root block number    */   139         struct journal_params s_journal;   140         __le16 s_blocksize;     /* block size */   141         __le16 s_oid_maxsize;   /* max size of object id array, see   142                                  * get_objectid() commentary  */   143         __le16 s_oid_cursize;   /* current size of object id array */   144         __le16 s_umount_state;  /* this is set to 1 when filesystem was   145                                  * umounted, to 2 - when not */   146         char s_magic[10];       /* reiserfs magic string indicates that   147                                  * file system is reiserfs:   148                                  * "ReIsErFs" or "ReIsEr2Fs" or "ReIsEr3Fs" */   149         __le16 s_fs_state;      /* it is set to used by fsck to mark which   150                                  * phase of rebuilding is done */   151         __le32 s_hash_function_code;    /* indicate, what hash function is being use   152                                          * to sort names in a directory*/   153         __le16 s_tree_height;   /* height of disk tree */   154         __le16 s_bmap_nr;       /* amount of bitmap blocks needed to address   155                                  * each block of file system */   156         __le16 s_version;       /* this field is only reliable on filesystem   157                                  * with non-standard journal */   158         __le16 s_reserved_for_journal;  /* size in blocks of journal area on main   159                                          * device, we need to keep after   160                                          * making fs with non-standard journal */   161 } __attribute__ ((__packed__));   162    163 #define SB_SIZE_V1 (sizeof(struct reiserfs_super_block_v1))   164    165 /* this is the on disk super block */   166 struct reiserfs_super_block {   167         struct reiserfs_super_block_v1 s_v1;   168         __le32 s_inode_generation;   169         __le32 s_flags;     /* Right now used only by inode-attributes, if enabled */   170         unsigned char s_uuid[16];       /* filesystem unique identifier */   171         unsigned char s_label[16];      /* filesystem volume label */   172         char s_unused[88];      /* zero filled by mkreiserfs and   173                                  * reiserfs_convert_objectid_map_v1()   174                                  * so any additions must be updated   175                                  * there as well. */   176 } __attribute__ ((__packed__));  

該結構定義中還包含了其他結構的定義,例如 journal_params,這是有關日誌的一個結構,並非本文關注的重點,讀者可以自行參考內核源代碼中的 include/linux/ reiserfs_fs.h 文件。

實際上,超級塊並不需要一個完整的數據塊來存儲,這個數據塊中剩餘的空間用來解決文件對象 id 的重用問題,詳細內容請參看本系列文章下一部分的介紹。





B+ 樹

與 ext2/ext3 的超級塊比較一下會發現,reiserfs 的超級塊中並沒有索引節點表的信息,這是由於 reiserfs 並沒有使用索引節點表,而是採用了 B+ 樹來組織數據(在 reiserfs 的文檔中也稱為是 S+ 樹)。圖 2 中給出了一棵典型的 2 階 B+ 樹,其深度為 4。


圖 2. 2 階 B+ 樹

一棵 B+ 樹有唯一一個根節點,其位置保存在超級塊的 root_block 欄位中。包含子樹的節點都是中間節點(internal node),不包含子樹的節點稱為葉子節點(leaf node)。按照是否包含 B+ 樹本身需要的信息,節點又可以分為兩類,一類節點包含 B+ 樹所需要的信息(例如指向數據塊位置的指針,同時也包括文件數據),稱為格式化節點(formatted node);另外一類只包含文件數據,而不包含格式化信息,稱為未格式化節點(unformatted node 或 unfleaf)。因此,所有的中間節點都必須是格式化節點。一個格式化的葉子節點中可以包含多個條目(item,也稱為項),所謂條目是一個數據容器,其內容可以保存到一個數據塊中,也就是說,一個條目只能隸屬於一個數據塊,它是節點管理空間的基本單位。

為了方便理解起見,我們可以認為對於 B+ 樹來說,一共包含 3 類節點:中間節點(其中保存了對葉子節點的索引信息)、葉子節點(包含一個或多個條目項)和數據節點(僅僅用來存放文件數據)。

熟悉數據結構的讀者都會清楚,B+ 樹是一棵平衡樹,從根節點到達每個葉子節點的深度都是相同的。與 B- 樹相比,B+ 樹的好處是可以將數據全部保存到葉子節點中,而中間節點中並不存放真正的數據,僅僅用作對葉子節點的索引。為了方便起見,樹的深度從葉子節點開始計算,葉子節點的深度為 1,之上的中間節點逐層加 1。在 B+ 樹中查找匹配項首先要對關鍵字進行比較並沿對應指針進行遍歷,直至搜索到葉子節點為止;而葉子節點也是按照關鍵字從小到大的順序進行排列的。也正是由於這種結構,使得 B+ 樹非常適合用來存儲文件系統結構。





關鍵字

reiferfs 中採用的關鍵字包含 4 個部分,形式如下:

( directory-id, object-id, offset, type )  

這 4 個部分分別表示父目錄的 id、本對象的 id、本對象在整個對象(文件)中的偏移量以及類型。關鍵字的比較就是按照這 4 個部分逐一進行的。讀者可能會好奇為什麼不簡單地採用對象 id 作為關鍵字。實際上,這種設計是有很多考慮的,採用 directory-id作為關鍵字的一部分,可以將相同目錄中的文件和子目錄組織在一起,加快對目錄項的存取。offset 的出現是為了支持大文件,一個間接條目(後文中會介紹)最多能指向 (數據塊大小-48)/4 個數據塊來存放文件數據,在默認的 4KB 數據塊中最大隻能支持 4048KB 的文件。因此使用 offset 就可以表明該對象在文件中所處的偏移量。type 可以用來區分對象的類型。目目前reiserfs 支持四種類型,TYPE_STAT_DATA、TYPE_INDIRECT 1、TYPE_DIRECT 2、 TYPE_DIRENTRY,解釋見後文的條目頭部分。

在 3.5 之前的版本中,這 4 部分都是 32 位的整數,這樣造成的問題是最大隻能支持大約 232=4GB 的文件。從 3.6 版本開始,設計人員將 offset 擴充至 60 位,將 type 壓縮至 4 位。這樣理論上能夠支持的最大文件就達到了 260 位元組,但是由於其他一些限制,reiserfs 中可以支持的文件上限是 8TB。正是由於這個原因,reiserfs 中有兩個版本的關鍵字,相關定義如清單 2 所示。


清單2. reiserfs 中與關鍵字有關的定義

                  363 struct offset_v1 {   364         __le32 k_offset;   365         __le32 k_uniqueness;   366 } __attribute__ ((__packed__));   367    368 struct offset_v2 {   369         __le64 v;   370 } __attribute__ ((__packed__));   371           372 static inline __u16 offset_v2_k_type(const struct offset_v2 *v2)   373 {   374         __u8 type = le64_to_cpu(v2->v) >> 60;   375         return (type <= TYPE_MAXTYPE) ? type : TYPE_ANY;   376 }   377    378 static inline void set_offset_v2_k_type(struct offset_v2 *v2, int type)   379 {   380         v2->v =   381             (v2->v & cpu_to_le64(~0ULL >> 4)) | cpu_to_le64((__u64) type << 60);   382 }   383    384 static inline loff_t offset_v2_k_offset(const struct offset_v2 *v2)   385 {   386         return le64_to_cpu(v2->v) & (~0ULL >> 4);   387 }   388    389 static inline void set_offset_v2_k_offset(struct offset_v2 *v2, loff_t offset)   390 {   391         offset &= (~0ULL >> 4);   392         v2->v = (v2->v & cpu_to_le64(15ULL << 60)) | cpu_to_le64(offset);   393 }   394    395 /* Key of an item determines its location in the S+tree, and   396    is composed of 4 components */   397 struct reiserfs_key {   398         __le32 k_dir_id;        /* packing locality: by default parent   399                                    directory object id */   400         __le32 k_objectid;      /* object identifier */   401         union {   402                 struct offset_v1 k_offset_v1;   403                 struct offset_v2 k_offset_v2;   404         } __attribute__ ((__packed__)) u;   405 } __attribute__ ((__packed__));   406  

儘管結構定義中並沒有顯式地聲明 offset 和 type 分別是 60 位和 4 位長,但是從幾個相關函數中可以清楚地看到這一點。





數據塊頭

在圖 2 中我們曾經介紹過,b+ 樹中的節點可以分為格式化節點和未格式化節點兩種。未格式化節點中保存的全部是文件數據,而格式化節點中包含了 b+ 樹本身需要的一些信息。為了與未格式化節點區分開來,每個格式化節點所佔用的數據塊最開頭都使用一個數據塊頭來表示。數據塊頭大小為 24 個位元組,定義如清單 3 所示。


清單3. block_head 結構定義

                  698 /* Header of a disk block.  More precisely, header of a formatted leaf   699    or internal node, and not the header of an unformatted node. */   700 struct block_head {   701         __le16 blk_level;       /* Level of a block in the tree. */   702         __le16 blk_nr_item;     /* Number of keys/items in a block. */   703         __le16 blk_free_space;  /* Block free space in bytes. */   704         __le16 blk_reserved;   705         /* dump this in v4/planA */   706         struct reiserfs_key blk_right_delim_key;   /* kept only for compatibility */   707 };  

block_head 結構中的 blk_level 表示該節點在 B+ 樹中的層次,對於葉子節點來說,該值為 1;blk_nr_item 表示這個數據塊中條目的個數;blk_free_space 表示這個數據塊中的空閑磁碟空間。

格式化節點可以分為中間節點和葉子節點兩類,它們所採用的存儲結構是不同的。





中間節點

中間節點由數據塊頭、關鍵字和指針數組構成。中間節點中的關鍵字和指針數組都是按照從小到大的順序依次存放的,它們在磁碟上的布局如圖 3 所示。


圖 3. 中間節點的布局

每個關鍵字就是一個 16 位元組的 reiserfs_key 結構,而指針則是一個 disk_child 結構,其大小為 8 個位元組,定義如清單 4 所示。


清單4. disk_child 結構定義

                  1086 /* Disk child pointer: The pointer from an internal node of the tree  1087    to a node that is on disk. */  1088 struct disk_child {  1089         __le32 dc_block_number; /* Disk child's block number. */  1090         __le16 dc_size;         /* Disk child's used space.   */  1091         __le16 dc_reserved;  1092 };  

其中 dc_block_number 欄位是所指向子節點所在的數據塊塊號,dc_size 表示這個數據塊中已用空間的大小。對於一共有 n 個關鍵字的中間節點來說,第 i 個關鍵字位於 24+i*16 位元組處,對應的指針位於 24+16*n+8*i 位元組處。

需要注意的是,對於中間節點來說,數據塊頭的 blk_nr_item 欄位表示的是關鍵字的個數,而指針數總是比關鍵字個數多 1,這是由 B+ 樹的結構所決定的,小於 Key 0 的關鍵字可以在 Pointer 0 指針指向的數據塊(下一層中間節點或葉子節點)中找到,而介於 Key 0 和 Key 1 之間的關鍵字則保存在 Pointer 1 指向的數據塊中,依此類推。大於Key n的關鍵字可以在Pointer n+1中找到。





葉子節點

格式化葉子節點的結構比中間節點的結構稍微複雜一點。為了能夠在一個格式化葉子節點中保存多個條目,reiserfs 採用了如圖 4 所示的布局結構。


圖 4. 格式化葉子節點的布局

從圖中可以看出,每個格式化葉子節點都以一個數據塊頭開始,然後是從兩端向中間伸展的條目頭和條目數據的數組,空閑空間保留在中間,這種設計是為了擴充方便。

所謂條目(item,或稱為項)就是可以存儲在單個節點中的一個數據容器,我們可以認為條目是由條目頭和條目數據體組成的。


清單5. item_head 結構定義

                  460 /*  Everything in the filesystem is stored as a set of items.  The   461     item head contains the key of the item, its free space (for   462     indirect items) and specifies the location of the item itself   463     within the block.  */   464    465 struct item_head {   466         /* Everything in the tree is found by searching for it based on   467          * its key.*/   468         struct reiserfs_key ih_key;   469         union {   470                 /* The free space in the last unformatted node of an   471                    indirect item if this is an indirect item.  This   472                    equals 0xFFFF iff this is a direct item or stat data   473                    item. Note that the key, not this field, is used to   474                    determine the item type, and thus which field this   475                    union contains. */   476                 __le16 ih_free_space_reserved;   477                 /* Iff this is a directory item, this field equals the   478                    number of directory entries in the directory item. */   479                 __le16 ih_entry_count;   480         } __attribute__ ((__packed__)) u;   481         __le16 ih_item_len;     /* total size of the item body */   482         __le16 ih_item_location;        /* an offset to the item body   483                                          * within the block */   484         __le16 ih_version;      /* 0 for all old items, 2 for new   485                                    ones. Highest bit is set by fsck   486                                    temporary, cleaned after all   487                                    done */   488 } __attribute__ ((__packed__));  

從 item_head 結構定義中可以看出,關鍵字已經包含在其中了。ih_item_len 和 ih_item_location 分別表示對應條目的數據體的長度和在本塊中的偏移量。請注意該結構的第 17、18 個位元組是一個聯合結構,對於不同類型的條目來說,該值的意義不同:對於 stat 數據條目(TYPE_STAT_DATA)或直接數據條目(TYPE_DIRECT),該值為 15;對於間接數據條目(TYPE_INDIRECT),該值表示最後一個未格式化數據塊中的空閑空間;對於目錄條目(TYPE_DIRENTRY),該值表示目錄條目中目錄項的個數。

目前 reiserfs 支持的條目類型有 4 種,它們是依靠關鍵字中的 type 欄位來區分的;而在舊版本的關鍵字中,則是通過 uniqueness 欄位來標識條目類型的,其定義如清單 6 所示。


清單6. reiserfs 支持的條目類型

                  346 //         347 // there are 5 item types currently   348 //   349 #define TYPE_STAT_DATA 0   350 #define TYPE_INDIRECT 1   351 #define TYPE_DIRECT 2   352 #define TYPE_DIRENTRY 3   353 #define TYPE_MAXTYPE 3   354 #define TYPE_ANY 15             // FIXME: comment is required   355    …   509 //   510 // in old version uniqueness field shows key type   511 //   512 #define V1_SD_UNIQUENESS 0   513 #define V1_INDIRECT_UNIQUENESS 0xfffffffe   514 #define V1_DIRECT_UNIQUENESS 0xffffffff   515 #define V1_DIRENTRY_UNIQUENESS 500   516 #define V1_ANY_UNIQUENESS 555   // FIXME: comment is required   517  

下面讓我們逐一來了解一下各種條目的存儲結構。

STAT 條目

stat 數據(TYPE_STAT_DATA)非常類似於 ext2 中的索引節點,其中保存了諸如文件許可權、MAC(modified、accessed、changed)時間信息等數據。在3.6 版本的 reiserfs 中,stat 數據使用一個stat_data 結構表示,該結構大小為 44 位元組,其定義如清單 7 所示:


清單7. stat_data 結構定義

                  835 /* Stat Data on disk (reiserfs version of UFS disk inode minus the   836    address blocks) */   837 struct stat_data {   838         __le16 sd_mode;         /* file type, permissions */   839         __le16 sd_attrs;        /* persistent inode flags */   840         __le32 sd_nlink;        /* number of hard links */   841         __le64 sd_size;         /* file size */   842         __le32 sd_uid;          /* owner */   843         __le32 sd_gid;          /* group */   844         __le32 sd_atime;        /* time of last access */   845         __le32 sd_mtime;        /* time file was last modified  */   846         __le32 sd_ctime;       /* time inode (stat data) was last changed */                                     /* (except changes to sd_atime and sd_mtime) */   847         __le32 sd_blocks;   848         union {   849                 __le32 sd_rdev;   850                 __le32 sd_generation;   851                 //__le32 sd_first_direct_byte;   852                 /* first byte of file which is stored in a   853                    direct item: except that if it equals 1   854                    it is a symlink and if it equals   855                    ~(__u32)0 there is no direct item.  The   856                    existence of this field really grates   857                    on me. Let's replace it with a macro   858                    based on sd_size and our tail   859                    suppression policy? */   860         } __attribute__ ((__packed__)) u;   861 } __attribute__ ((__packed__));   862 //   863 // this is 44 bytes long   864 //  

stat_data 條目使用的關鍵字中,offset 和 type 的值總是 0,這樣就能確保 stat 數據是相同對象(object-id)中的第一個條目,從而能夠加快訪問速度。

與 ext2 的 ext2_indoe 結構對比一下就會發現,stat_data 中既沒有記錄數據塊位置的地方,也沒有記錄刪除時間,而這正是我們在 ext2/ext3 中恢復刪除文件的基礎,因此可以猜測得到,在reiserfs 文件系統中要想恢復已經刪除的文件,難度會變得更大。

目錄條目

目錄條目中記錄了目錄項信息。目錄條目由目錄頭和目錄項數據(即文件或子目錄名)組成。如果一個目錄中包含的目錄項太多,可以擴充到多個目錄條目中存儲。為了方便管理某個目錄中子目錄或文件的增減,目錄條目也採用了與條目頭類似的設計:從兩端向中間擴充,其布局結構如圖 5 所示。


圖 5. 目錄條目存儲結構

目錄頭是一個 reiserfs_de_head 結構,大小為 16 位元組,其定義如清單 8 所示。


清單8. reiserfs_de_head 結構定義

                  920 /*   921    Q: How to get key of object pointed to by entry from entry?     922    923    A: Each directory entry has its header. This header has                deh_dir_id and deh_objectid fields, those are key   924       of object, entry points to */   925    926 /* NOT IMPLEMENTED:      927    Directory will someday contain stat data of object */   928    929 struct reiserfs_de_head {   930         __le32 deh_offset;      /* third component of the directory entry key */   931         __le32 deh_dir_id;      /* objectid of the parent directory of the object,    932                                    that is referenced by directory entry */   933         __le32 deh_objectid;    /* objectid of the object, that is referenced */                                       /* by directory entry */   934         __le16 deh_location;    /* offset of name in the whole item */   935         __le16 deh_state;       /* whether 1) entry contains stat data (for future),    936                                    and 2) whether entry is hidden (unlinked) */   937 } __attribute__ ((__packed__));  

reiserfs_de_head 結構中包含了 deh_dir_id 和 deh_objectid fields 這兩個欄位,它們就是其父目錄關鍵字中對應的兩個欄位。deh_offset 的 7 到 30 位是文件名的 hash 值,0 到 6 位用來解決 hash 衝突的問題(reiserfs 中可以使用 3 種 hash 函數:tea、rupasov 和 r5,默認為 r5)。文件名的位置保存在 deh_location 欄位中,而 deh_state 的第 2 位表示該目錄條目是否是可見的(該位為 1 則表示該目錄條目是可見的,為 0 表示不可見)。文件名是一個字元串,以空字元結束,按照 8 位元組對齊。

直接條目與間接條目

在 reiserfs 中,文件數據可以通過兩種方式進行存取:直接條目(direct item)和間接條目(indirect item)。對於小文件來說,文件數據本身和 stat 數據可以一起存儲到葉子節點中,這種條目就稱為直接條目。直接條目就採用圖 4 所示的存儲結構,不過每個條目數據體就是文件數據本身。對於大文件來說,單個葉子節點無法存儲下所有數據,因此會將部分數據存儲到未格式化數據塊中,並通過間接條目中存儲的指針來訪問這些數據塊。未格式化數據塊都是整塊使用的,最後一個未格式化數據塊中可能會遺留一部分剩餘空間,大小是由對應條目頭的 ih_free_space_reserved 欄位指定的。圖 6 給出了間接條目的存儲結構。


圖 6. 間接條目存儲結構

對於預設的 4096 位元組的數據塊來說,一個間接條目所能存儲的數據最大可達 4048 KB(4096*(4096-48)/4 位元組),更大的文件需要使用多個間接條目進行存儲,它們之間的順序是通過關鍵字中的 offset 進行標識的。

另外,文件末尾不足一個數據塊的部分也可以像小文件一樣存儲到直接條目中,這種技術就稱為尾部封裝(tail packing)。在這種情況下,存儲一個文件至少需要使用一個間接條目和一個直接條目。





實例分析

下面讓我們來看一個實際的例子,以便了解 reiserfs 中的實際情況是什麼樣子。首先讓我們來創建一個 reiserfs 文件系統,這需要在系統中安裝 reiserfs-utils 包,其中包含的內容如清單 9 所示。


清單9. reiserfs-utils 包中包含的文件

                  [root@vmfc8 reiserfs]# rpm -ql reiserfs-utils  /sbin/debugreiserfs  /sbin/fsck.reiserfs  /sbin/mkfs.reiserfs  /sbin/mkreiserfs  /sbin/reiserfsck  /sbin/reiserfstune  /sbin/resize_reiserfs  /usr/share/doc/reiserfs-utils-3.6.19  /usr/share/doc/reiserfs-utils-3.6.19/README  /usr/share/man/man8/debugreiserfs.8.gz  /usr/share/man/man8/mkreiserfs.8.gz  /usr/share/man/man8/reiserfsck.8.gz  /usr/share/man/man8/reiserfstune.8.gz  /usr/share/man/man8/resize_reiserfs.8.gz  

mkreiserfs 命令用來創建 reiserfs 文件系統,debugreiserfs 用來查看 reiserfs 文件系統的詳細信息,如清單 10 所示。


清單10. 創建 reiserfs 文件系統

                  [root@ vmfc8 reiserfs]# echo y | mkreiserfs /dev/sda2    [root@vmfc8 reiserfs]# debugreiserfs -m /dev/sda2  debugreiserfs 3.6.19 (2003 www.namesys.com)      Filesystem state: consistent    Reiserfs super block in block 16 on 0x802 of format 3.6 with standard journal  Count of blocks on the device: 977952  Number of bitmaps: 30  Blocksize: 4096  Free blocks (count of blocks - used [journal, bitmaps, data, reserved] blocks): 969711  Root block: 8211  Filesystem is clean  Tree height: 2  Hash function used to sort names: "r5"  Objectid map size 2, max 972  Journal parameters:          Device [0x0]          Magic [0x28ec4899]          Size 8193 blocks (including 1 for journal header) (first block 18)          Max transaction length 1024 blocks          Max batch size 900 blocks          Max commit age 30  Blocks reserved by journal: 0  Fs state field: 0x0:  sb_version: 2  inode generation number: 0  UUID: 02e4b98a-bdf3-4654-9cae-89e38970f43c  LABEL:   Set flags in SB:          ATTRIBUTES CLEAN  Bitmap blocks are:  #0: block 17: Busy (0-8211) Free(8212-32767)  used 8212, free 24556  #1: block 32768: Busy (32768-32768) Free(32769-65535)  used 1, free 32767  …  #29: block 950272: Busy (950272-950272) Free(950273-977951) Busy(977952-983039)  used 5089, free 27679  

從輸出結果中可以看出,這是大約是一個 4GB 的分區,總共劃分成 977952 個 4096B 大小的數據塊;而超級塊是第 16 個數據塊(從 0 開始計算)。格式化過程中佔用了其中的 8241 個數據塊,其中從第 18 個數據塊開始的 8193 個數據塊用於日誌(第 8210 數據塊供日誌頭使用)。這個 B+ 樹的根節點保存在該分區的第 8211 個數據塊中。另外,數據塊點陣圖總共佔用了 30 個數據塊,-m 參數給出了這些數據塊點陣圖所佔用的數據塊的具體位置,這與前文中的介紹是完全吻合的。

我們真正關心的是存儲實際數據的部分,從中可以了解在 reiserfs 中是如何對文件進行訪問的。下面讓我們來看一個實際文件系統的例子。


清單11. 分析 8211 數據塊的內容

                  [root@vmfc8 reiserfs]# mount /dev/sda2 /tmp/test  [root@vmfc8 reiserfs]# dd if=/dev/sda2 of=block.8211 bs=4096 count=1 skip=8211  [root@vmfc8 reiserfs]# hexdump –C block.8211.hex  

block.8211.hex 文件中就是根節點中的實際數據,圖 7 給出了一個更為清晰的分析結果。


圖 7. 新文件系統根節點的數據分析

從圖 7 中可以清楚地看出,這是一個格式化葉子節點(深度為 1),其中包含 4 個條目:兩個是 STAT 條目,另外兩個是目錄條目。實際上,它們分別是當前目錄和其父目錄。繼續分析就會發現,當前目錄中只包含兩個目錄項:當前目錄及其父目錄,因此這是一個空目錄。






小結

總體來說,reiserfs 所採用的設計很多都是專門針對如何充分提高空間利用率和改進小文件的訪問速度。與 ext2/ext3 不同,reiserfs 並不以固定大小的塊為單位給文件分配存儲空間。相反,它會恰好分配文件大小的磁碟空間。另外,reiserfs 還包括了圍繞文件末尾而專門設計的尾部封裝方案,將文件名和文件數據(或部分數據)共同保存在 B+ 樹的葉子節點中,而不會像 ext2 那樣將數據單獨保存在一個磁碟上的數據塊中,然後使用一個指針指向這個數據塊的位置。

這種設計會帶來兩個優點。首先,它可以極大地改進小文件的性能。由於文件數據和 stat_data(對應於 ext2 中的 inode)信息都是緊鄰保存的,只需要一次磁碟 I/O 就可以將這些數據全部讀出。其次,可以有效提高對磁碟空間的利用率。統計表明,reiserfs 採用這種設計之後,可以比相應的 ext2 文件系統多存儲超過 6% 的數據。

不過,尾部封裝的設計可能會對性能稍有影響,因為它每次文件發生修改時,都需要重新對尾部數據進行封裝。由於這個原因,reiserfs 的尾部封裝特性被設計為可以關閉的,這樣就為管理員在性能和存儲效率之間可以提供一個選擇。對於性能非常關鍵的應用程序來說,管理員可以使用 notail 選項禁用這個特性,從而犧牲一部分磁碟空間來獲得更好的性能。

與 ext2/ext3 相比,在處理小於 4KB 的文件時,reiserfs 的速度通常會快 10 到 15 倍。這對於新聞組、HTTP 緩存、郵件發送系統以及其他一些小文件性能非常重要的應用程序來說是非常有益的。

(責任編輯:A6)





[火星人 via ] 如何恢復 Linux 上刪除的文件已經有293次圍觀

http://www.coctec.com/docs/linux/show-post-69317.html