直接回答:netpoller 用操作系统的事件通知机制(Linux epoll、BSD kqueue、Windows IOCP)监听网络 fd,把“等数据”这件事从线程层面卸载:goroutine 发起网络读写时若数据未就绪,runtime 把 fd 注册进 epoll 并让 goroutine gopark,不占用任何 OS 线程;fd 就绪后 epoll_wait 返回,runtime 找到对应 goroutine 丢回运行队列。于是 Go 程序能用同步写法承载百万连接,而不需要每连接一个线程。
展开解析:与 GMP 的衔接是精髓:网络 goroutine 阻塞时不占 M(系统线程),这与直接调阻塞 syscall 有本质区别——后者会让 runtime 把 P 从该 M 上摘走(retake/hand off),线程成本高。netpoller 的循环挂在 sysmon 与调度器找可运行 goroutine 的路径上(startTheWorld 前的 netpoll 检查),有就绪事件就注入可运行队列,无独立轮询线程空转。细节:监听 socket、连接 socket 在创建时就设为非阻塞并注册;SetDeadline 通过给 pollDesc 挂定时器实现超时唤醒;边缘触发还是水平触发——Go 用边缘触发(EPOLLET)配合非阻塞读循环。为什么文件 IO 不走这条路:常规文件没有“未就绪”概念,epoll 对文件 fd 永远就绪,所以文件 IO 仍是真阻塞,靠 M/P 分离兜底。面试能把“goroutine 阻塞的三种形态:channel/锁走 gopark、网络走 netpoller、syscall 走 hand off”讲清就是加分项。
追问方向:cgo 调用阻塞时 runtime 怎么处理?GOMAXPROCS 与 netpoller 有何关系?
(约 500 字)