☰
Linux网络(三十四):面试必问:epoll凭什么比select高效?一文搞懂LT与ET模式及非阻塞原理
2026/10/23 3:00:33 网站建设 项目流程

◆ 博主名称: 小此方-CSDN博客
大家好,欢迎来到小此方的博客。
⭐️网络系列个人专栏: 【主题曲】计算机网络
⭐️此方的GitHub: github_此方
⭐️我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)

文章目录

  • 概要&序論
  • 一、epoll的优点以及select的缺点对比
    • 1.1 接口使用更方便
    • 1.2 数据拷贝轻量
      • 1.2.1 事件回调机制
    • 1.3 没有数量限制
    • 1.4 纠正一个常见的网传误区
    • 1.5 补充与引言
  • 二、如何理解水平和边缘触发
    • 2.1 场景一:宿舍楼小王与快递员张三
    • 2.2 场景二:快递员李四的送货方式
    • 2.3 新数据到来时的触发情况
    • 2.4 两种模式的设置与总结
  • 三、ET模式下为什么必须搭配非阻塞IO
    • 3.1 两个读取场景引发的思考
    • 3.2 为什么必须无脑循环读到EAGAIN
    • 3.3 关于LT与ET设置的建议
  • 四、为什么需要ET模式以及谁更高效
    • 4.1 为什么要保证一次读完这种确定性

概要&序論

Hello大家好,我是此方。上篇我们讲完了Epoll的使用与底层原理,本文继续讲解Epoll的两种模式,同时收尾上篇文章对于Epoll优缺点的介绍。

一、epoll的优点以及select的缺点对比

1.1 接口使用更方便

虽然epoll将原本庞大的接口拆分成了三个核心函数(epoll_create, epoll_ctl, epoll_wait),但这反而让使用起来变得更加方便和高效。

相比于select和poll在每次调用时都需要重新设置关注的文件描述符集合,epoll不需要每次循环都重复设置。它将输入输出参数分离开了,我们先通过epoll_ctl去注册或修改关注的事件,然后直接通过epoll_wait去等待事件的发生。这种设计极大降低了调用的负担。

1.2 数据拷贝轻量

在数据拷贝方面,select和poll是每次循环都需要将文件描述符集合从用户态拷贝到内核态,当并发量上来以后,这种开销是很恐怖的。

而epoll只在合适的时机调用EPOLL_CTL_ADD,将文件描述符结构拷贝到内核中。因为这种操作并不频繁,所以整体的拷贝开销被降到了最低。

1.2.1 事件回调机制

这是epoll最高效的核心所在。epoll没有采用传统的遍历方式来寻找就绪的文件描述符,而是采用了回调函数的方式。

当某个文件描述符就绪时,内核会通过回调函数,自动将该文件描述符结构加入到就绪队列中。这样当epoll_wait返回时,直接访问就绪队列就可以知道哪些文件描述符已经就绪。这个操作的时间复杂度是O(1)的。即使你监听了成千上万个文件描述符,epoll的效率也不会受到太大影响。

1.3 没有数量限制

epoll突破了文件描述符数目的限制,它能支持的文件描述符数目无上限。这得益于它的内核实现机制,不再需要像select那样受限于FD_SETSIZE,也不用像poll那样在大量文件描述符时遍历导致性能退化。

1.4 纠正一个常见的网传误区

网上有些博客在提到epoll的缺点或者原理时,会说epoll内部使用了内存映射机制(mmap),内核直接将就绪队列通过mmap的方式映射到用户态,从而避免了拷贝内存的额外性能开销。

这种说法是不准确的,甚至是完全错误的。实际上,我们定义的struct epoll_event结构体是我们自己在用户空间中分配好的内存。内核在传递数据时,依然需要将内核里的数据结构拷贝到我们用户空间的这个内存中。所以它并没有完全绕过内存拷贝。

1.5 补充与引言

关于epoll的缺点,其实基本上没有明显的硬伤,如果有的话可能也就是代码相对稍微复杂一点。

另外,大家在学完这些之后,一定要对比总结select, poll, epoll之间的优缺点,这绝对是面试中的高频考点。

在后续的内容中,我们会提到epoll的事件就绪通知机制。如果事件就绪了,但是我们不处理(没有来得及处理),epoll模型会一直通知我们吗?这就涉及到了epoll的两种工作模式:水平触发(Level Triggered,默认模式)和边缘触发(Edge Triggered,主动设置的模式),同时还会延伸到EPOLLIN、EPOLLOUT、EPOLLET等核心事件宏的用法。

二、如何理解水平和边缘触发

理解了epoll的基本优点后,我们来看看epoll内核的两种工作模式:水平触发(Level Triggered,简称LT)和边缘触发(Edge Triggered,简称ET)。我们可以用快递员送快递的场景来理解。

2.1 场景一:宿舍楼小王与快递员张三

小王买了5个快递,快递员张三拿着五个快递中的三个去派发。到小王的宿舍楼下,打电话通知小王。小王没有下来,张三又通知了小王,又通知了小王,小王终于下来了,取走了一个快递,剩下两个。张三看到快递还有两个,于是又通知小王,小王又下来取走一个,张三看还有一个快递于是继续通知小王,小王终于把快递全部取走了。

这就是LT模式。在这里,小王就是一个用户进程。只要底层有报文,就要一直通知上层!上层可以不读完,因为我知道你下次还会通知我!效率高不高,取决于有效通知有多少。

2.2 场景二:快递员李四的送货方式

李四去送快递,一共两个,它到小王的楼下,只打一次电话。说:我只给你打一次电话,如果你不下来取走取完快递,那么我就直接走了。小王下来取走了一个快递,还剩三个,李四于是真的走了。

这就是ET模式。即便是上层把数据拿走了一部分,后来再也没有新增了,ET再也不通知上层了!因为从网络中拿到数据,导致底层数据变化的时候,才会通知上层倒逼上层,必须收到通知,把本轮数据取完。

2.3 新数据到来时的触发情况

李四在回去的时候遇到了张三,张三凑巧也在送小王的快递,于是张三把包裹给了李四。李四看到新的包裹,于是转头回去。李四又给小王打了一个电话,说:“我原本不想回来的,但是你又有两个新快递在我这里,你下来取一下,不取走我继续回去了”。于是小王下来把包裹全部取走了。这说明当底层数据再次从无到有、从有到多发生变化时,ET模式会再次触发通知。

2.4 两种模式的设置与总结

在epoll的内核工作模式中,LT模式是没有选项的,默认就是LT。而ET模式需要我们在代码中主动设置,通常是通过设置EPOLLET这个事件选项来开启。同时,epoll的events还可以是其他几个宏,比如EPOLLIN表示对应的文件描述符可以读,EPOLLOUT表示可以写,EPOLLPRI表示有紧急数据可读,EPOLLERR表示发生错误,EPOLLHUP表示被挂断,EPOLLONESHOT表示只监听一次事件。

总结一下这两种模式的取舍。

  1. LT模式是默认的水平触发,只要事件就绪就会一直通知,逻辑简单且不容易遗漏,但可能因为频繁通知导致效率偏低。
  2. ET模式是边缘触发,只在数据发生变化时通知一次,通知效率高,但要求上层应用在收到通知后必须把数据读完,否则后续不再通知,通常需要配合非阻塞IO使用。

三、ET模式下为什么必须搭配非阻塞IO

我们可以借用模拟示波器的叫法来直观理解这两种模式:水平触发(Level Triggered)一直处于高电平状态,而边缘触发(Edge Triggered)则是从低电平跳变到高电平。

在上一节我们提到,ET通知就绪后,用户必须把缓冲区本轮的数据全部读完。通常会采用循环读取的方式,比如recv(fd, 100);每次读取100字节的数据,假设缓冲区里有3000字节,一次读不完怎么办?这里有两个非常经典的场景。

3.1 两个读取场景引发的思考

我给你两个场景,一个是我需要读取的数据是3000个,另一个是我需要读取的数据是2780个。

对于前者,读取30次能刚好读取完毕,但是需要读取第31次。因为我读取1-30次的时候,并不知道是否已经读取完毕了。既然读取第31次,那么第31次读取不到数据就会阻塞挂起。于是就需要非阻塞读取,非阻塞读取不到数据返回值为-1,判断后break退出循环,这是可以的。

对于后者,读取29次读取完毕,第30次读取到80个数据,少于预期的100个数据。似乎我们可以增加条件if (读取结果 < 预期结果) break;,但是我们为什么说要“不考虑返回值 < 期望值的情况,无脑循环直到EAGAIN”呢?

3.2 为什么必须无脑循环读到EAGAIN

因为recv()返回小于100只表示“这一次实际读到不足100字节”,并不能证明数据已经读完。有可能此时数据还没读完,后续仍然可能继续到达。只有非阻塞的recv()返回 -1 且errno == EAGAIN,才能确定当前内核接收缓冲区已经没有数据。所以ET下必须继续读到EAGAIN。

这里有一个核心的隐患:在recv的时候,万一读完了,因为用户并不清楚底层缓冲区数据已经读完,recv还会被调用!这个时候recv会被怎么样?被阻塞!

所以这是一道常考的面试题:ET模式,必须将fd设置为非阻塞的工作模式!也就是ET + 非阻塞。

3.3 关于LT与ET设置的建议

总结一下这两个问题。

  1. LT模式需要设置为非阻塞吗?可以,如果不想,也可以不设。
  2. LT不用担心读不到数据阻塞的问题,因为LT每次只读取一次不循环。但ET模式下,必须无脑循环读取直到EAGAIN,不考虑返回值 < 期望值的情况,以此防止最后一次读不到数据把进程阻塞死。

四、为什么需要ET模式以及谁更高效

有人可能会问,上面我们说的非阻塞循环读取,我LT也可以这样写啊!关于ET的工作特性,我LT在上层完全可以实现同样的代码逻辑。

确实如此,LT的上层当然可以这样写,但是ET的存在是为了约束。因为不是所有的程序员在上层写代码时都会去写非阻塞循环读取。谁来约束程序员必须这样写?是操作系统OS来约束程序员!目的就是为了增加IO读写方式的确定性。

4.1 为什么要保证一次读完这种确定性

既然要求一次读完,那么这就引出了一道经典的面试题:LT与ET,到底谁更高效?

  1. ET通知效率更高,有效通知的数量最多。
  2. ET尽快读完所有的数据,可以给对方更新一个更大的win窗口,提供对方滑动窗口大小,从而提升网络发送报文的并发度。

深入来看,ET模式倒逼程序员的本质,是为了给对方在概率上提供一个更大的应答WIN,提升TCP通信的效率。在TCP的接收缓冲区中有一个低水位线,如果我上层尽快读取掉数据了,我给你返回的16位窗口大小也就更大了,于是你对面的滑动窗口大小就有可能更大了。

此外,我们的TCP报文上带有一个PSH标志位,它的作用是让对端接收到后立刻读取该报文,但实际上就是发送过去后,把这个套接字直接激活,让读事件就绪。结合ET模式,这进一步确保了数据能被迅速处理。


——————计算机网络系列专栏:完——————

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询