Select/poll/epoll的区别

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制来监视多个描述符,一旦某个描述符就绪(读或写),就能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。在高并发服务器中,轮询I/O是最耗时间的操作之一。

1. select

select 的本质是通过设置或检查存放fd 标志位的数据结构进行下一步的操作,select的调用复杂度是线性的,每次以O(n)的时间遍历一遍所有的对象。它的缺点如下:

  • 对 socket 的扫描都是线性的,即每一个都要轮询一遍,效率低。当IO事件发生时,却不知道是哪个流或是哪几个流,只会无差异地轮询所有留,找出能进行读或写操作的流来。监视的流越多,需要的处理时间越长。
  • 除了慢之外,单个进程所能监视的fd数量也会受到限制,单个进程所能打开的最大连接数量由FD_SETSIZE 宏定义。32位机器中默认1024个,64位机器中默认2048个。
  • 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

2.poll

poll 本质上和select没有区别,也是通过轮询的方式来进行处理,只是用的存储结构不同。它是基于链表来存储的,所以与select相比没有最大连接数量的限制。但是它仍然需要把大量的fd数组复制于用户态和内核地址空间之间,而不论这些文件描述符是否就绪。开销随着fd的数量增加而线性增大。

3.epoll

epoll可以理解为event poll,epoll会把哪个流发生了怎样的I/O事件通知我们。

epoll提供了三个函数,epoll_create,epoll_ctlepoll_waitepoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

epoll 有两种触发机制: EPOLLLT 和 EPOLLET ,LT是默认的模式,ET是高速模式。

  • LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作.
  • 在ET模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。

epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。因此,epoll的这种机制,能够高效的处理更多的并发连接,而且性能不会随着连接数增加而下降。

优点:

  • 没有最大并发连接的限制。
  • 不需要进行轮询,效率提升,也不会随着fd数量的增加而降低效率。只有活跃的fd才会调用callback函数。
  • 内存拷贝,利用mmap() 文件映射内存加速与内核空间的消息传递,减少复制开销

4.总结

  1. 支持最大连接数:
    • select:由 FS_SETSIZE 宏定义,在32位的机器上,大小就是1024,同理64位机器上FD_SETSIZE为2048。
    • poll:poll采用链表存储,没有最大连接数量的限制。
    • epoll: 虽然连接数有上限,但是很大。
  2. fd增加后带来的IO 效率问题
    • select/poll:因每次调用都要轮询所有的fd,因此效率会线性下降。
    • select:是根据每个fd上的回调函数实现的,只有活跃的fd才会调用callback。但是当所有socket都活跃的情况下,可能会有性能问题。
  3. 消息传递机制
    • select/poll:内核需要将消息传递到用户空间,需要内核拷贝动作。
    • epoll:通过内核和用户空间共享内存来实现。

5. 同步、异步、阻塞、非阻塞

同步:进程执行 IO 操作后,进程等待或者轮询地查看 IO 操作是否完成,等待结果,才能执行后续的操作。

异步:进程执行 IO 操作后直接返回去做自己的事情, IO 交给内核处理,处理完成后再通知进程。

阻塞:进程执行 IO 操作后要一直等待操作完成才能执行后续的操作。

非阻塞:进程执行 IO 操作后,继续处理后续操作,不会被阻塞执行。过后再来询问操作是否完成。

同步有阻塞和非阻塞之分,而异步一定是非阻塞的。

同步和异步是针对应用程序来说的,关注的是程序之间的协作关系;阻塞和非阻塞关注的是单个进程的执行状态。

参考链接