select、poll和epoll区别

epoll VS select

  • 住校时,你的朋友来找你:
  • select版宿管阿姨,带着你的朋友挨个房间找,直到找到你
  • epoll版阿姨,会先记下每位同学的房间号, 你的朋友来时,只需告诉你的朋友你住在哪个房间,无需亲自带着你朋友满大楼逐个房间找人

如果来了10000个人,都要找自己住这栋楼的同学时,select版和epoll版宿管大妈,谁效率高?同理,高并发服务器中,轮询I/O是最耗时操作之一,epoll性能更高也是很明显。

  • select的调用复杂度O(n)。如一个保姆照看一群孩子,如果把孩子是否需要尿尿比作网络I/O事件,select就像保姆挨个询问每个孩子:你要尿尿吗?若孩子回答是,保姆则把孩子拎出来放到另外一个地方。当所有孩子询问完之后,保姆领着这些要尿尿的孩子去上厕所(处理网络I/O事件)
  • epoll机制下,保姆无需挨个询问孩子是否要尿尿,而是每个孩子若自己需要尿尿,主动站到事先约定好的地方,而保姆职责就是查看事先约定好的地方是否有孩子。若有小孩,则领着孩子去上厕所(网络事件处理)。因此,epoll的这种机制,能够高效的处理成千上万的并发连接,而且性能不会随着连接数增加而下降。
selectepoll
性能随着连接数的增加,急剧下降,处理成千上万并发时,性能很差随着连接数增加,性能没有明显下降,处理成千上万并发性能很好
连接数连接数有限制,处理的最大连接数不超过1024,。如果需要超过1024需要修改FD_SETSIZE宏,并重新编译连接数无限制
内在处理机制线性轮询回调callback
开发复杂性

select单个进程可监视的fd数量受到限制,epoll和select都可实现同时监听多个I/O事件的状态。

  • select 基于轮询机制
  • epoll基于os支持的I/O通知机制。epoll支持水平触发和边沿触发两种模式。

select

简介

select通过设置或检查存放fd标志位的数据结构进行下一步处理,这带来缺点:

  • 单个进程可监视的fd数量被限制,即能监听端口的数量有限 单个进程能打开的最大连接数由FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32^32,同理64位机器上FD_SETSIZE为32^64),当然也可对其修改,然后重新编译内核,但性能可能受影响,这需要进一步测试。一般该数和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认1024个,64位默认2048。
  • 对socket是线性扫描,即轮询,效率较低: 仅知道有I/O事件发生,却不知哪几个流,只会无差异轮询所有流,找出能读/写数据的流进行操作。同时处理的流越多,无差别轮询时间越长 - O(n)。

当socket较多时,每次select都要通过遍历FD_SETSIZE个socket,不管是否活跃,这会浪费很多CPU时间。若能给 socket 注册某个回调函数,当他们活跃时,自动完成相关操作,即可避免轮询,这就是epollkqueue

缺点

内核需要将消息传递到用户空间,都需要内核拷贝动作。需要维护一个用来存放大量fd的数据结构,使得用户空间和内核空间在传递该结构时复制开销大。

  • 每次调用select,都需把fd集合从用户态拷贝到内核态,fd很多时开销就很大
  • 同时,每次调用select都需在内核遍历传递进来的所有fd,fd很多时开销就很大
  • select支持的文件描述符数量太小,默认最大支持1024个
  • 主动轮询效率很低

poll

和select 类似,只是描述fd集合的方式不同,poll使用pollfd而非是select的fd_set结构

struct pollfd {
int fd;
short events;
short revents;
};

管理多个描述符也是进行空轮询,根据描述符的状态进行处理,但poll无最大文件描述符数量的限制

poll和select同样存在一个缺点:包含大量文教描述符的数组被整体复制与用户内核态和内核地址空间之间,而不论这些文件描述是否就绪,其开销也随着文件描述符数量增加而非线性增大

  • 将用户态传入的数组拷贝到内核空间
  • 然后查询每个fd对应设备状态:
    • 若设备就绪 在设备等待队列中加入一项继续遍历
      • 若遍历完所有fd后,都没发现就绪的设备 挂起当前进程,直到设备就绪或主动超时,被唤醒后它又再次遍历fd。这个过程经历多次无意义遍历。

无最大连接数限制,因其基于链表存储,缺点:

  • 大量fd数组被整体复制于用户态和内核地址空间间,而不管是否有意义
  • 若报告了fd后,没有被处理,则下次poll时会再次报告该fd

select 和poll的区别

  1. select使用的是定长数组,而poll是通过用户自定义数组长度的形式(pollfd[])。
  2. select只支持最大fd < 1024,如果单个进程的文件句柄数超过1024,select就不能用了。poll在接口上无限制,考虑到每次都要拷贝到内核,一般文件句柄多的情况下建议用epoll。
  3. select由于使用的是位运算,所以select需要分别设置read/write/error fds的掩码。而poll是通过设置数据结构中fd和event参数来实现read/write,比如读为POLLIN,写为POLLOUT,出错为POLLERR:
  4. select中fd_set是被内核和用户共同修改的,所以要么每次FD_CLR再FD_SET,要么备份一份memcpy进去。而poll中用户修改的是events,系统修改的是revents。所以参考muduo的代码,都不需要自己去清除revents,从而使得代码更加简洁。
  5. select的timeout使用的是struct timeval *timeout,poll的timeout单位是int。
  6. select使用的是绝对时间,poll使用的是相对时间。
  7. select的精度是微秒(timeval的分度),poll的精度是毫秒。
  8. select的timeout为NULL时表示无限等待,否则是指定的超时目标时间;poll的timeout为-1表示无限等待。所以有用select来实现usleep的。
  9. 理论上poll可以监听更多的事件

所以又有epoll模型。

epoll

epoll模型修改主动轮询为被动通知,当有事件发生时,被动接收通知,所以epoll模型注册套接字后,主程序可以做其他的事情,当事件发生时,接收到通知后再去处理。

可以理解为event poll,epoll会把哪个流发生哪种I/O事件通知我们。所以epoll是事件驱动(每个事件关联fd),此时我们这些流操作都是有意义的。复杂度也降到O(1)

struct epoll_event {
__u32 events;
__u64 data;
} EPOLL_PACKED;

触发模式

EPOLL LTEPOLL ET两种:

  • LT,默认的模式(水平触发) 只要该fd还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去操作
  • ET是“高速”模式(边缘触发)
public static final int EPOOLLET=epollet();

LT

只会提示一次,直到下次再有数据流入之前都不会再提示,无论fd中是否还有数据可读。所以ET模式下,read一个fd时,一定要把它的buffer读完,即读到read返回值小于请求值或遇到EAGAIN错误。

epoll使用“事件”就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似回调机制激活该fd,epoll_wait便可收到通知。

ET

若用LT,系统中一旦有大量无需读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这大大降低处理程序检索自己关心的就绪文件描述符的效率。 而采用ET,当被监控的文件描述符上有可读写事件发生时,epoll_wait会通知处理程序去读写。若这次没有把数据全部读写完(如读写缓冲区太小),则下次调用epoll_wait时,它不会通知你,即只会通知你一次,直到该文件描述符上出现第二次可读写事件才通知你。这比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。

优点

  • 无最大并发连接的限制,能打开的FD上限远大于1024(1G内存能监听约10万个端口)
  • 效率提升,不是轮询,不会随FD数目增加而效率下降。只有活跃可用的FD才会调用callback函数 即Epoll最大优点在于它只关心“活跃”连接,而跟连接总数无关,因此实际网络环境中,Epoll效率远高于select、poll
  • 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
  • epoll通过内核和用户空间共享一块内存而实现

表面上看epoll的性能最好,但在连接数少且都十分活跃情况下,select/poll性能可能比epoll好,毕竟epoll通知机制需要很多函数回调。

epoll跟select都能提供多路I/O复用。在现在的Linux内核里有都能够支持,epoll是Linux所特有,而select则是POSIX所规定,一般os均有实现。

select和poll都只提供一个函数:select或poll函数。而epoll提供了三个函数:

  • epoll_create:创建一个epoll句柄
  • epoll_ctl:注册要监听的事件类型
  • epoll_wait:等待事件的产生

总结

select,poll,epoll都是I/O多路复用机制,即能监视多个fd,一旦某fd就绪(读或写就绪),能够通知程序进行相应读写操作。 但select,poll,epoll本质都是同步I/O,因为他们都需在读写事件就绪后,自己负责进行读写,即该读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O实现会负责把数据从内核拷贝到用户空间。

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

select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,且把current往等待队列上挂也只挂一次(在epoll_wait开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少开销。
转载自一文搞懂select、poll和epoll区别 - 知乎 (zhihu.com)

Last modification:December 28, 2022
如果觉得我的文章对你有用,请随意赞赏