ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux文件占用问题排查与解决方案

Linux文件占用问题排查与解决方案

1. 问题场景与排查思路

当你在Linux服务器上尝试删除或移动某个文件时,突然弹出"Device or resource busy"的报错,这种场景对于系统管理员来说再熟悉不过了。上周我在清理一台运行了3年的日志服务器时就遇到了典型情况:想要归档/var/log/nginx下的access.log文件时,系统顽固地拒绝配合。这种文件占用问题背后通常隐藏着三类"嫌疑犯":

  • 某个正在运行的进程持有了该文件的描述符(最常见情况)
  • 文件被挂载到了某个特殊位置(比如通过mount --bind)
  • 文件系统本身出现了异常(最棘手但较少见)

经验之谈:遇到文件占用问题时,90%的情况都是第一种原因导致的。这也是为什么lsof命令会成为排查这类问题的瑞士军刀。

2. 核心排查工具链详解

2.1 lsof命令的深度使用

lsof(list open files)是排查文件占用的首选工具。但大多数人只停留在简单的lsof /path/to/file用法,其实它有许多进阶技巧:

# 基础用法:查看谁在占用文件 $ lsof /var/log/nginx/access.log # 显示完整进程信息(包括父进程) $ lsof -a +c 15 /var/log/nginx/access.log # 递归查看目录下所有被打开的文件 $ lsof +D /var/log/nginx/ # 查看特定用户打开的文件 $ lsof -u www-data # 结合grep过滤TCP连接 $ lsof -i TCP:80

输出字段解析示例:

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME nginx 12345 www-data 4w REG 253,0 1024000 1234567 /var/log/nginx/access.log

其中FD列的"4w"表示该进程以写模式(w)打开了文件描述符4。如果是"4u"表示读写模式,"4r"为只读。

2.2 fuser命令的实战技巧

当lsof不可用时(比如最小化安装的系统),fuser是很好的替代方案:

# 查看占用文件的进程 $ fuser -v /var/log/nginx/access.log # 强制解除占用(发送SIGKILL) $ fuser -km /var/log/nginx/access.log # 显示占用方式(读写/执行) $ fuser -c /mnt/nfs_share

危险警告:-km参数会直接杀死相关进程,生产环境慎用!建议先用-v查看后再决定是否kill。

2.3 /proc文件系统的妙用

Linux的/proc虚拟文件系统是个宝库,所有进程的文件描述符信息都保存在这里:

# 查找所有打开特定文件的进程 $ grep -l "/var/log/nginx/access.log" /proc/*/fd/* 2>/dev/null # 查看进程12345打开的所有文件 $ ls -l /proc/12345/fd # 通过inode反向查找文件名 $ find /proc/*/fd -ls | grep 1234567

这种方法特别适合在容器环境中使用,因为容器内可能没有安装lsof等工具。

3. 典型场景的排查流程

3.1 日志文件被占用无法轮转

这是最常见的运维场景,以Nginx日志为例的完整排查流程:

  1. 确认文件确实被占用:

    $ rm /var/log/nginx/access.log rm: cannot remove 'access.log': Device or resource busy
  2. 使用lsof定位进程:

    $ lsof /var/log/nginx/access.log nginx 12345 www-data 4w REG 253,0 1024000 1234567 /var/log/nginx/access.log
  3. 分析进程树关系:

    $ pstree -p 12345 nginx(12345)─┬─nginx(12346) └─nginx(12347)
  4. 优雅重启服务(比直接kill更安全):

    $ systemctl reload nginx # 或者向master进程发送USR1信号 $ kill -USR1 12345

专业技巧:对于日志类文件,更好的做法是使用logrotate配置copytruncate参数,避免直接操作原始文件。

3.2 卸载存储设备时报target is busy

当umount失败时,可以这样排查:

# 查看哪些进程在访问挂载点 $ lsof +f -- /mnt/nas_volume # 或者使用fuser $ fuser -vm /mnt/nas_volume # 如果是NFS挂载,还需检查rpcbind $ rpcinfo -p # 最后再尝试延迟卸载 $ umount -l /mnt/nas_volume

3.3 容器环境下的特殊处理

在Docker/K8s环境中,文件占用问题更复杂:

# 找出所有访问该文件的容器 $ for d in /proc/[0-9]*/root; do if [ -e "$d/var/log/nginx/access.log" ]; then pid=${d#/proc/} pid=${pid%/root*} echo "PID $pid ($(cat /proc/$pid/comm))" fi done # 在K8s中通过crictl查找 $ crictl ps -q | xargs -I{} crictl inspect {} | jq 'select(.info.runtimeSpec.mounts[].destination=="/var/log")'

4. 高级排查与预防措施

4.1 内核级文件锁定分析

当常规工具查不出原因时,可能需要深入内核:

# 查看文件锁状态 $ cat /proc/locks | grep 1234567 # 使用fatrace监控文件访问 $ fatrace | grep access.log # 使用strace跟踪特定进程 $ strace -p 12345 -e trace=file

4.2 自动化监控方案

对于关键文件,可以设置实时监控:

# 使用inotifywait监控文件事件 $ inotifywait -m /var/log/nginx/access.log # 结合auditd进行审计 $ auditctl -w /var/log/nginx/access.log -p war -k nginx_log_access

4.3 预防文件占用的最佳实践

  1. 重要操作前先检查:

    if lsof $file >/dev/null; then echo "文件被占用,请先处理!" exit 1 fi
  2. 使用flock进行脚本互斥:

    ( flock -x 200 # 你的临界区代码 ) 200>/var/lock/myscript.lock
  3. 日志文件处理采用更安全的方式:

    access_log /var/log/nginx/access.log main buffer=32k flush=5m;
  4. 定期检查异常文件占用:

    # 查找打开文件数异常的进程 $ ps -eo pid,nlwp,ruser,pcpu,pmem,cmd --sort=-nlwp | head

5. 疑难杂症解决方案

5.1 幽灵文件占用问题

有时即使kill了所有进程,文件仍显示被占用。这可能是因为:

  1. 内核模块持有引用:

    $ lsmod $ rmmod suspect_module
  2. 文件系统缓存未释放:

    $ sync; echo 3 > /proc/sys/vm/drop_caches
  3. NFS客户端缓存:

    $ umount -f /mnt/nfs

5.2 无法删除的已删除文件

当lsof显示某个进程持有已删除文件的描述符时:

# 查找所有已删除但仍被打开的文件 $ lsof | grep deleted # 通过procfs清空文件内容 $ > /proc/12345/fd/4 # 或者直接关闭文件描述符 $ gdb -p 12345 -ex "call close(4)" -batch

5.3 容器文件系统特殊问题

在overlayfs上可能会遇到这样的报错:

rm: cannot remove 'file': Stale file handle

解决方法:

# 查找所有挂载点 $ findmnt -t overlay # 强制卸载后重新挂载 $ umount -l /var/lib/docker/overlay2/xxx $ mount -a

6. 性能优化与工具增强

6.1 加速lsof的技巧

在大规模系统上,lsof可能运行缓慢。可以:

# 只检查特定文件系统 $ lsof /dev/sda1 # 使用更快的替代工具 $ sudo apt install psmisc $ fuser -vm /path # 预构建缓存(适用于频繁查询) $ lsof -b -w -n > lsof.cache

6.2 自定义监控脚本

这是一个实时监控关键文件占用的脚本:

#!/bin/bash monitor_file() { while true; do if lsof "$1" >/dev/null 2>&1; then echo "$(date) - $1 is in use by:" lsof "$1" | awk '{print $1,$2,$3}' | sort | uniq fi sleep 5 done } monitor_file /var/log/nginx/access.log

6.3 内核参数调优

对于高并发场景下的文件描述符问题:

# 增加系统级文件描述符限制 $ echo "fs.file-max = 1000000" >> /etc/sysctl.conf $ sysctl -p # 调整inotify监控数量 $ echo "fs.inotify.max_user_watches=524288" >> /etc/sysctl.conf # 查看当前使用情况 $ cat /proc/sys/fs/file-nr

7. 可视化分析方案

7.1 使用ncdu分析文件占用

$ ncdu /var/log # 按d删除文件时会自动检查占用情况

7.2 生成文件占用关系图

# 安装graphviz后 $ lsof -n | awk '{print $1,$2,$NF}' | grep -v "(" | dot -Tpng -o usage.png

7.3 使用systemtap深度追踪

对于开发人员,可以使用systemtap脚本追踪文件操作:

# 监控所有open系统调用 $ stap -e 'probe syscall.open { printf("%s %s\n", execname(), filename) }'

8. 不同Linux发行版的特殊处理

8.1 RHEL/CentOS的SELinux影响

当SELinux导致文件操作异常时:

# 检查SELinux上下文 $ ls -Z /var/log/nginx/access.log # 临时修改上下文 $ chcon -t httpd_log_t /var/log/nginx/access.log # 永久修改 $ semanage fcontext -a -t httpd_log_t "/var/log/nginx(/.*)?" $ restorecon -Rv /var/log/nginx

8.2 Ubuntu的AppArmor限制

# 检查当前配置 $ aa-status # 查看具体限制 $ dmesg | grep apparmor # 临时禁用某个profile $ apparmor_parser -R /etc/apparmor.d/usr.sbin.nginx

8.3 Alpine Linux的特殊情况

在Alpine这种使用musl libc的系统中:

# lsof可能需要busybox版本 $ apk add lsof # 或者使用替代方案 $ apk add procps $ ls -l /proc/*/fd/ | grep deleted

9. 历史案例与经验总结

去年处理过一个典型案例:某台服务器上的/tmp目录无法写入,但df显示空间充足。最终发现是某个异常进程持有数万个已删除文件的描述符,导致inode耗尽。排查过程:

  1. 使用df -i确认inode使用率100%
  2. lsof +L1列出所有链接数为0的文件
  3. 发现某个Java进程持有大量/tmp目录下的临时文件
  4. 联系开发团队修正了未正确关闭文件流的代码

关键教训:

  • 定期检查df -i而不仅仅是df -h
  • 关键业务进程应实现文件描述符泄漏检测
  • /tmp目录最好单独分区并设置定期清理

另一个有趣案例:某次NFS挂载点无法卸载,即使fuser显示没有进程占用。最终发现是automount服务在后台重新挂载。解决方法:

$ systemctl stop autofs $ umount /mnt/nfs $ systemctl start autofs
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进