首页 > 代码库 > 从I/O复用谈epoll为什么高效

从I/O复用谈epoll为什么高效

上一篇文章中,谈了一些网络编程的基本概念。在现实使用中,用的最多的就是I/O复用了,无非就是select,poll,epoll

很多人提到网络就说epoll,认为epoll效率是最高的。单纯的这么认为,其实有失偏颇。epoll固然高效,可是它是怎么做到高效的,它到底比select或poll优异在哪儿?

我们通过调用流程来简单分析下。

首先以select为例(poll类似),看下其调用过程

1.选择想要处理的套接字,通过接口FD_SET(fd, &set)加入到set中;

2.调用select(max+1, &set,,..)

3.对set中所有套接字调用FD_ISSET(fd,&set),查看fd上是否有事件发生

select存在的问题

  1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE    1024)
  2. 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
  3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

epoll调用过程

1 .epoll_create 创建一个epoll对象,一般epollfd = epoll_create()

2 .epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件

比如epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注册缓冲区非空事件,即有数据流入     

      epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//注册缓冲区非满事件,即流可以被写入

添加事件的时候,其实是向内核注册了一个回调函数。回调函数作用是,在相应的套接字上发生事件时,将其加入到epoll对象的时间就绪链表中,而这是在内核完成的。

3 epoll_wait(epollfd,...),获取就绪事件。即从就绪事件链表中取出所有的事件。

可以看到epoll比select高效的地方在于,其返回的就是所有已经发生事件的套接字,而不需要像select那样需要在用户态去判断每个套接字上是否有事件发生。

另外,在调用select时,内核需要去一一检测传入的套接字集合是否有事件,而调用epoll_wait时,只是将内核中的就绪数据取出而已

如果有n个连接,并且这n个连接都有事件发生,那么使用select与epoll其实并没有多少区别。对于select来说,用户态对每一个套接字的事件监测都是有效的。

但是select有一个问题是,每次去调用select之前,都要重置套接字set。如果连接数很大,每次FD_SET(fd, &set)调用接口,也会对性能造成不小的影响。而epoll中,只需调用一次epoll_ctl即可。

所以,在连接数很大,且活跃连接不多的情况下,使用epoll有明显的优势;而如果连接数较少,且连接基本都是活跃的,其实select的效果反而会更好。

从I/O复用谈epoll为什么高效