epoll、select、poll
(一)epoll 交互过程
一个客户端和使用了 epoll 的服务端的交互过程如下图所示。
1.epoll 原理
(1)select 的问题
select 的问题在于描述符的限制,能监控的文件描述符最大为 FD_SETSIZE,对于连接数很多的场景就无法满足;
另外select 还有一个问题,每调用 select 都需要从用户空间把描述符集合拷贝到内核空间,当描述符集合变大之后,用户空间和内核空间的内存拷贝会导致效率低下;
另外每次调用 select 都需要在内核线性遍历文件描述符的集合,当描述符增多,效率低下。
由于 select 存在上面的问题,于是 poll 被提了出来,它能解决 select 对文件描述符数量有限制的问题,但是依然不能解决线性遍历以及用户空间和内核空间的低效数据拷贝问题。
(3)epoll 原理
上面其实讨论了 select/poll 几个缺点,针对这几个缺点,就需要解决以下几件事:
- 如何突破文件描述符数量的限制
- 如何避免用户态和内核态对文件描述符集合的拷贝
- socket 就绪后,如何避免线性遍历文件描述符集合
针对第一点:如何突破文件描述符数量的限制,其实 poll 已经解决了,poll 使用的是链表的方式管理 socket 描述符,但问题是不够高效,如果有百万级别的连接需要管理,如何快速的插入和删除就变得很重要,于是 epoll 采用了红黑树的方式进行管理,这样能保证在添加 socket 和删除 socket 时,有 O(log(n)) 的复杂度。
针对第二点:如何避免用户态和内核态对文件描述符集合的拷贝,其实对于 select 来说,由于这个集合是保存在用户态的,所以当调用 select 时需要屡次的把这个描述符集合拷贝到内核空间。所以如果要解决这个问题,可以直接把这个集合放在内核空间进行管理。没错,epoll 就是这样做的,epoll 在内核空间创建了一颗红黑树,应用程序直接把需要监控的 socket 对象添加到这棵树上,直接从用户态到内核态了,而且后续也不需要再次拷贝了。
针对第三点:socket就绪后,如何避免内核线性遍历文件描述符集合,这个问题就会比较复杂,要完整理解就得涉及到内核收包到应用层的整个过程。这里先简单讲一下,与 select 不同,epoll 使用了一个双向链表来保存就绪的 socket,这样当活跃连接数不多的情况下,应用程序只需要遍历这个就绪链表就行了,而 select 没有这样一个用来存储就绪 socket 的东西,导致每次需要线性遍历所有socket,以确定是哪个或者哪几个 socket 就绪了。这里需要注意的是,这个就绪链表保存活跃链接,数量是较少的,也需要从内核空间拷贝到用户空间。
从上面 3 点可以看到 epoll 的几个特点:
- 程序在内核空间开辟一块缓存,用来管理 epoll 红黑树,高效添加和删除
- 红黑树位于内核空间,用来直接管理 socket,减少和用户态的交互
- 使用双向链表缓存就绪的 socket,数量较少
- 只需要拷贝这个双向链表到用户空间,再遍历就行,注意这里也需要拷贝,没有共享内存
2.总结
- epoll 在内核开辟了一块缓存,用来创建 eventpoll 对象,并返回一个 file descriptor 代表 epoll instance
- 这个 epoll instance 中创建了一棵红黑树以及一个就绪的双向链表(当然还有其他的成员)
- 红黑树用来缓存所有的 socket,支持 O(log(n)) 的插入和查找,减少后续与用户空间的交互
- socket 就绪后,会回调一个回调函数(添加到 epoll instance 上时注册到 socket 的)
- 这个回调函数会把这个 socket 放到就绪链表,并唤醒 epoll_wait
- 应用程序拷贝就绪 socket 到用户空间,开始遍历处理就绪的 socket
- 如果有新的 socket,再添加到 epoll 红黑树上,重复这个过程
(二)epoll与select、poll的对比
1. 用户态将文件描述符传入内核的方式
- select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程可以打开的fd数量限制,默认是1024。
- poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。
- epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。
2. 内核态检测文件描述符读写状态的方式
- select:采用轮询方式,遍历所有fd,最后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。
- poll:同样采用轮询方式,查询每个fd的状态,如果就绪则在等待队列中加入一项并继续遍历。
- epoll:采用回调机制。在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。
3. 找到就绪的文件描述符并传递给用户态的方式
- select:将之前传入的fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。
- poll:将之前传入的fd数组拷贝传出用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。
- epoll:epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。这里返回的文件描述符是通过mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。
4. 重复监听的处理方式
- select:将新的监听文件描述符集合拷贝传入内核中,继续以上步骤。
- poll:将新的struct pollfd结构体数组拷贝传入内核中,继续以上步骤。
- epoll:无需重新构建红黑树,直接沿用已存在的即可。
epoll更高效的原因
- select和poll的动作基本一致,只是poll采用链表来进行文件描述符的存储,而select采用fd标注位来存放,所以select会受到最大连接数的限制,而poll不会。
- select、poll、epoll虽然都会返回就绪的文件描述符数量。但是select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可。
- select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而epoll创建的有关文件描述符的数据结构本身就存于内核态中,系统调用返回时利用mmap()文件映射内存加速与内核空间的消息传递:即epoll使用mmap减少复制开销。
- select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。
- epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符
虽然epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了