结论:三者都是 I/O 多路复用系统调用。select 用位图传 fd 集合,有 1024(FD_SETSIZE)上限,且每次调用都要把整个集合从用户态拷到内核、内核线性扫描;poll 用链表式数组去掉数量上限,但仍需全量拷贝和 O(n) 扫描;epoll 把"注册兴趣"和"等待事件"拆成 epoll_ctl/epoll_wait,内核用红黑树维护兴趣集合、就绪事件放入就绪链表,wait 只返回就绪的 fd,复杂度 O(就绪数) 而非 O(总数)。
展开:epoll 的性能优势来自三点:1)fd 集合只注册一次,不用每次调用重复拷贝——高频调用下这是主要差距;2)就绪通知基于回调(fd 状态变化时驱动把它挂入就绪队列),不用每次全量扫描;3)epoll_wait 与就绪 fd 通过 mmap 共享内存时代(早期实现)进一步减拷贝,现代实现是少量拷贝就绪数组。规模定律:连接少且活跃比例高时三者差距不大;万级连接、活跃比例低(C10K 场景)epoll 碾压。易错点:1)epoll 不是银弹,全连接都活跃时与 poll 相当甚至更慢(多两次系统调用);2)select 的 fd_set 会被调用修改,每次循环必须重新 FD_SET;3)macOS/BSD 没有 epoll,对应物是 kqueue,跨平台用 libevent/libuv 抽象。
追问方向:epoll 的 LT/ET 模式、kqueue 与 epoll 的 API 哲学差异(事件变更即注册)、io_uring 为什么更进一步(共享环形队列消除系统调用本身)。