linux下select, poll和epoll的区别

本文详细解析了内核2.6版本中epoll与poll的工作原理,并通过测试数据对比了两者在处理大量文件描述符时的性能差异。epoll通过减少重复操作和优化查找流程,在监控文件描述符数量增加时表现出更稳定的性能,尤其适用于Web服务器场景。测试结果显示,随着监控的文件描述符数量增加,poll的性能线性下降,而epoll保持相对稳定。
随着2.6内核对epoll的完全支持,网络上很多的文章和示例代码都提供了这样一个信息:使用epoll代替传统的poll能给网络服务应用带来性能上的提升。但大多文章里关于性能提升的原因解释的较少,这里我将试分析一下内核(2.6.21.1)代码中poll与epoll的工作原理,然后再通过一些测试数据来对比具体效果。

       POLL:

       先说poll,poll或select为大部分Unix/Linux程序员所熟悉,这俩个东西原理类似,性能上也不存在明显差异,但select对所监控的文件描述符数量有限制,所以这里选用poll做说明。
   
       poll是一个系统调用,其内核入口函数为sys_poll,sys_poll几乎不做任何处理直接调用do_sys_poll,do_sys_poll的执行过程可以分为三个部分:

       1,将用户传入的pollfd数组拷贝到内核空间,因为拷贝操作和数组长度相关,时间上这是一个O(n)操作,这一步的代码在do_sys_poll中包括从函数开始到调用do_poll前的部分。

       2,查询每个文件描述符对应设备的状态,如果该设备尚未就绪,则在该设备的等待队列中加入一项并继续查询下一设备的状态。查询完所有设备后如果没有一个设备就绪,这时则需要挂起当前进程等待,直到设备就绪或者超时,挂起操作是通过调用schedule_timeout执行的。设备就绪后进程被通知继续运行,这时再次遍历所有设备,以查找就绪设备。这一步因为两次遍历所有设备,时间复杂度也是O(n),这里面不包括等待时间。相关代码在do_poll函数中。

       3,将获得的数据传送到用户空间并执行释放内存和剥离等待队列等善后工作,向用户空间拷贝数据与剥离等待队列等操作的的时间复杂度同样是O(n),具体代码包括do_sys_poll函数中调用do_poll后到结束的部分。

       EPOLL:

       接下来分析epoll,与poll/select不同,epoll不再是一个单独的系统调用,而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成,后面将会看到这样做的好处。

       先来看sys_epoll_create(epoll_create对应的内核函数),这个函数主要是做一些准备工作,比如创建数据结构,初始化数据并最终返回一个文件描述符(表示新创建的虚拟epoll文件),这个操作可以认为是一个固定时间的操作。

        epoll是做为一个虚拟文件系统来实现的,这样做至少有以下两个好处:

        1,可以在内核里维护一些信息,这些信息在多次epoll_wait间是保持的,比如所有受监控的文件描述符。

        2, epoll本身也可以被poll/epoll;

       具体epoll的虚拟文件系统的实现和性能分析无关,不再赘述。

       在sys_epoll_create中还能看到一个细节,就是epoll_create的参数size在现阶段是没有意义的,只要大于零就行。

       接着是sys_epoll_ctl(epoll_ctl对应的内核函数),需要明确的是每次调用sys_epoll_ctl只处理一个文件描述符,这里主要描述当op为EPOLL_CTL_ADD时的执行过程,sys_epoll_ctl做一些安全性检查后进入ep_insert,ep_insert里将 ep_poll_callback做为回掉函数加入设备的等待队列(假定这时设备尚未就绪),由于每次poll_ctl只操作一个文件描述符,因此也可以认为这是一个O(1)操作

        ep_poll_callback函数很关键,它在所等待的设备就绪后被系统回掉,执行两个操作:

       1,将就绪设备加入就绪队列,这一步避免了像poll那样在设备就绪后再次轮询所有设备找就绪者,降低了时间复杂度,由O(n)到O(1);
   
       2,唤醒虚拟的epoll文件;

       最后是sys_epoll_wait,这里实际执行操作的是ep_poll函数。该函数等待将进程自身插入虚拟epoll文件的等待队列,直到被唤醒(见上面ep_poll_callback函数描述),最后执行ep_events_transfer将结果拷贝到用户空间。由于只拷贝就绪设备信息,所以这里的拷贝是一个O(1)操作。

       还有一个让人关心的问题就是epoll对EPOLLET的处理,即边沿触发的处理,粗略看代码就是把一部分水平触发模式下内核做的工作交给用户来处理,直觉上不会对性能有太大影响,感兴趣的朋友欢迎讨论。

       POLL/EPOLL对比:

       表面上poll的过程可以看作是由一次epoll_create/若干次epoll_ctl/一次epoll_wait/一次close等系统调用构成,实际上epoll将poll分成若干部分实现的原因正是因为服务器软件中使用poll的特点(比如Web服务器):

       1,需要同时poll大量文件描述符;

       2,每次poll完成后就绪的文件描述符只占所有被poll的描述符的很少一部分。

       3,前后多次poll调用对文件描述符数组(ufds)的修改只是很小;

       传统的poll函数相当于每次调用都重起炉灶,从用户空间完整读入ufds,完成后再次完全拷贝到用户空间,另外每次poll都需要对所有设备做至少做一次加入和删除等待队列操作,这些都是低效的原因。

        epoll将以上情况都细化考虑,不需要每次都完整读入输出ufds,只需使用epoll_ctl调整其中一小部分,不需要每次epoll_wait都执行一次加入删除等待队列操作,另外改进后的机制使的不必在某个设备就绪后搜索整个设备数组进行查找,这些都能提高效率。另外最明显的一点,从用户的使用来说,使用epoll不必每次都轮询所有返回结果已找出其中的就绪部分,O(n)变O(1),对性能也提高不少。

       此外这里还发现一点,是不是将epoll_ctl改成一次可以处理多个fd(像semctl那样)会提高些许性能呢?特别是在假设系统调用比较耗时的基础上。不过关于系统调用的耗时问题还会在以后分析。

       POLL/EPOLL测试数据对比

       测试的环境:我写了三段代码来分别模拟服务器,活动的客户端,僵死的客户端,服务器运行于一个自编译的标准2.6.11内核系统上,硬件为 PIII933,两个客户端各自运行在另外的PC上,这两台PC比服务器的硬件性能要好,主要是保证能轻易让服务器满载,三台机器间使用一个100M交换机连接。

       服务器接受并poll所有连接,如果有request到达则回复一个response,然后继续poll。

       活动的客户端(Active Client)模拟若干并发的活动连接,这些连接不间断的发送请求接受回复。

       僵死的客户端(zombie)模拟一些只连接但不发送请求的客户端,其目的只是占用服务器的poll描述符资源。

        测试过程:保持10个并发活动连接,不断的调整僵并发连接数,记录在不同比例下使用poll与epoll的性能差别。僵死并发连接数根据比例分别是:0,10,20,40,80,160,320,640,1280,2560,5120,10240。

       下图中横轴表示僵死并发连接与活动并发连接之比,纵轴表示完成40000次请求回复所花费的时间,以秒为单位。红色线条表示poll数据,绿色表示 epoll数据。可以看出,poll在所监控的文件描述符数量增加时,其耗时呈线性增长,而epoll则维持了一个平稳的状态,几乎不受描述符个数影响。

       在监控的所有客户端都是活动时,poll的效率会略高于epoll(主要在原点附近,即僵死并发连接为0时,图上不易看出来),究竟epoll实现比poll复杂,监控少量描述符并非它的长处。
### Linux I/O 复用机制:`select`、`poll` `epoll` 的区别比较 #### 一、基本概念 I/O 多路复用是一种允许单个线程同时监视多个文件描述符的技术,以便在任意一个或多个文件描述符变为可读/写时得到通知。Linux 提供了三种主要的 I/O 多路复用机制:`select`、`poll` `epoll`。 --- #### 二、具体特性对比 1. **`select`** - **特点**: - 支持的最大文件描述符数受限于系统常量(通常为 1024)。可以通过重新编译内核调整上限[^3]。 - 每次调用都需要将所有的文件描述符集合从用户空间拷贝到内核空间,并在线性扫描这些文件描述符以判断其是否准备好[^3]。 - 时间复杂度为 O(n),其中 n 表示要监控的文件描述符总数。 - **性能**: - 在少量文件描述符的情况下表现尚可,但在高并发场景下由于需要频繁地复制扫描大量文件描述符,性能下降明显[^3]。 - **典型应用场景**: - 小规模网络应用或者嵌入式设备上的简单通信程序[^3]。 2. **`poll`** - **特点**: - 解决了 `select` 中最大文件描述符数量限制的问题,理论上可以支持无限多的文件描述符。 - 类似于 `select`,仍然存在每次调用都要将所有感兴趣的文件描述符列表传送到内核的空间开销问题[^3]。 - 同样采用轮询方式检测哪些文件描述符已准备就绪,时间复杂度仍为 O(n)[^3]。 - **性能**: - 相较于 `select` 略有改进,但由于底层实现原理相似,在大规模并发情况下依然不够高效[^3]。 - **典型应用场景**: - 较大但非极高的并发环境下的服务器端编程。 3. **`epoll`** - **特点**: - 基于事件驱动模型设计,专门针对高性能需求而优化[^1]。 - 使用红黑树存储注册过的文件描述符及其关联的状态信息;利用双向链表管理活跃的事件队列[^4]。 - 只需一次性的将文件描述符加入到 epoll 实例中即可,后续无需重复传输整个集合至内核层。 - 支持两种触发模式——边缘触发(Edge Triggered, ET)与水平触发(Level Triggered, LT),提供了更大的灵活性[^1]。 - **性能**: - 即使面对海量连接也能维持较高的效率,因为它只会关注那些确实发生了变化的少数几个文件描述符而不是全部检查一遍[^1]。 - 数据结构的设计使得无论是添加还是查询操作都非常迅速,整体时间复杂度接近于 O(1)[^4]。 - **典型应用场景**: - Web 服务、数据库代理等超高并发的服务端架构。 --- #### 三、总结表格 | 属性 | Select | Poll | Epoll | |--------------|---------------------------------|--------------------------------|-------------------------------| | 文件描述符限 | 默认最多 1024 | 无固定限制 | 几乎不受限 | | 效率 | O(n) | O(n) | 接近 O(1) | | 用户态<->内核态交互频率 | 每次都需传递完整集合 | 每次也都需传递完整集合 | 初始化阶段一次性设置 | | 是否支持多种触发模式 | 否 | 否 | 是 | --- #### 四、代码示例 以下是使用 `epoll` 的一段简化版 C++ 示例代码: ```c++ #include <sys/epoll.h> #include <unistd.h> #define MAX_EVENTS 10 int main() { int epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); return 1; } struct epoll_event event; event.events = EPOLLIN; event.data.fd = STDIN_FILENO; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &event) == -1) { perror("epoll_ctl: add"); close(epoll_fd); return 1; } struct epoll_event events[MAX_EVENTS]; while (true) { int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_pwait"); break; } for(int i = 0;i < nfds; ++i){ printf("Event on fd %d\n",events[i].data.fd ); } } close(epoll_fd); } ``` --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值