很多Linux用户认为rm命令执行后文件就彻底消失了,但实际上rm只是从目录树中移除了文件的目录项(directory entry),并将文件的链接计数(link count)减1。
恢复的关键窗口期: 只要有任何进程仍然保持着该文件的打开文件描述符(file descriptor),内核就会保留文件的inode和数据块。只有当最后一个持有该文件描述符的进程关闭它或退出后,内核才会将磁盘块标记为空闲,此时数据才真正被“删除”。
这意味着:如果正在运行的服务(如Web服务器、数据库、日志收集器)仍在持续写入你刚刚误删的文件,那么数据仍然完好地保存在磁盘上,并且可以通过/proc文件系统中的特殊路径访问。
1. 找到持有文件的进程
恢复误删文件的第一步,是找到哪个进程仍然保持着该文件的打开句柄。这里需要使用lsof(list open files)命令。
安装lsof:
| 发行版 | 安装命令 |
|---|
| sudo apt install lsof |
| RHEL / CentOS / Fedora / Rocky / AlmaLinux | sudo dnf install lsof |
搜索已删除的文件:
sudo lsof | grep deleted
输出示例:
nginx 1423 www-data 4w REG 253,1 204800 131074 /var/log/nginx/access.log (deleted)
rsyslogd 1201 root 7w REG 253,1 819200 131075 /var/log/syslog (deleted)
关键字段解读:
- 第4列(FD):文件描述符编号(如
4w表示写打开的描述符4) - 最后一列:文件路径,
(deleted)标记表示该文件已被删除
如果普通的grep deleted没有找到目标文件,可以尝试直接列出所有链接计数小于1的文件:
sudo lsof +L1
输出示例:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINKS NODE NAME
nginx 1423 www-data 4w REG 253,1 204800 0 131074 /var/log/nginx/access.log (deleted)
注意:NLINKS列显示0,确认目录项已消失,但内核仍持有数据。
2. 通过 /proc/fd 恢复文件
Linux内核为每个进程将打开的文件描述符暴露在/proc/<PID>/fd/目录下,每个描述符显示为指向原文件路径的符号链接——即使文件已被删除。
从上面的lsof输出可知:nginx进程的PID为1423,文件描述符为4。因此,仍存活的数据路径为:/proc/1423/fd/4
使用cp命令复制恢复:
sudo cp /proc/1423/fd/4 /var/log/nginx/access.log.recovered
验证恢复的文件:
ls -lh /var/log/nginx/access.log.recovered
输出示例:
-rw-r--r-- 1 root root 200K May 6 03:14 /var/log/nginx/access.log.recovered
如果看到文件大小符合预期,说明数据已完整恢复。之后可以将文件移回原路径,或交给需要的工具处理。
注意事项:
- 如果进程仍在向已删除的描述符写入数据,它仍然写入
/proc/<PID>/fd/4,但不会写入你刚复制出来的文件(那是复制时的快照)。 - 恢复后建议重启相关服务,使其重新打开正常的链接文件。
- 如果
Permission denied,使用sudo执行,或确认PID是否还存在:
ps aux | grep PID
3. 快速恢复Shell脚本
半夜定位生产问题时,手动输入多个命令容易出错。以下脚本将整个查找和复制过程封装成一步:
recover_deleted() {
local filename="$1"
local output="${2:-/tmp/recovered_file}"
local result
result=$(sudo lsof +L1 2>/dev/null | grep "$filename")
if [[ -z "$result" ]]; then
echo"No process holds $filename open. Data may already be gone."
return 1
fi
local pid fd
pid=$(echo"$result" | awk 'NR==1{print $2}')
fd=$(echo"$result" | awk 'NR==1{print $4}' | tr -d 'rwu')
echo"Found: PID=$pid FD=$fd"
sudo cp /proc/"$pid"/fd/"$fd""$output" && echo"Recovered to $output"
}
使用方法:
# 添加到 ~/.bashrc 或共享的运维配置文件后
source ~/.bashrc
recover_deleted /var/log/nginx/access.log /var/log/nginx/access.log.recovered
输出示例:
Found: PID=1423 FD=4
Recovered to /var/log/nginx/access.log.recovered
脚本逻辑说明:
awk 'NR==1':取第一个匹配结果(避免多个进程持有同一文件)tr -d 'rwu':去除文件描述符字段中的读/写/锁后缀,留下干净的整数
技巧: 如果有多个进程持有同一已删除文件,建议从文件大小最大的进程恢复(数据更完整):
sudo ls -lh /proc/<PID>/fd/<FD>
4. 如果进程已关闭文件怎么办?
一旦所有持有该文件的进程都退出或关闭了描述符,内核就会释放磁盘块,数据从/proc中消失。此时进入了硬件恢复领域:
| 文件系统 | 推荐工具 |
|---|
| extundelete |
| extundelete、debugfs |
| testdisk、photorec |
这些工具的局限性:
- 必须在只读挂载的文件系统上运行,否则可能覆盖要恢复的数据块
警告:extundelete或任何恢复工具,绝对不要在读写挂载的文件系统上运行。请先卸载分区,或从Live USB启动,否则文件系统可能覆盖你正要恢复的数据块。
5. 预防措施:硬链接或Bind Mount
如果管理一个服务,该服务向单个路径写入日志,并且希望日志能扛住一次误操作rm,最干净的解决方案是硬链接。
硬链接提供额外的一条目录项指向同一个inode,因此单次rm不会使链接计数降至0。
创建硬链接:
ln /var/log/nginx/access.log /var/log/nginx/access.log.hardlink
验证指向同一个inode:
ls -li /var/log/nginx/access.log /var/log/nginx/access.log.hardlink
输出示例:
131074 -rw-r--r-- 2 www-data www-data 204800 May 6 03:14 /var/log/nginx/access.log
131074 -rw-r--r-- 2 www-data www-data 204800 May 6 03:14 /var/log/nginx/access.log.hardlink
两处入口显示相同的inode号(131074),链接计数为2。此时执行rm /var/log/nginx/access.log只会使链接计数降为1,数据在硬链接路径下仍然完好无损。
限制: 硬链接只能在同一文件系统内工作。如需跨文件系统保护,可以使用mount --bind实现类似效果。
6. 总结与实战练习
/proc/<PID>/fd/路径是Linux中一个非常实用的机制,掌握后你会发现rm不再是不可逆的命令。
关键步骤回顾:
(1)使用sudo lsof +L1找到持有已删除文件的进程PID和文件描述符FD
(2)通过cp /proc/<PID>/fd/<FD> recovered_file复制出数据
(3)恢复后重启相关服务,使其重新打开正常文件
(4)考虑使用硬链接作为长期预防措施
实战练习:
现在打开你的Linux终端,亲自尝试:
(1)创建一个测试文件
(2)在一个终端执行tail -f持续读取该文件
(3)在另一个终端执行rm删除该文件
(4)运行lsof +L1 | grep 测试文件名找到存活的描述符
(5)用cp /proc/<PID>/fd/<FD> /tmp/recovered恢复文件
(6)验证恢复的内容与原文件一致