select与epoll

133 阅读9分钟

select

什么是 Select ?

在很遥远的时代,Internet (互联网) 的使用并不广泛,普通日常用户占总用户量的极小部分, 并且很多网站的内容也十分简单,因此一天的访问量最多可能也就几百的浏览次数。 但随着计算机技术的发展,互联网变得越来越普及,网络用户数开始激增。

网络的数据传输都要涉及到操作系统的网络I/O。在没有 I/O Multiplexing (I/O复用) 的时期,网络服务器应用程序是没有办法同时感知到多个网络链接的到来,也就是说程序没有办法在一段很短的时间内感应到多个I/O事件和各种异常情况。有了I/O复用技术之后,我们可以将此技术应用到网络程序中,从而改善网络服务器应用程序和网络客户端应用程序的表现。

为什么要用 Select ?

select() 是第一个I/O复用技术的实现。它的移植性很好,在不同的操作系统平台上都能见到它的身影。但除此之外,我找不到其他使用 select() 的理由 。当然,如果一个程序对性能和响应时间没有更高的要求的话,照样可以使用 select(),因为它并不差。

其实 select() 的思想很简单,就是返回一段时间内对应的各个检测事件中就绪的文件描述符的数量。假设你有 10 个 file descriptor (fd, 文件描述符) ,这些文件描述符可以是 socket (网络套接字) 也可以是 file (普通的文件) 或者是 standard input (标准输入)。你可以通过 select() 提供的一系列函数去检查这 10 个文件描述符中哪些文件描述符已经处于可读的就绪状态。不仅读事件,select() 还可以同时监测写事件和异常事件

select原理

select本质上是通过设置或检查存放fd标志位的数据结构进行下一步处理。

这带来缺点:

  • 单个进程可监视的fd数量被限制,即能监听端口的数量有限 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试 一般该数和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认1024个,64位默认2048。

  • 对socket是线性扫描,即轮询,效率较低: 仅知道有I/O事件发生,却不知是哪几个流,只会无差异轮询所有流,找出能读数据或写数据的流进行操作。同时处理的流越多,无差别轮询时间越长 - O(n)。

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

监控原理

  • 1.用户向定义各个自己关心的描述符集合,将描述符添加到相应的集合中。
  • 2.调用select接口,将集合传入,将集合中数据拷贝到内核中进行监控,监控原理:在内核中不断进行轮询遍历,判断哪个描述符就绪`可读就绪:读缓冲区中,数据大小大于低水位标记(通常是1个字节)可写就绪:写缓冲区中,剩余空间大小大于低水位标记(通常是1个字节)当前任意一个集合中有描述符就绪,则遍历完集合之后select调用返回select在调用返回之前,将集合中所有未就绪的描述符从集合中移除了select返回的集合是一个就绪描述符集合
  • 3.用户在select调用返回之后虽然无法立即获取就绪的描述符,但是可以通过判断当前哪个描述符还在集合中来判断描述符就是就绪描述符,然后进行相应操作。
  • 4.因为集合被select在就绪返回前被修改了,仅仅保留了就绪的描述符,因此每次重新监控前需要重新添加到描述符集合中。

缺点

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

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

epoll

什么是epoll

epoll接口是为解决Linux内核处理大量文件描述符而提出的方案。该接口属于Linux下多路I/O复用接口中select/poll的增强。其经常应用于Linux下高并发服务型程序,特别是在大量并发连接中只有少部分连接处于活跃下的情况 (通常是这种情况),在该情况下能显著的提高程序的CPU利用率。

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

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

epoll设计思路简介

(1)epoll在Linux内核中构建了一个文件系统,该文件系统采用红黑树来构建,红黑树在增加和删除上面的效率极高,因此是epoll高效的原因之一。

(2)epoll提供了两种触发模式,水平触发(LT)和边沿触发(ET)。当然,涉及到I/O操作也必然会有阻塞和非阻塞两种方案。目前效率相对较高的是 epoll+ET+非阻塞I/O 模型,在具体情况下应该合理选用当前情形中最优的搭配方案。

(3)epoll所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于1024,举个例子,在1GB内存的机器上大约是10万左右。

触发模式

EPOLLLTEPOLLET两种:

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

然后就是epoll提供了两种工作模式,一种是水平触发模式,这种模式和select的触发方式是一样的,要只要文件描述符的缓冲区中有数据,就永远通知用户这个描述符是可读的,这种模式对block和noblock的描述符都支持,编程的难度也比较小;而另一种更高效且只有epoll提供的模式是边缘触发模式,只支持nonblock的文件描述符,他只有在文件描述符有新的监听事件发生的时候(例如有新的数据包到达)才会通知应用程序,在没有新的监听时间发生时,即使缓冲区有数据(即上一次没有读完,或者甚至没有读),epoll也不会继续通知应用程序,使用这种模式一般要求应用程序收到文件描述符读就绪通知时,要一直读数据直到收到EWOULDBLOCK/EAGAIN错误,使用边缘触发就必须要将缓冲区中的内容读完,否则有可能引起死等,尤其是当一个listen_fd需要监听到达连接的时候,如果多个连接同时到达,如果每次只是调用accept一次,就会导致多个连接在内核缓冲区中滞留,处理的办法是用while循环抱住accept,直到其出现EAGAIN。这种模式虽然容易出错,但是性能要比前面的模式更高效,因为只需要监听是否有事件发生,发生了就直接将描述符加入就绪队列即可。

EPOLLET触发模式的意义

若用EPOLLLT,系统中一旦有大量无需读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这大大降低处理程序检索自己关心的就绪文件描述符的效率。 而采用EPOLLET,当被监控的文件描述符上有可读写事件发生时,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所规定,一般操作系统均有实现。

select、epoll原理图

image.png

image.png