virtiofs ENFILE 问题排查手册
六月 13, 2026 [linux, virtualization] #virtiofs #debug #qemu #libvirt #fuse #file-descriptorvirtiofs ENFILE 问题排查手册
问题现象
VM 内执行文件操作时报错:
ls: 无法打开目录 '.': 系统中打开的文件过多
# 对应系统错误: ENFILE (Too many open files in system)
排查步骤
1. 系统级文件句柄状态
cat /proc/sys/fs/file-nr
# 输出格式: <已分配数量> <空闲数量> <最大限制>
# 如果第 2 列 = 0 表示文件表已耗尽
cat /proc/sys/fs/file-max
cat /proc/sys/fs/nr_open
2. 定位 virtiofsd 进程
pgrep -a virtiofsd
ps -o pid,ppid,user,etime,rss,args -p $(pgrep -d',' virtiofsd)
# rss: 内存占用 etime: 运行时长
3. 检查 virtiofsd fd 数量
# 精确计数
sudo -A ls /proc/<PID>/fd | wc -l
# 间接估算(fd 目录字节数 ≈ 30×fd数)
stat /proc/<PID>/fd | grep Size
4. 查看 fd 类型分布
sudo -A ls -la /proc/<PID>/fd/ | awk '{
target=$NF
if (target ~ /^socket:/) type="socket"
else if (target ~ /^pipe:/) type="pipe"
else if (target ~ /^anon_inode:/) type="anon_inode"
else if (target ~ /\(deleted\)$/) type="deleted_file"
else if (target ~ /^\//) type="regular_file"
else if (target ~ /^\/dev\//) type="dev"
else type="other"
count[type]++
} END { for(t in count) print count[t], t }' | sort -rn
5. 查看 fd 指向哪些目录
sudo -A ls -la /proc/<PID>/fd/ | awk '{print $NF}' | \
sed 's|/home/peter/project||' | \
awk -F'/' '{print $1"/"$2"/"$3"/"$4}' | \
sort | uniq -c | sort -rn | head -30
6. 采样查看 fd 具体内容
# 前 40 个
sudo -A ls -la /proc/<PID>/fd/ | head -40
# 中间段
sudo -A ls -la /proc/<PID>/fd/ | sed -n '5000,5020p'
# 末尾
sudo -A ls -la /proc/<PID>/fd/ | tail -20
7. 检查 virtiofsd 内存和资源限制
for pid in <PID_LIST>; do
echo "=== PID $pid ==="
cat /proc/$pid/limits | grep -E "open files|processes"
cat /proc/$pid/status | grep -E "VmRSS|VmSize|Threads"
done
8. 检查 FUSE 连接参数
ls /sys/fs/fuse/connections/
for conn in /sys/fs/fuse/connections/*/; do
echo "=== $(basename $conn) ==="
cat "$conn/max_background" 2>/dev/null
cat "$conn/congestion_threshold" 2>/dev/null
cat "$conn/waiting" 2>/dev/null
done
# max_background 默认 12,congestion_threshold 默认 9
9. 检查 QEMU 进程
ps aux | grep -E "qemu|libvirt" | grep -v grep
for pid in <QEMU_PID_LIST>; do
stat /proc/$pid/fd | grep Size
done
10. 全系统 fd 统计
total=0
for p in /proc/[0-9]*/fd/; do
total=$((total + $(ls "$p" 2>/dev/null | wc -l)))
done
echo "用户可见: $total"
echo "系统分配: $(awk '{print $1}' /proc/sys/fs/file-nr)"
echo "差值(root): $(( $(awk '{print $1}' /proc/sys/fs/file-nr) - total ))"
11. 查找 fd 最大的进程
for p in /proc/[0-9]*/fd/; do
size=$(stat -c%s "$p" 2>/dev/null)
[ "$size" -gt 1000 ] 2>/dev/null && \
echo "Size=$size PID=$(basename $(dirname $(dirname $p))) Name=$(cat /proc/$(basename $(dirname $(dirname $p)))/comm 2>/dev/null)"
done | sort -t= -k2 -rn | head -10
根因分析
第一层:fd 泄漏链
debian13 VM 内某个进程 (IDE/编译器/文件搜索)
→ 遍历 /home/peter/project 下数十万文件
→ virtiofsd 为每个 open() 在宿主机分配一个真实 fd
→ 数小时内累积数十万 fd + 数 GB 内存
→ 宿主机 file-nr 耗尽 (allocated=1,015,480, free=0)
→ 其他 VM 尝试访问 virtiofs → ENFILE
第二层(真根因):fs.file-max 设为 INT64_MAX 触发内核溢出
这不是简单的"文件开太多"问题,真正的根因是 fs.file-max 被设成了 9223372036854775807(INT64_MAX)。
内核分配文件描述符时,会动态扩展 fdtable(文件描述符表)。扩展逻辑大致是:以当前 files_stat.max_files 为上限,按倍数扩容(n = last * 2 + 1)。
当 file-max = INT64_MAX 时:
- 整数溢出:扩容公式
n = (last * 2 + 1)在极端大的上限下,乘法结果溢出回绕成非法值 - 分配失败:内核尝试分配巨大的 fd table 数组(几十 GB 的连续内存),
kvmalloc返回NULL - 返回 ENFILE:分配失败后内核认为"系统范围文件表已满",返回
ENFILE— 但实际上文件表远没满
因此即使系统只打开了 14 万个文件(远未达到真实承载能力),只要某个进程需要扩展自己的 fd table,就会触发溢出 → 返回 ENFILE。
判断标准
| 症状 | 结论 |
|---|---|
fs.file-max ≈ 9223372036854775807 | 核心问题:触发内核 fd table 溢出 |
file-nr 第 2 列 ≈ 0 | 文件表已饱和 |
| virtiofsd fd 总数 > 10 万 | 严重泄漏 |
| fd 99% 为常规文件 | VM 内进程遍历文件系统 |
fd 集中在 build/cache、node_modules、target/debug 等 | 构建工具或 IDE 扫描 |
| virtiofsd RSS > 数 GB | 内存与 fd 同步泄漏 |
修复方案
主要修复:设定合理的 fs.file-max
# 在 VM 内执行(立即生效)
sudo sysctl -w fs.file-max=2097152
sudo sysctl -w fs.nr_open=2097152
fs.file-max 只是一个上限,不会预分配任何资源。设成 2097152(200 万)在内核内部数据结构的安全范围内,扩容计算不会溢出,分配也不会失败。200 万已经非常充裕,一般 VM 用到 10~20 万。
持久化:
cat << 'EOF' | sudo tee /etc/sysctl.d/99-file-max.conf
fs.file-max = 2097152
fs.nr_open = 2097152
EOF
sudo sysctl --system
注意:
fs.file-max太大反而有害。INT64_MAX 会导致内核 fd table 扩容时整数溢出,明明还有余量却报 ENFILE。这是 Linux 内核 6.x 的已知行为。
临时:释放 fd
# 重启 virtiofsd(会短时断开 VM 内挂载)
sudo kill -HUP <virtiofsd_PID>
VM 内:清理缓存
# 创建定时脚本
cat > /usr/local/bin/manage_virtiofs_cache.sh << 'EOF'
#!/bin/bash
CACHED_MB=$(free -m | awk '/^Mem:/ {print $6}')
if [ $CACHED_MB -gt 4096 ]; then
sync
echo 3 > /proc/sys/vm/drop_caches
fi
EOF
chmod +x /usr/local/bin/manage_virtiofs_cache.sh
# crontab: 每 30 分钟执行
# */30 * * * * /usr/local/bin/manage_virtiofs_cache.sh
VM 内:调整内核参数
echo 1000 > /proc/sys/vm/vfs_cache_pressure
echo 1000 > /proc/sys/vm/dirty_writeback_centisecs
# 持久化
cat >> /etc/sysctl.conf << EOF
vm.vfs_cache_pressure=1000
vm.dirty_writeback_centisecs=1000
EOF
永久:修改 libvirt 配置
<filesystem type='mount' accessmode='passthrough'>
<driver type='virtiofs' queue='1024'/>
<source dir='/home/peter/project'/>
<target dir='project'/>
<binary path='/usr/libexec/virtiofsd' xattr='on'>
<cache size='8192'/>
</binary>
</filesystem>
核心排查逻辑
file-nr 异常高
→ ps 找 virtiofsd 进程
→ ls /proc/<pid>/fd | wc -l 数 fd
→ ls -la /proc/<pid>/fd/ 看类型
→ awk 按目录聚合 找来源
→ 定位 VM 内是什么进程在遍历文件