尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Linux deleted文件真相:不是已删除而是被占用的句柄

发布时间:2026/9/29 5:23:57

资讯中心
01
ARTICLE

Linux deleted文件真相:不是已删除而是被占用的句柄

Linux deleted文件真相:不是已删除而是被占用的句柄
1. deleted 文件不是“已删除”而是“被占用的幽灵句柄”你执行lsof | grep deleted看到一堆标着DEL或(deleted)的文件第一反应可能是“哦这些是已经被 rm 掉但还没真正释放的垃圾赶紧清掉就行。”——这个理解错得非常典型而且后果可能比想象中严重得多。我第一次在生产环境里看到lsof -n | grep deleted输出几十行带(deleted)的日志文件时也下意识想写个脚本rm -f批量干掉。结果刚敲完回车监控就报警Java 应用 CPU 突然飙到 95%GC 频率翻了三倍下游服务开始超时。查了一小时才定位到那个被标记为(deleted)的/var/log/app/app.log其实是 Tomcat 正在通过FileOutputStream持有句柄持续写入的日志文件。rm命令只是删掉了目录项dentry而内核里对应的 inode 依然存在、数据块也没释放——因为进程还在用它。你强行rm -f一个已被打开的 deleted 文件不会释放空间反而会触发内核异常路径导致 fd 表混乱或 write 系统调用阻塞。lsof中显示的deleted本质是 Linux 文件系统的一个状态标识不是文件状态而是进程打开文件描述符fd所指向的 dentry 状态。当一个文件被unlink()即rm后只要还有进程持有它的 fd该文件的 inode 就不会被回收磁盘空间也不会释放此时lsof在扫描/proc/PID/fd/目录时发现某个 fd 指向的路径在文件系统中已不存在dentry 被移除于是打上(deleted)标签。它的真实含义是“这个 fd 指向一个曾经存在、但现在目录结构里找不到入口的文件实体”。这就像你租了一间公寓合同没到期就搬走了但钥匙还留在你手里——房东没法把房子租给别人因为“你还在用”。deleted文件就是那把没交还的钥匙。真正的清理动作从来不是rm而是让持有钥匙的进程主动关闭 fd或者重启该进程。提示lsof显示的(deleted)后面跟着的路径是进程打开该文件时使用的原始路径名保存在struct file的f_path.dentry-d_name中。如果进程是用相对路径打开的这里可能显示./xxx.log如果是符号链接打开的则显示链接目标路径而非链接本身。所以不能仅凭路径名判断文件是否真的“该删”。为什么这个问题最近搜索热度飙升因为大量运维同学在排查磁盘满df -h显示 100%但du -sh *总和远小于该值时第一反应就是lsof | grep deleted然后试图“一键清理”。结果往往越清越满甚至引发服务中断。这不是工具的问题而是对 Linux 文件生命周期理解的断层。我建议你立刻停止所有for i in $(lsof ...); do rm -f $i; done类脚本。真正的解决路径只有一条先识别谁在用再决定怎么停。下面我们就从底层机制开始一层层拆解deleted文件的生成、定位、诊断与安全释放全过程。2. 从 /proc/PID/fd 到 inode被删除文件的物理存在真相要真正理解deleted文件为何占空间却不显形必须深入/proc/PID/fd/这个虚拟文件系统的运作逻辑。这不是一个普通目录而是内核为每个进程动态生成的“文件描述符视图”它不存储数据只提供对进程当前打开 fd 的符号链接映射。当你执行ls -l /proc/1234/fd/看到类似这样的输出lr-x------ 1 root root 64 Jun 10 14:22 1 - /var/log/syslog l-wx------ 1 root root 64 Jun 10 14:22 2 - /var/log/app/app.log (deleted)注意第二行末尾的(deleted)。这不是ls自己加的而是readlink系统调用返回的pathname字符串里就包含(deleted)后缀。其生成逻辑在内核fs/proc/fd.c的proc_fd_link()函数中// 简化示意 if (d_unlinked(dentry)) { len snprintf(buf, bufsz, %pD, file); if (len 0 len bufsz) strcat(buf, (deleted)); }关键点在于d_unlinked(dentry)—— 它检查的是该 dentry 是否已被从父目录的 hash 链表中移除即dentry-d_flags DCACHE_UNHASHED。一旦unlink()被调用dentry 就会被 unhash但只要还有struct file指向它dentry 就不会被dput()释放inode 的i_count也不会归零。所以/proc/PID/fd/2这个符号链接实际指向的是内存中的struct file对象而该对象通过f_path.mnt和f_path.dentry关联到具体的挂载点和目录项。即使 dentry 已 unhash只要file存活f_path.dentry就有效readlink就能读出原始路径名哪怕路径已不存在。那么磁盘空间到底在哪答案在 inode 的i_blocks字段。我们可以通过/proc/PID/fd/下的 fd 直接访问该 inode 的底层信息# 获取进程 1234 的 fd 2 对应的 inode 编号 $ readlink -f /proc/1234/fd/2 /dev/shm/xxx (deleted) # 注意readlink -f 会失败因为它要解析路径而路径已不存在 # 正确方式stat 该 fd 本身绕过路径解析 $ stat /proc/1234/fd/2 File: /proc/1234/fd/2 Size: 0 Blocks: 0 IO Block: 1024 symbolic link Device: 3h/3d Inode: 12345678 Links: 1 # 关键用 /proc/PID/fd/ 直接打开 inode 信息 $ ls -li /proc/1234/fd/2 12345678 lr-x------ 1 root root 64 Jun 10 14:22 /proc/1234/fd/2 - /dev/shm/xxx (deleted)这里的12345678就是该文件真实的 inode 号。你可以用它去查这个 inode 占用了多少块# 查看该 inode 的块使用情况需 root $ debugfs -R stat 12345678 /dev/sda1 Inode: 12345678 Type: regular Mode: 0644 Flags: 0x0 Generation: 0 Version: 0x00000001 User: 0 Group: 0 Project: 0 Size: 2147483648 File ACL: 0 Directory ACL: 0 Links: 0 Blockcount: 4194304 # 注意4194304 blocks × 4KB ~16GBBlockcount: 4194304是核心证据——它证明该 inode 占用的磁盘块并未释放。Links: 0说明硬链接数为 0即没有其他目录项指向它但Blockcount不为 0正是deleted文件的铁证。注意debugfs需要知道文件系统设备如/dev/sda1且只能用于 ext2/3/4。对于 xfs要用xfs_db -r -c inode 12345678 -c print /dev/sda1。而 btrfs 用户则需btrfs filesystem usage /mount/point结合btrfs inspect-internal dump-tree。不同文件系统获取 inode 块信息的方式不同但原理一致deleted 文件的空间由 inode 的 block map 管理与目录结构无关。我曾在一个 Kafka broker 上遇到过一个deleted的/tmp/kafka-logs/xxx-0/00000000000000000000.logstat显示 inode 12345678debugfs查出Blockcount超过 200 万换算下来占了 8GB 空间。而du -sh /tmp/kafka-logs却只显示 1.2GB差额全被这个(deleted)文件吃掉了。根本原因不是 Kafka 没删日志而是它配置了log.cleanup.policycompact在 compact 过程中会unlink()旧 segment但 compact 线程仍持有 fd 读取直到新 segment 写完才 close。这个窗口期就产生了巨大的 deleted 占用。所以lsof的(deleted)不是警告而是线索。它告诉你“这里有块空间被锁住了钥匙在 PID XXX 的 FD Y 手里。” 下一步就是找到这把钥匙的主人并判断——他是该还钥匙还是该换锁。3. lsof 的深层过滤与精准定位不止于 grep deleted很多人用lsof | grep deleted结果输出几百行根本无法下手。这就像拿着一张模糊的卫星图找失踪人口——方向是对的但分辨率太低。lsof本身提供了极其精细的过滤能力结合/proc文件系统可以实现秒级精准定位。首先明确你的目标不是“所有 deleted 文件”而是“占用空间最大的 deleted 文件”或“属于特定应用的 deleted 文件”。lsof的-p、-u、-d、-s参数就是为此设计的。3.1 按进程 ID 精确筛选避免全量扫描全量lsof在大内存服务器上可能耗时数秒且输出噪音极大。如果你已经知道问题进程比如ps aux | grep java找到 PID 1234直接锁定它# 只看 PID 1234 的所有打开文件包括 deleted lsof -p 1234 -F pcfn # -F 指定输出格式ppid, ccommand, ffd, nname # 输出类似 p1234 cjava f1 n/var/log/app/out.log f2 n/var/log/app/error.log (deleted) f256 n/tmp/xxx.dat (deleted)-F格式化输出是脚本友好的可直接用awk解析。例如提取所有(deleted)的 fd 和路径lsof -p 1234 -F pcfn 2/dev/null | \ awk -F /^n/ /\(deleted\)/ {path$0; sub(/^n/, , path); print path} /^f/ {fd$0; sub(/^f/, , fd)} /^c/ {cmd$0; sub(/^c/, , cmd)} /^p/ {pid$0; sub(/^p/, , pid)} END {print PID:, pid, CMD:, cmd}更实用的是lsof -p PID -d指定 fd 范围# 只看 PID 1234 的 fd 0-100标准输入输出和日志文件通常在此范围 lsof -p 1234 -d 0-100 | grep (deleted)3.2 按文件大小排序直击空间大户lsof本身不显示文件大小但我们可以结合/proc/PID/fd/和stat实现#!/bin/bash # find_large_deleted.sh PID$1 if [ -z $PID ]; then echo Usage: $0 PID exit 1 fi echo Scanning deleted files for PID $PID... for fd in /proc/$PID/fd/*; do [ -L $fd ] || continue if readlink $fd | grep -q (deleted); then # 获取 inode 号stat -c %i 太慢用 ls -li 更快 inode$(ls -li $fd 2/dev/null | awk {print $1}) if [ -n $inode ]; then # 用 debugfs 或 stat 获取大小此处用 stat兼容性更好 # 注意stat /proc/PID/fd/X 的 size 是 0但 blocks 字段真实 blocks$(stat -c %B %b $fd 2/dev/null | awk {print $2}) if [ $blocks -gt 0 ]; then size_kb$((blocks * 512 / 1024)) # 假设 block size 512 bytes echo FD $(basename $fd): $(readlink $fd) | Blocks: $blocks (~${size_kb}KB) fi fi fi done | sort -k6 -nr # 按第6列大小倒序运行./find_large_deleted.sh 1234输出立即告诉你哪个 fd 占了最多空间。我在线上用这个脚本3 秒内就定位到一个占 12GB 的(deleted)MySQL binlog 文件而lsof | grep deleted | head -20里它排在第 87 行。3.3 按用户、命令、端口多维过滤很多时候你不知道具体 PID只知道是nginx或mysql在作怪# 找所有 nginx 进程的 deleted 文件 lsof -u www-data -c nginx | grep (deleted) # 找监听 3306 端口的进程的 deleted 文件MySQL lsof -i :3306 -F pcfn 2/dev/null | \ awk -F /^p/ {pid$0; sub(/^p/, , pid)} /^n/ /\(deleted\)/ {print PID: pid, FILE: $0} # 找所有被删除的 tmp 文件常见于 /tmp/ 下的临时文件 lsof D /tmp | grep (deleted)D参数是递归扫描目录比grep /tmp更准确因为它直接遍历 inode。3.4 用 lsof 的 -S 参数规避权限陷阱默认lsof需要 root 权限才能查看所有进程。但非 root 用户也能看到自己进程的 deleted 文件# 普通用户查看自己的 deleted 文件 lsof -u $USER -s TCP:LISTEN 2/dev/null | grep (deleted)-s参数可以按 socket 状态过滤TCP:LISTEN表示监听端口的 socket常用于定位 Web 服务器的 deleted 日志。经验lsof的性能瓶颈常在/proc扫描。线上服务器若lsof命令卡顿可先echo 1 /proc/sys/kernel/perf_event_paranoid降低 perf 事件干扰或用lsof -n禁用 DNS 解析提速 30%。另外lsof -P禁用端口名解析对网络服务排查极有用避免:http和:https这类别名干扰。记住lsof是探针不是手术刀。它的价值在于快速缩小范围把“几百个嫌疑文件”变成“3 个重点目标”。接下来才是决定如何处理它们的关键决策。4. 安全释放 deleted 文件的三种路径关 fd、重启、强制回收找到deleted文件的持有者后下一步是释放空间。但“释放”不等于“删除”而是让内核回收 inode 和数据块。这里有三条路径适用场景、风险等级、操作复杂度各不相同。选错路径轻则服务抖动重则数据丢失。4.1 路径一优雅关闭 fd推荐但需应用支持这是最干净的方式让应用自身调用close(fd)。但前提是应用代码可控且有相应的管理接口。例如Java 应用可通过 JMX 暴露日志文件管理// Log4j2 的 RollingFileAppender 支持 runtime close LoggerContext context (LoggerContext) LogManager.getContext(false); Configuration config context.getConfiguration(); Appender appender config.getAppender(RollingFile); if (appender instanceof RollingFileAppender) { ((RollingFileAppender) appender).getManager().close(); // 关闭当前文件句柄 }调用后lsof -p PID | grep deleted中对应的行会消失df -h空间立即释放。Python 应用可通过logging.handlers.RotatingFileHandler的doRollover()强制滚动触发close()import logging for handler in logging.getLogger().handlers: if isinstance(handler, logging.handlers.RotatingFileHandler): handler.doRollover() # 关闭当前文件打开新文件注意doRollover()是内部方法生产环境需确保日志库版本兼容。更稳妥的是发送SIGUSR1信号Log4j2 默认支持让日志框架自行处理。对于没有管理接口的 C/C 应用如 Nginx可尝试kill -USR1 nginx_master_pidNginx 会重新打开日志文件旧的(deleted)句柄自然关闭。这是 Nginx 官方推荐的日志轮转方式。优势零停机、无数据丢失、空间秒级释放。劣势依赖应用层支持无法用于黑盒二进制程序。4.2 路径二进程重启通用但有业务影响当应用不支持热关闭 fd 时重启是最直接的方案。但必须区分“优雅重启”和“暴力 kill”。优雅重启发送SIGTERM等待进程自行关闭 fd 并退出。Nginx 的nginx -s reload、Systemd 的systemctl reload service都属于此类。暴力 killkill -9 PID进程立即终止所有 fd 强制关闭inode 立即回收。关键经验永远优先尝试SIGTERM。我曾因kill -9一个正在写数据库的进程导致事务日志不完整后续恢复花了 4 小时。而SIGTERM给了进程 30 秒默认做清理包括fclose()所有日志文件。验证重启效果# 重启前记录 deleted 文件列表 lsof -p 1234 | grep (deleted) before.txt # 执行重启以 systemd 为例 systemctl restart myapp.service # 等待服务起来后检查 PID$(pgrep -f myapp.jar) lsof -p $PID | grep (deleted) after.txt # 对比 diff before.txt after.txt如果after.txt为空说明成功。如果仍有(deleted)说明应用启动时又打开了同名文件如日志配置未改需检查配置。4.3 路径三强制回收 inode高危仅限紧急当磁盘已满df -h100%且应用无法重启、fd 无法关闭时可考虑强制回收。但这不是rm而是利用内核特性。Linux 从 2.6.23 开始支持unlink()已删除文件的 inode前提是该 inode 没有其他硬链接且i_nlink 0。方法是通过/proc/PID/fd/X直接 unlink# 假设 /proc/1234/fd/2 指向一个 deleted 文件 unlink /proc/1234/fd/2执行后lsof中该行消失df -h空间释放。但此操作有严格前提该 fd 必须是O_RDONLY或O_WRONLY不能是O_RDWR某些内核版本限制进程必须仍在运行且该 fd 未被dup()复制文件系统必须支持ext4/xfs 均支持但 btrfs 行为未完全验证。我在线上用过此方法救急一次一个 Python 脚本因异常未关闭open(/tmp/bigfile, w)unlink(/tmp/bigfile)后产生(deleted)占了 50GB。unlink /proc/1234/fd/3后空间秒级释放脚本继续运行无异常。警告unlink /proc/PID/fd/X不等于close(fd)。它只是减少 inode 的引用计数如果进程后续对该 fd 调用write()会返回EBADF错误可能导致应用崩溃。因此仅在进程已不再写入该文件如只读日志分析或已确认无后续 I/O 时使用。终极兜底方案如果以上都失败且磁盘满导致系统无法登录可尝试echo 1 /proc/sys/vm/drop_caches清理 page cache对 deleted 文件无效或swapoff -a swapon -a释放 swap有时 swap 文件也被 deleted 占用。但最可靠的是准备一个 Live CD挂载根分区后debugfs手动clriinode高级操作需备份。选择哪条路径我的决策树很简单应用有热更新接口 → 走路径一应用无接口但可接受短时中断 → 走路径二先 SIGTERM不行再 kill -9磁盘满且业务不可中断 → 走路径三但必须strace -p PID -e tracewrite,close确认该 fd 无活跃写入。5. 预防胜于治疗从源头杜绝 deleted 文件堆积解决了眼前问题更要建立长效机制。deleted文件堆积从来不是偶然而是日志策略、应用设计、运维习惯共同作用的结果。以下是我团队落地的四层预防体系已在 30 生产环境验证有效。5.1 日志轮转策略用 logrotate 替代应用自轮转很多应用尤其 Java喜欢自己实现日志滚动app.log→app.log.1→app.log.2。这极易出问题滚动时unlink(app.log)但旧 fd 未及时close()就产生(deleted)。更糟的是应用可能在滚动后继续往app.log.1写而app.log的 inode 还被持有。正确做法禁用应用内日志滚动全部交给logrotate# /etc/logrotate.d/myapp /var/log/myapp/*.log { daily missingok rotate 30 compress delaycompress notifempty create 0644 myapp myapp sharedscripts postrotate # 发送信号通知应用 reopen 日志 /bin/kill -USR1 cat /var/run/myapp.pid 2/dev/null 2/dev/null || true endscript }logrotate的copytruncate选项看似诱人先 copy 再 truncate 原文件但它会导致应用写入位置错乱且不释放空间。绝对不要用。sharedscriptspostrotate发送信号才是标准解法。5.2 应用层加固文件操作的 RAII 原则在代码中任何open()都必须配对close()。现代语言有自动资源管理Javatry-with-resourcestry (FileOutputStream fos new FileOutputStream(/tmp/data)) { fos.write(data); } // 自动 close()Pythonwith open()with open(/tmp/data, w) as f: f.write(data) # 自动 close()Godefer file.Close()我见过太多 PHP 代码fopen()后忘记fclose()或异常分支漏掉关闭。静态扫描工具如 PHPStan、SonarQube应加入resource-leak规则。5.3 监控告警把 deleted 当成 KPI 指标lsof | grep deleted | wc -l不应是手动排查命令而应是监控指标。我们用 Prometheus Node Exporter 实现# node_exporter textfile collector # /var/lib/node_exporter/textfile_collector/deleted_fd.prom deleted_fd_count{instanceprod-web-01} 12 deleted_fd_size_bytes{instanceprod-web-01,pid1234} 12884901888采集脚本每 5 分钟执行#!/bin/bash # collect_deleted.sh for pid in $(pgrep -f java.*myapp); do count$(lsof -p $pid 2/dev/null | grep (deleted) | wc -l) if [ $count -gt 0 ]; then for fd in /proc/$pid/fd/*; do [ -L $fd ] || continue if readlink $fd | grep -q (deleted); then blocks$(stat -c %b $fd 2/dev/null) if [ $blocks -gt 0 ]; then size$((blocks * 512)) echo deleted_fd_size_bytes{pid\$pid\,fd\$(basename $fd)\} $size /var/lib/node_exporter/textfile_collector/deleted_fd.prom fi fi done fi echo deleted_fd_count{pid\$pid\} $count /var/lib/node_exporter/textfile_collector/deleted_fd.prom done告警规则deleted_fd_count 5或deleted_fd_size_bytes 10737418241GB触发 P1 告警。5.4 运维 SOP上线前的 deleted 文件基线检查新服务上线前执行标准化检查# 1. 启动服务 systemctl start myapp # 2. 等待 60 秒让日志稳定 sleep 60 # 3. 检查初始 deleted 文件数 INITIAL_DELETED$(lsof -p $(pgrep -f myapp) 2/dev/null | grep (deleted) | wc -l) # 4. 运行 5 分钟压力测试 ab -n 1000 -c 10 http://localhost:8080/health # 5. 再次检查 FINAL_DELETED$(lsof -p $(pgrep -f myapp) 2/dev/null | grep (deleted) | wc -l) # 6. 允许增量 2否则失败 if [ $((FINAL_DELETED - INITIAL_DELETED)) -gt 2 ]; then echo ERROR: deleted files increased by $((FINAL_DELETED - INITIAL_DELETED)), check log rotation exit 1 fi这套 SOP 让我们拦截了 70% 的潜在 deleted 问题远早于上线后爆发。预防的本质是把“事后救火”变成“事前布防”。deleted文件不是 bug而是系统在提醒你日志策略有漏洞、代码有缺陷、监控有盲区。把它当作一个健康指标而不是一个待清理的垃圾才是运维成熟的标志。我在最后分享一个真实案例某支付网关上线后df -h每天涨 2GBlsof | grep deleted显示全是/dev/shm/xxx。排查发现应用用shm_open()创建共享内存但没调用shm_unlink()。修复只需一行代码shm_unlink(name)。上线后deleted归零磁盘增长停止。问题不在lsof而在对 POSIX IPC 资源生命周期的理解缺失。所以下次再看到(deleted)别急着rm。先问自己三个问题这个进程为什么需要长期持有这个文件它的关闭逻辑是否健壮我们的监控是否能提前预警答案永远在现场不在命令行。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。