Go 的 netpoll 是 runtime 内部封装的跨平台 I/O 多路复用机制,不暴露 epoll/kqueue 给用户,与 G-P-M 调度深度耦合,仅管理非阻塞网络 fd,并通过专用 OS 线程轮询就绪事件唤醒 goroutine。

Go 的 netpoll 并不直接暴露 epoll/kqueue 给用户,它在 runtime 内部封装了一套跨平台的 I/O 多路复用机制——你没法在标准库代码里找到 epoll_wait 或 kqueue 的裸调用,因为它们被藏在 runtime/netpoll.go 和对应平台的 runtime/netpoll_*.c(或 .s)里,且与 G-P-M 调度深度耦合。
netpoll 不是独立模块,而是 runtime 的一部分
很多人误以为 netpoll 是 net 包里的某个可导出类型或函数,其实它根本不可导出,也没有公开 API。它的核心结构体 netpoll(小写开头)只存在于 runtime 包中,初始化发生在 runtime.main 启动时,通过 netpollinit 调用平台特定实现:
- Linux:调用
netpollinit→epoll_create1 -
macOS/BSD:调用
netpollinit→kqueue - Windows:用 IOCP 模拟,不走 kqueue/epoll 路径
关键点在于:这个 poller 不管理所有 fd,只管理那些被标记为“non-blocking”且注册了网络 I/O 的 fd(比如 conn.Read 阻塞时,底层会把该 fd 交给 netpoll 等待就绪)。
goroutine 是怎么被挂起又唤醒的
当你调用 conn.Read,最终会走到 internal/poll.(*FD).Read,它判断当前 fd 是否 ready:
- 如果 fd 已就绪(比如缓冲区有数据),直接读,不阻塞
- 如果未就绪,调用
runtime.netpollblock,把当前 goroutine 的g挂到该 fd 对应的等待队列上,并调用gopark让出 M - 与此同时,runtime 有一个单独的、始终运行的轮询线程(
netpollworker),它在后台持续调用epoll_wait或kevent - 一旦某个 fd 就绪,worker 会遍历其等待队列,对每个
g调用netpollready→goready,将其重新入 runqueue
注意:netpoll worker 不是 goroutine,它是 runtime 自己用 C 或汇编启动的 OS 线程(sysmon 之外的另一条常驻线程),避免依赖 Go 调度器本身。
为什么不能直接用 epoll_ctl?
你无法在用户代码里安全调用 epoll_ctl 管理同一个 fd,因为:
- Go 运行时已经对该 fd 设置了
EPOLLONESHOT(Linux)或等效语义,一次就绪后必须显式重注册,而标准库的net包内部会自动处理;手动干预会导致就绪事件丢失 - fd 的生命周期由
runtime.fdmgr管理,close 时会自动从 epoll/kqueue 中删除;外部调用epoll_ctl(..., EPOLL_CTL_DEL)可能触发 double-unregister panic - Go 的 fd 复用策略(比如
SO_REUSEPORT场景下多个 listener 共享一个端口)依赖 runtime 的统一调度,混用系统调用会破坏一致性
如果你真需要底层控制(比如写高性能代理或自定义协议栈),正确做法是用 syscall.RawConn 控制权移交,再配合 Control 方法在 fd 创建后、启用前插入自定义 epoll_ctl 调用——但必须确保不干扰 runtime 的后续管理逻辑。
调试 netpoll 行为的关键位置
想确认某次 Read/Write 是否真的进入了 netpoll 等待,可以:
- 设置环境变量
GODEBUG=netdns=cgo+1(辅助判断 DNS 是否干扰) - 用
strace -e trace=epoll_wait,epoll_ctl,kevent观察系统调用频次(注意:仅限 Linux/macOS,且需关闭 cgo DNS) - 在
runtime/netpoll.go的netpoll函数加日志(需重新编译 Go 源码) - 查看
pprof/goroutine?debug=2输出中是否有大量IO wait状态的 goroutine
最易忽略的一点:netpoll 的效率高度依赖 fd 是否真正 non-blocking。如果底层 syscall 返回 EAGAIN / EWOULDBLOCK,runtime 才会走挂起流程;否则会当作同步操作处理——所以务必确保监听 socket 和 conn 都已调用 SetNonblock(true)(标准库已做,但自定义 fd 容易漏)。










