SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

rm -rf误删文件后如何在ext4上恢复

误执行 rm -rf 后先停止写入并卸载或只读挂载磁盘。本文解释 ext4 为何难以恢复,并按成功可能性排列 TestDisk、映像和专业恢复方案。

首次 60 秒内应采取的措施

是否能恢复使用 rm -rf 删除的文件,取决于两件事,而且这两件事都应在打开搜索引擎之前完成。停止向该文件系统写入数据。然后停止使用它,可以卸载文件系统,或将其重新挂载为只读。

rm 不会擦除任何内容。它会删除目录项,然后将 inode 和文件数据块标记为空闲。字节仍然保留在设备上。只有当块分配器将这些块分配给其他对象,并且该对象向其中写入数据后,原有内容才会消失。文件系统每多挂载并运行一秒,守护进程就可能写入一行日志,数据库也可能刷新一个页面;这两种写入都可能覆盖您要恢复的数据块。

因此,最先执行的命令应当用于停止写入,而不是用于恢复文件。

sudo systemctl stop nginx postgresql
sudo umount /mnt/data

如果 umount 返回 umount: /mnt/data: target is busy.,请找出哪个进程正在占用该文件系统。

sudo fuser -vm /mnt/data
sudo lsof +D /mnt/data

如果无法释放该文件系统,请将其重新挂载为只读。只读挂载会阻止新的块分配,这通常已经满足大部分需求。

sudo mount -o remount,ro /mnt/data

如果被删除的路径位于根文件系统上,处理会更困难。sudo mount -o remount,ro / 通常会失败并返回 mount: /: cannot remount /dev/vda1 read-only.,因为运行中的进程会以写入方式打开文件,内核不会强制关闭这些文件。在 VPS 上,实际可行的方法是使用服务商提供的救援模式或恢复模式:该模式会启动一个独立的实时系统,并连接您的磁盘,但不会挂载它。之后,下面的每条命令都会针对没有任何进程写入的设备执行。

本指南始终适用一条规则。切勿将恢复的文件、磁盘映像或新安装的工具写入正在恢复的文件系统。请连接第二个卷,或通过 SSH 将输出发送到另一台计算机。

为什么 ext4 上使用 rm -rf 后恢复文件基本无望

安装任何工具前,先合理评估恢复预期。确认正在使用的文件系统:

lsblk -f

ext4 是几乎所有 VPS 镜像的默认文件系统。文件数据的位置以 extent 树的形式保存在 inode 中。一个 extent 记录该文件的逻辑块 N 从物理块 M 开始,并连续占用 L 个块。小文件最多可将 4 条记录直接保存在 inode 中。较大的文件会指向额外的数据块,其余树结构保存在这些数据块中。

文件的最后一个链接被删除后,ext4 会遍历这棵树,将每个 extent 归还给块分配器,并清除 inode 中的树结构。随后,inode 会被标记为空闲,并写入删除时间。文件数据本身不会被立即改写,但记录其位置的信息已经被删除。

这与 ext3 不同。ext3 的已删除 inode 仍会保留足够的信息,工具 ext3grep 可以据此跟踪文件。ext4 仍可列出已删除的 inode:

sudo debugfs -R lsdel /dev/vdb1

除非传入 -w,否则 debugfs 会以只读方式打开设备。因此,在未挂载的设备上执行是安全的,尝试也不会修改数据。命令会列出 inode,但转储 inode 时恢复流程就会终止,因为 inode 原先保存的块映射已经被清除,dump 无法继续跟踪数据。

有两个工具会尝试通过读取日志来绕过这一限制。日志是 ext4 用于在崩溃后保持元数据一致性的固定大小环形区域,其中可能仍保留删除前的 inode 副本。extundeleteext4magic 都会搜索该日志。先检查当前使用的日志大小:

sudo dumpe2fs -h /dev/vdb1 | grep -i journal

日志只保存元数据,而且容量较小,因此普通写入活动会很快循环覆盖其中的内容。在运行中的服务器上,删除前 inode 仍存在的时间窗口通常只有几分钟。这两个工具都不再积极维护,也不是每个发行版都会提供软件包。应将它们视为成功概率很低的方案,在未挂载的设备或磁盘映像上运行,并且不要因为没有返回结果而感到意外。

如果 lsblk -f 报告 xfs,情况也不会更好,因为 XFS 同样没有受支持的 undelete 工具。下面选项的顺序不会改变这一点。

运行中的进程是否仍打开该文件?

这是本页中成功率较高的唯一恢复方法。这也是不应重启使用该文件的服务的原因。

只有两个计数都降为零时,文件才真正消失:指向其 inode 的目录项数量,以及打开的文件描述符数量。rm 会将第一个计数降为零。如果某个进程仍保持文件打开,第二个计数就不是零,因此 inode 及其数据块仍已分配,数据仍可读取。

查找链接计数已降为零的打开文件:

sudo lsof +L1

+L1 表示列出链接计数小于 1 的打开文件。每个匹配项都会显示进程、文件描述符编号、NLINK(内容为 0)以及以 (deleted) 结尾的路径。将 PID 和描述符编号传递给 /proc

sudo ls -l /proc/1234/fd

条目类似于 3 -> /var/log/app/events.log (deleted)。该链接仍可访问文件数据。将数据复制到另一个文件系统:

sudo cp /proc/1234/fd/3 /mnt/rescue/events.log

使用 cp,不要使用 mv。打开 /proc/1234/fd/3 会从偏移量零开始,为同一个 inode 获取一个新的句柄,因此可以获得整个文件,而不是写入进程当前位置之后的部分。

有两个限制需要注意。已删除的目录树无法通过这种方式恢复,因为仍被进程持有的只有进程打开过的单个文件。数据库引擎正在写入时复制数据库文件,只能得到崩溃一致的副本。因此,应计划对该副本运行数据库引擎自身的恢复流程,不要将其视为干净副本。lsof 显示为 mem 而不是描述符编号的条目属于内存映射文件,并且没有可供复制的 /proc/<pid>/fd 条目。

btrfs、ZFS 或 LVM 上是否有快照?

如果文件系统支持快照,被删除的文件可能仍原封不动地保存在某个快照中。只有在删除前已经存在快照时,这种方法才有用。现在创建的快照无法恢复过去的状态。

btrfs 将快照保存为子卷:

sudo btrfs subvolume list /

浏览该快照,并使用 cp -a 将所需路径复制出来。优先复制单个路径,不要回滚整个子卷,因为回滚还会丢弃创建快照后写入的所有内容。

ZFS 会将每个快照显示为只读目录:

zfs list -t snapshot
ls /tank/data/.zfs/snapshot/

.zfs 目录默认隐藏,在对数据集根目录执行普通 ls 时不会显示,但可以按名称进入该目录。从中复制文件。zfs rollback 会将整个数据集恢复到指定快照,并销毁该快照之后创建的所有快照,因此应将其作为最后手段。

LVM 快照是具有固定大小的写时复制卷:

sudo lvs
sudo mount -o ro /dev/vg0/data-snap /mnt/snap

以只读方式挂载该卷,然后复制文件。确认 lvs 的结果后再使用其中的文件,因为 LVM 快照占满分配的空间后会被内核标记为无效;一旦发生这种情况,其中的内容就会丢失。

快照不是备份。快照与原始数据位于同一磁盘或同一存储池,因此会受到原始数据所面临的所有故障影响。快照非常适合撤销两分钟前发生的误操作,这正是这里需要的功能。

使用 PhotoRec 进行文件雕刻,但针对磁盘镜像而不是在线磁盘

如果上述方法都不适用,剩下的就是文件雕刻:扫描原始设备,查找标志已知文件类型起始位置的字节模式,然后写出后续内容。文件雕刻只读取文件数据。文件名、目录结构、时间戳和所有权都属于文件系统元数据,而这些元数据已被 rm 销毁,因此无法恢复。恢复出的文件名会是 f0384512.jpg,并保存在按编号命名的输出目录中,之后需要手动整理。

以下两条规则决定这种方法是否可行。

首先,在对设备执行其他操作前,先创建设备镜像。在 Debian 和 Ubuntu 上,软件包是 gddrescue,它安装的二进制文件是 ddrescue

sudo apt update && sudo apt install -y gddrescue testdisk
sudo ddrescue -n /dev/vdb1 /mnt/rescue/vdb1.img /mnt/rescue/vdb1.map

/mnt/rescue 必须位于其他设备上,并且可用空间至少要与该分区的容量相同。lsblk -b 会以字节为单位输出精确大小。映射文件可以让中断的复制继续进行,而无需从头开始。创建镜像后,您可以稍后使用其他工具针对完全相同的字节再次尝试恢复。如果第一个工具已经覆盖磁盘,则无法这样做。

其次,将恢复工具指向镜像文件。

sudo photorec /d /mnt/rescue/recup /mnt/rescue/vdb1.img

photorec 会打开文本菜单。选择分区、文件系统类型、要搜索的文件签名以及目标目录。开始前,将签名列表缩小到实际丢失的文件类型,因为默认列表会搜索所有类型,最终需要筛选数万个文件片段。

同一软件包中的 testdisk 也提供自己的 undelete 功能,但仅支持 FAT、exFAT、NTFS 和 ext2。在 ext4 上,这意味着 photorec

预期碎片化文件可能无法正确恢复。文件雕刻假设文件块是连续的,因此分配器将其分散到磁盘不同位置的文件,要么会被错误重组,要么会完全漏检。媒体文件通常能较好地完成雕刻,因为它们具有明显的文件头。纯文本、配置文件和源代码的雕刻效果较差,因为没有字节签名可以标志 shell 脚本的起始位置。

空格导致的路径错误:错误的路径为何被删除

几乎所有 rm -rf 事故都源于 shell。rm 接收路径列表,然后依次删除每个路径。它无法知道您的真实意图。

最典型的情况是多出一个空格:

rm -rf /home/deploy/app /old
rm -rf /home/deploy/app/old

第一行包含两个参数。它先删除应用,再删除 /old。如果 /old 不存在,rm 完全不输出任何内容,因为 -f 会抑制文件不存在错误。没有输出不代表操作正确。

第二种情况是未加引号的变量中包含空格:

dir="/srv/my app"
rm -rf $dir

shell 会按空白字符拆分变量值,因此 rm 会将 /srv/myapp 作为两个独立路径接收。写成 rm -rf "$dir" 时,它才是一个路径。

第三种情况是变量为空,通常是因为本应为其赋值的命令执行失败:

rm -rf "$TARGET"/*

如果未设置 TARGET,它会展开为 rm -rf /*。GNU rm 拒绝这种裸形式:rm -rf / 会输出 rm: it is dangerous to operate recursively on '/',然后退出。通配符形式没有同样的保护,因为在 rm 运行之前,shell 就会将 /* 替换为真实的顶层路径列表,而 / 不在其中,因此保护条件永远不会触发。

可防止下次事故的习惯

  • 将用作路径的每个变量都加引号。每次都写 "$dir",包括测试和循环中的变量。
  • 对空值直接失败。rm -rf "${TARGET:?TARGET is not set}"/* 会让 shell 在 rm 启动前显示您的消息并停止,只要 TARGET 未设置或为空。在任何执行删除操作的脚本顶部加入 set -euo pipefail
  • 添加 --one-file-system。它会让 rm 跳过与传入参数位于不同文件系统中的目录,因此递归删除不会进入已挂载的备份卷或 bind mount。
  • 不要以 root 身份执行删除。服务账户只能删除其拥有的内容,这正是让每个服务使用独立的非特权用户运行的全部理由。如果您不确定某个账户可以访问哪些内容,读取 ls 输出中的权限位即可用一条命令确认。
  • 执行操作前先输出列表。在脚本中先生成路径,使用 printf '%s\n' 处理它们,读取输出,然后在第二遍执行删除。
  • 随手保留一个回收站命令。sudo apt install trash-cli 提供 trash-puttrash-listtrash-restoretrash-empty。删除的文件会移动到 ~/.local/share/Trash,而 trash-empty 30 会清除超过三十天的内容。

rm 别名设置为 trash-put 看似是显而易见的下一步,实际上这是一个陷阱。该别名会形成一种条件反射,但下一台没有此别名的服务器会使它失效;而且别名不会在脚本中生效,代价高昂的错误往往就发生在脚本中。请有意识地输入 trash-put

每次都有效的唯一恢复方案

上文介绍的所有措施都只是可能性。备份不是可能性。

备份要真正可靠,需要满足两点。它会按计划自动运行,不需要您记住;并且您至少成功从中恢复过一次。从未进行过恢复的仓库只能算是一种信念,因为导致备份失效的问题(例如 include 列表中的路径错误,或无人记录仓库密码)只有在真正需要时才会暴露。

使用 restic,只需执行两个命令即可恢复。

restic -r /srv/restic-repo snapshots
restic -r /srv/restic-repo restore latest --target /mnt/rescue --include /srv/appdata

请将内容恢复到空目录,而不是直接覆盖线上路径。这样您可以先比较两者,再将内容移入正确位置。在 VPS 上设置 restic 备份介绍了仓库配置以及用于运行备份的 systemd 定时器。

使用 Borg:

borg list /srv/borg-repo
borg extract /srv/borg-repo::daily-2026-08-08 srv/appdata

Borg 归档中的路径不会包含开头的斜杠,因此 srv/appdata 可以匹配,而 /srv/appdata 不会匹配任何内容。borg extract 会将内容写入当前工作目录,因此应先将 cd 恢复到临时目录。

如果您还没有在两者之间做出选择,restic 与 Borg 的比较介绍了去重和 append-only 仓库,以及该属性如何防止已被入侵的服务器删除自己的备份历史。使用任一工具都可以。错误的选择是两者都不运行。

新服务器刚部署时是设置备份的最佳时机,因为此时服务器上还没有值得丢失的数据。新 VPS 上线后的前十分钟介绍了这项工作,以及 SSH 和防火墙配置。

然后在日历中设置周期性提醒:每月从仓库恢复一个目录到 /tmp,并读取其中的文件。这个习惯的价值超过本页介绍的所有工具。

FAQ

可以恢复 ext4 上被删除的文件吗?

通常不能。文件的最后一个硬链接被删除后,ext4 会从 inode 中清除 extent tree,因此磁盘上不再记录数据原来的位置。extundeleteext4magic 会在 ext4 日志中搜索该 inode 的较早副本,但只有在删除发生于几分钟前、且此后文件系统一直处于空闲状态时才可能有用。这两个项目目前都无人积极维护。请对未挂载的设备或磁盘映像运行其中任意一个工具,绝不要对已挂载的文件系统运行。先使用 sudo dumpe2fs -h /dev/vdb1 | grep -i journal 检查目标。

某个服务仍打开着已删除的文件。我能恢复它吗?

可以,这是最理想的情况。进程保持文件打开时,其 inode 和数据块仍处于已分配状态,因此仍可读取数据。不要重启服务,因为关闭最后一个文件描述符会完成删除操作。运行 sudo lsof +L1,列出链接计数为 0 的打开文件,记下 PID 和文件描述符编号,然后使用 /proc 配合 sudo cp /proc/1234/fd/3 /mnt/rescue/events.log 进行复制。请将副本写入其他文件系统。使用 mem 而不是文件描述符编号显示的条目属于内存映射文件,没有可用于复制的 /proc/<pid>/fd 路径。

为什么应该先创建磁盘映像,而不是直接在磁盘上运行恢复工具?

因为工具都必须将输出写入某个位置,而向正在恢复的文件系统写入数据,可能会覆盖仍保存着数据的空闲块。先使用 sudo ddrescue -n /dev/vdb1 /mnt/rescue/vdb1.img /mnt/rescue/vdb1.map 将分区复制到其他设备,再让 photorec 处理映像文件。映像还允许您稍后使用第二个工具,对完全相同的数据字节再次尝试恢复;一旦原始磁盘被覆盖,就无法做到这一点。

rm -rf / 仍会摧毁 Linux 系统吗?

直接运行该命令不会。GNU rm 会拒绝执行,并输出 rm: it is dangerous to operate recursively on '/'。真正危险的是通过其他方式构造出的命令。rm -rf "$TARGET"/*TARGET 未设置时会展开为 rm -rf /*,shell 会将真实的顶层目录列表传给 rm,其中不包含 /,因此保护机制不会触发。请改写为 "${TARGET:?TARGET is not set}",这样 shell 会在 rm 运行前停止。

文件系统快照算备份吗?

不算。btrfs 或 ZFS 快照与受保护的数据位于同一个存储池中,因此磁盘故障或存储池损坏会同时影响两者。LVM 快照还有一个问题:大小固定。快照空间用尽后,内核会使其失效,其中的内容也会丢失。快照非常适合撤销两分钟前发生的删除操作。除此之外,请在独立硬件上保留备份存储库。

#linux#rm#data-recovery#备份#ext4