加入收藏 | 设为首页 | 会员中心 | 我要投稿 财气旺网 - 财气网 (https://www.caiqiwang.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 服务器 > 搭建环境 > Linux > 正文

linux删除 0 字节文件,如何恢复 Linux 上删除的文件

发布时间:2022-11-24 13:02:06 所属栏目:Linux 来源:
导读:  要想恢复误删除的文件,必须清楚数据在磁盘上究竟是如何存储的,以及如何定位并恢复数据。本文从数据恢复的角度,着重介绍了 ext2 文件系统中使用的一些基本概念和重要数据结构,并通过几个实例介绍了如何手工恢
  要想恢复误删除的文件,必须清楚数据在磁盘上究竟是如何存储的,以及如何定位并恢复数据。本文从数据恢复的角度,着重介绍了 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 文件系统来说,磁盘被划分成一个个大小相同的数据块linux分区工具,每个块的大小可以是1024、2048 或 4096 个字节。其中,第一个块称为引导块,一般保留做引导扇区使用,因此 ext2 文件系统一般都是从第二个块开始的。剩余的块被划分为一个个的块组,ext2 文件系统会试图尽量将相同文件的数据块都保存在同一个块组中,并且尽量保证文件在磁盘上的连续性,从而提高文件读写时的性能。
 
  至于一个分区中到底有多少个块组,这取决于两个因素:
 
  分区大小。
 
  块大小。
 
  最终的计算公式如下:
 
  分区中的块组数=分区大小/(块大小*8)
 
  这是由于在每个块组中使用了一个数据块位图来标识数据块是否空闲,因此每个块组中最多可以有(块大小*8)个块;该值除上分区大小就是分区中总的块组数。
 
  每个块组都包含以下内容:
 
  超级块。存放文件系统超级块的一个拷贝。
 
  组描述符。该块组的组描述符。
 
  数据块位图。标识相应的数据块是否空闲。
 
  索引节点位图。标识相应的索引节点是否空闲。
 
  索引节点表。存放所有索引节点的数据。
 
  数据块。该块组中用来保存实际数据的数据块。
 
  在每个块组中都保存了超级块的一个拷贝,默认情况下,只有第一个块组中的超级块结构才会被系统内核使用;其他块组中的超级块可以在 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 /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. 再次查看索引节点 的详细信息
 
  # echo "stat " | 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 文件系统中的一些特殊文件,以及如何恢复整个目录等方面的问题。
 

(编辑:财气旺网 - 财气网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!