、select /poll 的痛点
表格
| 模型 | 缺陷 |
|---|---|
| select | 1. FD 最大限制 1024;2. 每次调用都把全部 FD 拷贝进内核;3. 返回后需要遍历全部 FD 找就绪的 |
| poll | 用链表去掉了 1024 上限,但依然要遍历全部 FD,FD 数量大时性能变差 |
二、epoll 核心结构(eventpoll)
struct eventpoll {
rb_root rbr; // 红黑树:保存所有需要监听的FD
list_head rdlist;// 就绪链表:存放已经就绪的FD
};
epoll 是 select/poll 的改进方案,提供 3 个核心 API:
- epoll_create(size):内核创建 eventpoll 结构体,返回 epfd 句柄
- epoll_ctl(epfd, op, fd, event):ADD/MOD/DEL,把 FD 加到红黑树,绑定回调;FD 就绪时内核自动把它放进就绪链表 rdlist
- epoll_wait(epfd, events, maxevents, timeout):检查就绪链表,把就绪 FD 拷贝到用户空间 events 数组,只返回就绪 FD,不用全量遍历
✅ epoll 解决 select/poll 问题的三点:
- 红黑树存监听 FD,无 1024 上限,增删改查 O (logn),性能不随 FD 数量上涨明显衰减
- FD 仅需要 epoll_ctl 添加一次,后续 epoll_wait 不需要重复拷贝全部 FD 到内核
- 内核维护就绪链表,epoll_wait 只返回就绪 FD,用户态不用遍历所有监听 fd
三、epoll 两种通知模式 LT & ET
- LT 水平触发(默认):只要缓冲区还有可读数据,每次 epoll_wait 都会重复通知,直到数据读完。编码简单,容易出现惊群。
- ET 边缘触发 :只有状态变化的一瞬间(新数据到达)通知一次 ,缓冲区剩余数据不会再次通知;必须搭配非阻塞 IO,一次性循环读完缓冲区所有数据,编码更复杂,能减少通知次数,避免惊群。
示例:收到 2KB 数据,只读走 1KB LT:再次 epoll_wait 还会通知,剩下 1KB 继续可读; ET:不会再通知,如果本次没读完剩下 1KB,数据会滞留缓冲区。
四、epoll 实现 Web 服务完整流程
- epoll_create 创建 epoll 实例
- 创建 serverSocket 监听 fd(ssfd)
- epoll_ctl,把 ssfd 加入 epoll 红黑树监听读事件
- 循环调用 epoll_wait 阻塞等待就绪 FD
- 拿到就绪 FD,判断事件:
- 如果是服务端 listenfd 可读:代表有新连接,调用 accept 拿到客户端 fd,epoll_ctl 加入监听
- 如果是客户端 fd 可读:读取请求数据
- 如果是写事件:向客户端写出响应