Go 的 netpoller 不是 epoll 封装,而是与调度器协同的内部机制:Read 时 gopark,事件就绪时标记 goroutine 为 ready;它通过 PollDesc 关联 goroutine 而非 fd,不可手动替换为 syscall.Epoll,否则导致 EBADF、死锁或事件丢失。

你不能也不该直接操作 epoll 或 kqueue —— Go 的 netpoller 是 runtime 内部调度机制的一部分,不是供用户调用的 I/O 多路复用 API。
netpoller 不是 epoll 封装层,而是调度器协作者
很多人看到 Linux 下 Go 网络性能高,就以为 netpoller 是对 epoll_ctl 和 epoll_wait 的简单封装。事实相反:它把系统调用嵌进 goroutine 生命周期里。当你调用 conn.Read(),底层不是在等 epoll_wait 返回,而是在触发 gopark;当内核通过 epoll 通知某个 fd 可读,runtime 不是“唤醒线程”,而是把对应 goroutine 标记为 ready,交由调度器 pick。
这意味着:
-
netpoller没有公开构造函数或初始化入口,runtime.netpollinit是内部符号,不可导出 - 每个
net.Conn背后绑定一个PollDesc,它存着等待读/写的 goroutine 列表,而非 fd 本身 - 你无法用
syscall.EpollCreate1替换它——哪怕成功注册了同一个 fd,也会因缺少PollDesc关联而无法唤醒 goroutine
为什么手动混用 syscall.Epoll 会出 EBADF 或死锁
常见错误是:在 HTTP handler 里开个 goroutine,用 syscall.EpollWait 监听同一个 listener fd。这时问题立刻出现:
- 标准库
net.Listener.Accept()已经把该 fd 注册进netpoller,你再用epoll_ctl(ADD)会导致重复管理 - runtime 不知道你的
epoll_wait在等什么,不会为你调用runtime.Entersyscall,P 可能被卡住,后续 goroutine 无法调度 - 更隐蔽的是 fd dup:
Accept()返回的 conn fd 实际是新 dup 出来的,你监听原 listener fd 对连接数据无感知,但netpoller仍在监听旧 fd,造成超时失效、事件丢失
如何确认 netpoller 正在起作用(而不是你误以为的“自己在用 epoll”)
别看 strace 或 lsof —— 那些只是表象。真正线索藏在运行时行为里:
- 用
go tool trace查看runtime.netpoll事件频率:如果大量 goroutine 卡在IO wait状态,且netpoll调用间隔稳定(如 ~1ms),说明它正正常轮询 - pprof goroutine 堆栈中出现
net.(*conn).Read→internal/poll.(*FD).Read→runtime.netpoll,这是标准路径 - 修改
/proc/sys/net/core/somaxconn后观察ListenOverflows计数变化,这反映的是netpolleraccept 回调的及时性,而非你写的代码
什么时候你才需要绕过 netpoller
极少数场景下,比如实现自定义协议栈、嵌入式裁剪、或对接非标准 fd(如 vsock、AF_XDP),你才可能用到 syscall.RawConn:
- 必须显式调用
rawConn.Control()获取底层 fd,并用runtime.SetFinalizer确保关闭时清理PollDesc - 若要手动轮询,得配合
runtime.Netpoll(非导出函数)和runtime.Entersyscall/Exitsyscall,否则 P 会被长期占用 -
net/http.Server、grpc.Server等高层组件完全不支持这种模式,强行替换会导致 accept 漏收、连接半开、deadline 失效
绝大多数服务写 http.ListenAndServe 或 net.Listen 就够了。netpoller 的复杂性藏在 runtime 里,它的价值恰恰在于你不需要理解 epoll 的 EPOLLIN 和 EPOLLET 区别——只要记得设 SetReadDeadline,别的交给调度器。











