尧图精选

epoll 到底快在哪?三张图看懂 select、poll、epoll 的区别

🕒 发布时间:2026/10/2 17:18:29 📁 来源:尧图网络
图解epoll 到底快在哪三张图看懂 select、poll、epoll 的区别理解 epoll可以先看 select、poll 在等待事件时反复做了哪些工作再看 epoll 怎样减少这些工作。核心是保存监听关系并在事件发生时提前记录就绪对象。一 select 和 poll 的重复开销假设程序监听 A、B、C、D、E 五个连接开始都没有事件随后只有 B、D 可读。select、poll 都能让调用线程睡眠数据到达后再唤醒它它们的开销主要在下面三处。每轮都要传入完整的监听范围。即使还是关注 A 到 E下一次调用仍要把完整集合或数组交给内核产生重复的参数拷贝。select 使用位图返回时会修改集合所以再次调用前还要恢复监听集合poll 使用数组可以复用用户态数组但每次仍要提交它。内核需要扫描才能确定谁已就绪。调用时先检查监听项如果没有事件就睡眠被唤醒后再检查。即使只有 B、D 可读也要检查 A 到 E而不能直接从一个保存好的就绪链表中取出 B、D。连接越多检查非活跃连接的工作越多。应用还要从返回的标记中找出就绪连接。select 的结果位图 只保留 B、D 的就绪位其他位清零poll 的数组仍包含 A 到 E在 B、D 对应项的revents中设置就绪标记。应用需要检查位图或数组中的标记找到本轮要处理的连接。二 epoll 如何减少重复开销epoll 将“管理监听关系”和“等待事件”分成了epoll_ctl()与epoll_wait()并在内核中保存监听关系和就绪链表。三个问题分别对应三个改进select 和 poll 的重复工作epoll 的解决方式每轮传入完整监听范围用epoll_ctl()注册内核红黑树持续保存监听关系需要增删改时才更新epoll_wait()不再传入整份监听范围。内核扫描全部监听项寻找就绪连接注册时建立回调关联B、D 就绪时回调直接找到对应监听记录把它们加入就绪链表这样就不用遍历全部监听项了。epoll_wait()主要复查这些候选。应用检查集合或数组中的标记需要二次处理才能拿到就绪的事件epoll_wait()把就绪链表的事件写入用户态数组这个数组的事件都是就绪事件应用逐项处理这些事件即可。下面的图把同一次事件放到三种机制中比较上方是用户态下方是内核态按编号看执行顺序。接下来只看 epoll把它拆成注册和数据到达两个阶段。三 注册监听假设程序已经创建了 epoll 实例socket 101 当前没有数据可读。程序调用epoll_ctl(ADD)把 socket 101 加入监听范围。内核按图中的顺序完成以下工作。找到 epoll 和 socket。内核通过文件描述符找到这次使用的 epoll 实例以及需要监听的 socket。把监听记录放入红黑树。内核创建一条监听记录叫作epitem记下目标 socket 和关注的事件再把它加入红黑树。红黑树保存着这个 epoll 实例的全部监听记录。在 socket 上登记回调。回调就是提前登记好、在相应事件发生时被调用的函数。这里使用的是内核函数ep_poll_callback内核在 socket 的等待队列中登记一个调用它的回调项并将这个回调项与监听记录关联起来。这个回调负责把“socket 可读了”变成“epoll 有事件可以处理了”。socket 变为可读时内核调用它它就直接找到对应的监听记录加入就绪链表如果有线程正在等待再唤醒线程。所以回调的作用是记录就绪、通知等待线程。真正读取数据的工作由应用拿到事件后调用recv()完成。检查现在是否已经可读。本例还没有数据所以就绪链表为空。如果注册时就已经可读则会直接把对应监听项加入就绪链表。注册完成后程序调用epoll_wait()。如果没有就绪事件并且允许阻塞线程就进入epoll 的等待队列等待通知。这里等待的是调用epoll_wait()的应用线程epoll 不会为每个 socket 新建线程。epoll 等待队列的作用是记住哪些线程正在等事件。没有事件时这些线程可以睡眠、让出 CPU回调记录就绪后再按需唤醒它们。就绪链表记录“哪些连接需要处理”epoll 等待队列记录“哪些线程等待通知”。到这里监听关系已经建立好了后续每次等待都可以复用。四 数据到达现在socket 101 收到了数据而程序正阻塞在epoll_wait()中。接下来看数据到达后的流程。数据进入 socket。网卡收到数据协议栈处理后把数据交给目标 socket保存在接收缓冲区中。socket 发出可读通知。当 socket 满足可读条件时触发它的等待队列中登记的回调。回调找到对应的监听记录。利用注册时保存的关联回调直接找到 socket 101 对应的epitem不用遍历全部连接也不用重新搜索红黑树。把监听项加入就绪链表。回调把它记录为待处理的就绪项如果已经在链表中就不重复加入。它在红黑树中的监听关系仍然保留。唤醒等待线程。回调唤醒阻塞在 epoll 等待队列中的线程使程序有机会继续执行。返回就绪事件。epoll_wait()检查就绪链表中的候选确认当前状态再把符合条件的事件返回给程序。程序拿到“socket 101 可读”的事件后再调用recv()读取数据。epoll 返回的是就绪事件实际数据由程序随后通过recv()从 socket 接收缓冲区读取。可以用下面两条流程记住它注册找到连接 → 保存监听记录 → 登记回调 → 检查当前状态 通知数据到达 → 回调记录就绪 → 唤醒等待线程 → 返回事件 → recv 读取数据红黑树记住“关注谁”就绪链表记住“谁需要处理”等待队列记住“哪些线程正在等事件”。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →