select、poll、epoll的原理与区别


  系统创建进程是需要消耗大量资源的,所以会导致系统资源不足的情况。那么有没有一种方式可以让一个进程同时为多个客户端端提供服务。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪,能够通知程序进行相应的读写操作。select、poll和epoll这三个函数是Linux系统中I/O复用的系统调用函数。I/O复用使得这三个函数可以同时监听多个文件描述符(File Descriptor, FD),因为每个文件描述符相当于一个需要 I/O的“文件”,在socket中共用一个端口。

select

在这里插入图片描述
1、从用户空间将fd_set拷贝到内核空间
2、注册回调函数
3、调用其对应的poll方法
4、poll方法会返回一个描述读写是否就绪的mask掩码,根据这个mask掩码给fd_set赋值
5、如果遍历完所有的fd都没有返回一个可读写的mask掩码,就会让select的进程进入休眠模式,直到发现可读写的资源后,重新唤醒等待队列上休眠的进程。如果在规定时间内都没有唤醒休眠进程,那么进程会被唤醒重新获得CPU,再去遍历一次fd
6、将fd_set从内核空间拷贝到用户空间

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

  1. 单个进程可监视的fd数量被限制,即能监听端口的大小有限
  2. 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低
  3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll

  poll的实现和select非常相似,其和select不同的地方:采用链表的方式替换原有fd_set数据结构,而使其没有连接数的限制。它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

  1. 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义
  2. poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd

epoll

在这里插入图片描述
  epoll、select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

调用epoll_create时,做了以下事情:

  1. 内核在epoll文件系统里建了个file结点
  2. 在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket
  3. 建立一个list链表,用于存储准备就绪的事件

调用epoll_ctl时,做了以下事情:

  1. 把socket放到epoll文件系统里file对象对应的红黑树上
  2. 给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里

调用epoll_wait时,做了以下事情:

  1. 观察list链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。epoll_wait仅需要从内核态copy少量的句柄到用户态而已

总结如下:

一颗红黑树,一张准备就绪句柄链表,少量的内核cache,解决了大并发下的socket处理问题。

执行epoll_create时,创建了红黑树和就绪链表
执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据
执行epoll_wait时立刻返回准备就绪链表里的数据

epoll的工作方式

epoll的两种工作方式:水平触发(LT)与边缘触发(ET)
两种模式的区别:LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时重复返回这个句柄,而ET模式仅在第一次返回。

两种模式的实现:
  当一个socket句柄上有事件时,内核会把该句柄插入准备就绪list链表,这时调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait检查这些socket,如果是LT模式,并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表。所以,LT模式的句柄,只要它上面还有事件,epoll_wait每次都会返回。

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

epoll的优点:

  1. 没有最大并发连接的限制
  2. 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数
  3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销

epoll同样只告知那些就绪的文件描述符,而且当调用epoll_wait()获得就绪文件描述符时, 返回的不是实际的描述符,而是一个代表就绪描述符数量的值,只需要去epoll指定的一 个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

epoll如何对select和poll的缺点进行改进

  对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时,会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
  对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。

在这里插入图片描述

select、poll、epoll 区别总结

1、支持一个进程所能打开的最大连接数

  • select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试
  • poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
  • epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接

2、fd剧增后带来的IO效率问题

  • select:因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”
  • poll:同上
  • epoll:因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback

3、 消息传递方式

  • select:内核需要将消息传递到用户空间,都需要内核拷贝动作
  • poll:同上
  • epoll:epoll通过内核和用户空间共享一块内存来实现的

猜你喜欢

转载自blog.csdn.net/ThreeAspects/article/details/105845586