select/poll/epoll
为什么要传到内核态判断就绪状态
-
判断 fd 是否就绪的唯一依据是内核中的 socket 缓冲区状态,用户态根本访问不到这块内存。所以不管 select/poll 还是 epoll,判断就绪这件事必须在内核里做。区别只是:
-
select/poll:每次都把 fd 列表拷贝到内核,内核遍历判断
select的流程是:无数据时阻塞 → 数据到达唤醒 → 重新遍历全部 fd 找出就绪者 → 返回。这种“唤醒即全扫”的模式,正是它在大并发下性能崩塌的根源。
-
epoll:fd 列表常驻内核,数据到达时回调标记就绪
-
select/poll数据为什么存在用户态,epoll数据为什么存在内核态
-
select/poll数据存在用户态
-
select/poll 是无状态的系统调用,内核不帮你记住任何东西
-
select/poll 设计为一次性查询——调用时传入,返回后就"忘记"了
-
内核不保存 fd 集合,下次调用你得重新传
-
所以 fd 数组天然存在用户态,每次调用时拷贝到内核,用完内核就丢弃
-
-
epoll数据存在内核态
- epoll 是有状态的,内核帮你维护一个长期存在的 fd 集合
- epoll 的设计目标就是长期监听大量 fd,避免反复拷贝
- 红黑树常驻内核,一次注册,持续有效
- 事件驱动:设备驱动在 fd 就绪时通过回调函数直接把节点挂入就绪链表,这个操作必须在红黑树所在的内核空间完成
- 补充:一个 EventLoop 线程就会有一个selector,就会有一个对应的epoll对象
select、poll、epoll的区别
┌─────────────────────────────────────────────────────────────────────────────┐
│ select / poll │
│ │
│ 用户态 每次拷贝整个集合 内核态 │
│ ┌────────────┐ ────────────────────────► ┌────────────┐ │
│ │ fd 集合 │ │ 内核接收后 │ │
│ │ [1,2,3,..] │ ◄──────────────────────── │ 线性扫描 │ │
│ └────────────┘ 返回整个集合 └────────────┘ │
│ │ │
│ ▼ │
│ 遍历整个集合,找出就绪的 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ epoll │
│ │
│ 用户态 内核态 │
│ ┌─────────────────────┐ │
│ epoll_create ────────────────────────► │ eventpoll 对象 │ │
│ │ ┌───────┐ ┌──────┐│ │
│ epoll_ctl(ADD) ─── 单个fd信息 ────────► │ │红黑树 │ │就绪链 ││ │
│ (只传这一个fd) │ │管理全部│ │表(就 ││ │
│ │ │ fd │ │绪的) ││ │
│ epoll_wait ◄── 只返回就绪的fd ────── │ └───────┘ └──────┘│ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ 直接处理就绪的 fd,无需遍历全部 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
-
区别
-
select、poll是:应用程序的一个数组或者列表管理所有的fd,数据在用户区,想要知道哪些就绪,就需要应用程序把所有的fd发给内核,然后内核遍历标记就绪的,然后给到用户态的应用程序再次遍历找出就绪的处理。
-
epoll:一开始epoll_create创建红黑树+就绪链表构,数据本身就在内核区,epoll_ctl增删改红黑树节点(一个节点一个fd),epoll_wait内核直接返回就绪链表,然后给到用户态的应用程序处理。
- epoll用【epoll_ctl方法+ epoll_wait方法】实现select【select方法】\ poll【poll方法】的功能
-
select/poll的工作模式- 管理方:应用程序在用户态维护一个数组或列表,存放所有需要监控的 fd。
- 每次调用:应用程序必须把整个 fd 集合从用户态拷贝到内核态。
- 内核动作:内核线性扫描(轮询) 全部 fd,找出就绪的,标记后返回。
- 返回结果:内核把处理后的集合拷贝回用户态。
- 用户处理:应用程序遍历整个集合,找出被标记的就绪 fd 进行处理。
痛点:每次调用都要全量拷贝 + 全量扫描(O(n)),连接数越多性能越差。
epoll的工作模式- 初始化(
epoll_create):内核创建一个eventpoll对象,内部包含红黑树(管理所有 fd)和就绪链表(存放就绪的 fd)。这个对象本身就在内核态。 - 增删改(
epoll_ctl):在内核的红黑树上增、删、改节点(一个节点 = 一个 fd),只需要把单个 fd 的信息从用户态拷贝到内核态(增量拷贝)。 - 等待事件(
epoll_wait):内核直接检查就绪链表,如果不为空,就把就绪的 fd 拷贝到用户态返回。 - 用户处理:应用程序直接遍历返回的就绪 fd 列表进行处理,无需再扫描全部。
优势:一次性注册(树),只返回就绪的(链表),全量扫描变增量管理,O(n) 变 O(1)。
-
-
epoll特点:
- 红黑树 + 就绪链表
数据结构 职责 时间复杂度 关键优势 问题背景 红黑树 管理所有注册的 fd 增删改查 O(log n) 支持海量 fd 的快速管理 应用程序需要随时向 epoll 实例中添加(ADD)、修改(MOD)或删除(DEL)要监控的 fd。
这个操作是动态且频繁的。
如果使用简单的数组或链表,在海量连接(比如 10 万个)中查找一个 fd 的时间复杂度是 O(n),这在高并发下是不可接受的。就绪链表(双向链表) 存储就绪的 fd 插入/删除 O(1) 避免了全量扫描,只处理活跃连接 select和poll最大的性能瓶颈在于:内核不知道哪些 fd 就绪了,所以必须线性扫描全部 fd(O(n))。
即使 10 万个连接中只有 1 个活跃,也要检查 10 万次。同时,每次都要把全部 fd 集合在用户态和内核态之间复制,开销巨大。这种设计使得 epoll 在连接数巨大但活跃连接很少的场景下(比如大多数 Web 服务器),性能远优于
select和poll。- 回调驱动 (O(1)):就绪时通过回调加入就绪链表,
epoll_wait直接返回
-
特性维度 select poll epoll 底层数据结构 固定大小的位图 (Bitmap) 动态数组 ( pollfd结构体)红黑树 + 就绪链表 最大连接数 1024(由 FD_SETSIZE硬编码限制)无上限(受系统内存限制) 无上限(受系统内存限制) 每次调用开销 全部复制:每次调用都需将整个 fd 集合从用户态拷贝到内核态 全部复制:每次调用都需将整个 pollfd数组拷贝到内核态增量管理:通过 epoll_ctl注册时拷贝一次,epoll_wait只拷贝就绪的 fd就绪检查方式 轮询 (O(n)):内核线性扫描所有 fd 轮询 (O(n)):内核线性扫描所有 fd 回调驱动 (O(1)):就绪时通过回调加入就绪链表, epoll_wait直接返回触发模式 仅支持水平触发 (LT) 仅支持水平触发 (LT) 支持水平触发 (LT) 和边缘触发 (ET) 性能特点 随 fd 数量增加性能线性下降 随 fd 数量增加性能线性下降 连接数越大,性能优势越明显(活跃连接数少时近乎 O(1)) -
水平触发 (LT)和边缘触发 (ET)
-
水平触发 (LT):只要有数据一直通知你读数据,直到读完停止通知
- 优先:编程复杂度低
- 缺点:系统调用(CPU 上下文切换多)多,性能不好,
-
边缘触发 (ET):只有从无数据变成有数据那一下通知,如果没读完可能就永远读不到了
- 优先:减少系统调用(减少了 CPU 上下文切换),性能好,好个~5%
- 缺点:编程复杂度高,容易出bug
-
触发模式 触发条件 触发次数 LT(水平触发) 电平处于高电平状态 持续触发,直到状态变为低 ET(边缘触发) 电平发生0→1 的跳变 只触发一次(上升沿)
-
-
Netty 始终使用水平触发模式 (Netty always uses level-triggered mode)
-
即使底层是 LT,Netty 也帮你一次性读完了所有数据,达到了类似 ET 的效果,但没有 ET 的编程风险。
-
Netty 当前版本已经固定为 LT 模式,无法再使用 ET。这是 Netty 团队做出的工程选择——用不到 5% 的性能损失,换来 100% 的代码安全性和开发便利性。放心用 LT,Netty 已经在应用层帮你把 LT 优化到了接近 ET 的性能水平。
-
-
因为select/poll是复制所有fd去轮询检查的,大量fd却只有少量是活跃的说明系统会调用大量资源只为了给那几个fd服务,开销大,性能肯定低。epoll就反过来咯
![image-20260717161953808]()

浙公网安备 33010602011771号