select、poll、epoll之间的区别

  select,poll,epoll都是IO多路复用的机制。

  I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

  但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

 

  在一个高性能的网络服务上,大多情况下一个服务进程(线程)process需要同时处理多个socket,我们需要公平对待所有socket,对于read而言,哪个socket有数据可读,process就去读取该socket的数据来处理。

  于是对于read,一个朴素的需求就是关心的N个socket是否有数据”可读”,也就是我们期待”可读”事件的通知,而不是盲目地对每个socket调用recv/recvfrom来尝试接收数据。

  我们应该block在等待事件的发生上,这个事件简单点就是”关心的N个socket中一个或多个socket有数据可读了”,当block解除的时候,就意味着,我们一定可以找到一个或多个socket上有可读的数据。

 

select:

  int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

  

  select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。

  调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。

  当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。 select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

  select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

 

select的几大缺点:

  每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

  同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

  select支持的文件描述符数量太小了,默认是1024

 

poll:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

 

 

  poll只是解决了select的fds集合大小1024限制问题。poll改变了fds集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的fds集合限制远大于select的1024。

  poll虽然解决了fds集合大小1024的限制问题,但是,它并没改变大量描述符数组被整体复制于用户态和内核态的地址空间之间,以及个别描述符就绪触发整体描述符集合的遍历的低效问题。

  poll随着监控的socket集合的增加性能线性下降,poll不适合用于大并发场景。

 

epoll:

  epoll是在2.6内核中提出的,是之前的select和poll的增强版本。

 

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

 

 

int epoll_create(int size):

  创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。

  这个参数不同于select()中的第一个参数,select()会给出最大监听的fd+1的值。

  参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。

  当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

 

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):

  函数是对指定描述符fd执行op操作。

  epfd:是epoll_create()的返回值。

  op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。

  fd:是需要监听的fd(文件描述符)

  epoll_event:是告诉内核需要监听什么事,比如读写。

 

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout):

  等待epfd上的io事件,最多返回maxevents个事件。

  参数events用来从内核得到事件的集合。

  maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。

  参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。

  该函数返回需要处理的事件数目,如返回0表示已超时。

 

工作模式:

  LT模式(默认):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。   

  ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

 

LT模式(水平触发):

  是缺省的工作方式,并且同时支持block和no-block socket.

  在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

 

ET模式(边沿触发):

  是高速工作方式,只支持no-block socket。

  在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。

  然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。

  但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

  ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

 

  在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描。

  而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。)

 

epoll的优点:

  对于select的第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时,会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

  对于select的第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd

  对于select的第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,一般来说这个数目和系统内存关系很大。

 

总结:

  select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

  select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

 

posted @ 2018-06-25 11:35  __Meng  阅读(302)  评论(0编辑  收藏  举报