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 统计。

为什么 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 在这里都有独立文件,内容含 flags、mnt_id 和关键的 type 字段。例如:
cat /proc/12345/fdinfo/7 pos: 0 flags: 02000002 mnt_id: 12 type: eventpoll
所以排查必须进 /proc/PID/fdinfo/ 扫描 type: 行,不能只信 lsof 或 ls -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.js、Nginx、Redis 的事件循环都重度依赖;若 epoll 实例未被显式close()(比如线程异常退出、对象析构失败),fd 就泄漏 -
signalfd:Go runtime 在启动时创建一个全局signalfd用于信号转发,但不会随 goroutine 退出而销毁;glibc 的pthread_atfork注册也可能隐式创建 -
timerfd:Java NIO 的EPollArrayWrapper、Rust 的mio、Python 的asyncio都用它实现超时调度;若定时器未 cancel 就丢弃引用(如 asyncio.Task 被 gc 但底层 timerfd 未关),fd 就滞留
它们都不占磁盘 inode,也不出现在 lsof -i 或 lsof -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_wait或sys_timerfd_create的线程 - 对 Go 程序:
kill -SIGQUIT PID输出 goroutine stack,搜索epollwait或timerfd相关调用;Java 可用jstack+Unsafe.park上下文辅助判断
最隐蔽的情况是:C++ RAII 对象析构函数里 close 失败(比如被 signal 中断),或多线程环境下 close 被重复调用导致 EBADF 后静默忽略——这类 bug 不会报错,但 fd 就永远卡在那儿。










