/dev/null | cut..."/>

too many open files 的 eventpoll / signalfd / timerfd 隐藏 fd 排查

9次阅读

lsof -p PID 看不到 eventpoll/signalfd/timerfd 是因它们属于内核匿名文件,仅在 /proc/PID/fdinfo/ 中通过 type: 字段标识,需用 grep -h “type:” /proc/PID/fdinfo/* 2>/dev/NULL | cut -d’ ‘ -f2 | sort | uniq -c 统计。

too many open files 的 eventpoll / signalfd / timerfd 隐藏 fd 排查

为什么 lsof -p PID 看不到 eventpoll / signalfd / timerfd 却报 “too many open files”

因为这些 fd 是内核匿名文件(anonymous file),不挂载在任何路径下,也不出现在 /proc/PID/fd/ 的符号链接目标中——lsof 默认靠解析 /proc/PID/fd/ 下的 symlink 来识别类型,而 eventpoll、signalfd、timerfd 创建的 fd 指向的是 anon_inode:[eventpoll] 这类伪路径,lsof 会跳过或归类为 “unknown”,导致数量被低估。

真正能暴露它们的是 /proc/PID/fdinfo/:每个 fd 在这里都有独立文件,内容含 flagsmnt_id 和关键的 type 字段。例如:

cat /proc/12345/fdinfo/7 pos:    0 flags:  02000002 mnt_id: 12 type:   eventpoll

所以排查必须进 /proc/PID/fdinfo/ 扫描 type: 行,不能只信 lsofls -l /proc/PID/fd/

快速统计 eventpoll / signalfd / timerfd 数量的 shell 命令

直接遍历 /proc/PID/fdinfo/ 并按 type 计数,比写脚本更可靠:

grep -h "type:" /proc/12345/fdinfo/* 2>/dev/null | cut -d' ' -f2 | sort | uniq -c | sort -nr

输出类似:

42 eventpoll      18 timerfd       5 signalfd

注意几点:

  • 2>/dev/null 必须加,因为进程可能在扫描过程中关闭部分 fd,导致 /proc/PID/fdinfo/N 文件消失,触发 warning
  • 某些旧内核(如 3.10)的 fdinfo 不输出 type:,而是用 fanotify: 或无标识,此时需结合 readlink /proc/PID/fd/N 看是否含 anon_inode:
  • epoll_create1(0)epoll_create() 都生成 eventpoll 类型,无需区分

这些 fd 为什么容易积而不释放

根本原因不是“忘了 close”,而是它们常被封装在库或框架内部,生命周期脱离开发者直觉控制:

  • eventpoll:libuv、Node.jsnginxredis事件循环都重度依赖;若 epoll 实例未被显式 close()(比如线程异常退出、对象析构失败),fd 就泄漏
  • signalfdgo runtime 在启动时创建一个全局 signalfd 用于信号转发,但不会随 goroutine 退出而销毁;glibc 的 pthread_atfork 注册也可能隐式创建
  • timerfdjava nioEPollArrayWrapperrustmiopythonasyncio 都用它实现超时调度;若定时器未 cancel 就丢弃引用(如 asyncio.Task 被 gc 但底层 timerfd 未关),fd 就滞留

它们都不占磁盘 inode,也不出现在 lsof -ilsof -U 中,纯属内核资源,所以 ulimit -n 到了就直接 EMFILE,毫无缓冲。

如何定位是哪个模块创建的 eventpoll / timerfd

单靠 fdinfo 只能知道类型,不能回溯调用。需要运行时干预:

  • strace -e trace=epoll_create,epoll_create1,signalfd,timerfd_create -p PID 抓创建点(注意开销大,慎用于生产)
  • 若进程支持 /proc/PID/stack(需内核开启 CONFIG_PROC_KCORE),可在 fdinfo 发现大量 eventpoll 后,立刻 cat /proc/PID/stack 看当前所有线程的内核态调用链,找频繁出现 do_epoll_waitsys_timerfd_create 的线程
  • 对 Go 程序:kill -SIGQUIT PID 输出 goroutine stack,搜索 epollwaittimerfd 相关调用;Java 可用 jstack + Unsafe.park 上下文辅助判断

最隐蔽的情况是:c++ RAII 对象析构函数里 close 失败(比如被 signal 中断),或多线程环境下 close 被重复调用导致 EBADF 后静默忽略——这类 bug 不会报错,但 fd 就永远卡在那儿。

text=ZqhQzanResources