博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
select&epoll
阅读量:6332 次
发布时间:2019-06-22

本文共 7919 字,大约阅读时间需要 26 分钟。

  hot3.png

Select() & Poll()

Select()调用是I/O多路复用模型下的产物,I/O多路复用实际上是一种复合I/O模型,即利用类似于select的调用对阻塞或非阻塞式I/O的一个集合进行监控,epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。

基于POSIX-2001的接口,需要引入4个.h文件,select系统调用的参数一共有5个:
参数1:你所监视的系统描述符fd最大的,然后+1
参数2:fd读的状态有通知了,回填到这个读的集合中
参数3:fd写集合传入,也回填到这个参数
参数4:异常集合传入,也回填到这个参数
参数5:超时设置,如果没有这个参数,2,3,4参数啥都没发生,select就一直阻塞。

经典的select处理方式是这样的:调用者将需要监控的I/O句柄(FD)放入一个数组中,将这个数组传递给select调用,并设定监控何种事件,这时select会阻塞调用进程;当有I/O事件发生时,select就在数组中给发生了事件的那些I/O句柄做一个标记后返回;之后,调用者便轮询这个数组,发现被打了标记的便进行相应的处理,并去掉这个标记以备下次使用。这样,对于服务器程序来说,一个进程或线程就可以处理很多客户端的读写请求了。不过select有一个限制,就是传递给它的I/O句柄最多不能超过1024个。于此人们就引入了poll这个调用,poll调用本质上和select没有区别,只是在I/O句柄数理论上没有上限了,原因是它是基于链表来存储的,但是同样有缺点:

1)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

2)poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

事情看似解决的不错,可是互联网越来越发达了,上网的人也越来越多了,网络服务器在处理数以万计的客户端连接时,经常会出现效率低下甚至完全瘫痪的局面,这就是非常著名的C10K问题。C10K问题的特点是:一个设计不良好的程序,其性能和连接数,以及机器性能的关系往往是非线性的。换句话说,如果没有考虑C10K问题,一个经典的基于select或poll的程序在旧机器上能很好地处理1000连接,它在2倍性能的新机器上往往处理不了2000并发。这是因为大量的操作的消耗与当前连接数n成线性相关,从而导致单个任务的的资源消耗和当前任务的关系会是O(n)。那么服务器程序同时对数以万计的网络I/O事件进行处理所积累下来的资源消耗会相当可观,结果就是系统吞吐量不能和机器性能匹配。为了解决这个问题,必须改变I/O复用的策略。

select这个系统调用是很古老的,一般的Unix的衍生操作系统都支持,移植行也是非常的好,并且基于事件进行组织;但是,通过前面你查看参数就可以总结出来,其缺陷也是多多:

缺陷1:参数1非常的奇怪,最大的fd还要+1,经常有人会没有加1,而导致系统调用失败,对于此,只能怨Unix设计者的古老了,没有招;

缺陷2:2,3,4参数,每一次是我设置的需要监视的参数,需要传入到这个select中,但是在恰恰这个传入的参数,如果有事件发生的话,也是回填到这个参数。==》这相当于什么?相当于你好不容易传入的东西,都被冲掉了,因此,你每一次select完事之后,这些fd你还得重新设置一遍,==》可以总结接口非常的难用,而java的NIO接口在这里也延续了这一习惯。
缺陷3:void FD_CLR(int fd, fd_set *set);===》对于fd描述符的操作集合的方法很不好用,这个极容易引起混淆,注意这个系统调用不是清空,而是删除,名字容易糊涂.
缺陷4:监视的fd,仅仅就是读,写,异常,监视事件范围太单一,不利于查找;

为了解决上述这个问题,大神们就发明了epoll、kqueue和/dev/poll这三套利器。其中epoll是Linux的方案,kqueue是freebSD的方案,/dev/poll是最为古老的solaris的方案,使用难度依次递增。这些方案几乎不约而同地做了两件事:

1)是避免每次调用select或poll时内核用于分析参数建立事件等待结构的开销,取而代之的是维护一个长期的事件关注表,应用程序通过句柄修改这个列表和铺货I/O事件。

2)是避免了select或poll返回后,应用程序扫描整个句柄表的开销,取而代之的是直接返回具体的事件列表。这样,就彻底摆脱了具体操作的消耗与当前连接数n的线性关系,从而极大地提高了服务器的处理能力。

Epoll()

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

Epoll的优点

1)支持一个进程打开大数目的socket描述符

select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024。对于那些需要支持的上万连接数目的服务器来说显然太少了。Epoll虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大。不过这是理论上的,原因就是一个进程所能创建的,或者说能够使用的I/O句柄数是有限制的。默认情况下,Linux允许一个进程对多拥有1024个I/O句柄。虽然可以修改为无限制模式。但是这个不建议,如果要处理上万并发连接的话,最好采用多进程模式,这样不但可以充分利用CPU资源,还可以保证系统的整体稳定性。

2)IO效率不随FD数目增加而线性下降

传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是“活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对“活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个“伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

3)使用mmap加速内核与用户空间的消息传递

这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核与用户空间mmap同一块内存实现的。不管是什么方案,都是避免不了内核向用户空间传递消息,那么避免不必要的数据拷贝就不失为一个绝妙的办法。mmap就能够做到,因为mmap可以使得内核空间和用户空间的虚拟内存块映射为同一个物理内存块。从而不需要数据拷贝,内核空间和用户空间就可以访问到相同的数据。

最后,epoll可以支持内核微调,不过不能把这个优点完全归epoll所有,这是整个Linux系统的优点,赋予你微调内核的能力,就是procfs文件系统,对内核进行微调的开放接口。

Epoll的工作模型

epoll有两种工作模式,即ET(边沿触发)和LT(水平触发),还可以称为事件触发和条件触发。所谓边沿触发就是当状态有变化时,也就是发生了某种事件就发出通知,而水平触发就是当处于某种状态,也可以说是具备某种条件就发出通知。

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。

 

二、select&poll&epoll对比

1.关于fd事件数组的复制

对比了三个系统调用,你可以理解poll和select差不多,实线上是用户态,实线下是内核态。

可以看到select和poll的fd_set集合,是在用户态进行定义,然后你通过系统调用,将这个参数传入到内核态中,这是一次复制,这个数据结构就在内核态也被复制一份(虚线部分);

而select和poll的系统调用结束,发现有一些fd有事件来了,再将这个数据结构,从内核态传回用户态,然后用户再进行遍历;一里一外,这就是两次fd数组的复制,我们要是有10000个fd关注,可以看到,每一次select和poll都来回折腾一遍,消耗太大!

epoll改进在于通过epoll_create系统调用,直接在内核态创建fd数组,没有复制,epoll系统调用结束,发现有一些fd事件来了,将内核态传入用户态,这有一次复制;

总结,一次系统调用查找到有事件的fd,select,poll两次来回在用户态和内核态复制,而epoll只有1次,这个就是第一个优点。

2.fd事件数组的遍历

select和poll在内核中也对应fd_set数组,可以看到这是从用户态拷贝到内核中的,而epoll的fd_set数组是内核中的数据结构,这是我们已知的二者的大不同;正因为如此,select和poll的fd_set数组,就是普通的数据,没有任何的附加功能,因此IO多路复用,硬件事件发生后(也称就绪状态),会直接赋值到这个fd_set数组中;而select和poll在每一次阻塞-唤醒,这一过程中,至少有1到n次的select的轮询工作;select和poll需要遍历,epoll同样也得遍历,但是epoll的机制在于遍历的内容少的吓人,epoll中内核所谓的fd_set集合,并不是遍历的对象,他其中每一个fd都对应回调函数,当就绪事件发生后,将这个真正有事件的fd连同事件,一块放到一个epollfd就绪队列中。可以看到,每一次epoll遍历仅仅是这个fd就绪队列,这个队列中的fd全部都是就绪的,甚至可以这么说,epoll就压根没有遍历,只需判断一下fd就绪队列是否为空,不为空就返回,因此效率惊人,同比100w个fd做监视,对于那种网卡类的稀疏网络事件的情况(也就是大部分时间,甚至99%以上的时间都没事干,没流量),select和poll一般至少要遍历100w次或者200w次,甚至设置超时时间的话,在等待超时时间这段cpu就爆满了;  

但是epoll仅仅遍历1次?2次?最坏的等待超时也仅仅是n次,数量级差太多了,这也就是epoll的优势,总结一下,也就是epoll单独搞了一个fd就绪队列的模式,减少了遍历!

3.fd事件与硬件驱动

从内核角度来看,基于fd事件数组在内核态都需要与硬件驱动进行绑定,绑定是很耗时的,epoll的fd事件数组,就在内核中,绑定1次就OK,而select这些用户态的数组,执行到内核态中,每一次都需要重新绑定一次:

4.fd事件的限制

总结了上面的4条,其实epoll性能优异可以归结于一句话,就是epoll的事件fd放在了内核中,不在用户态折腾了,直接更底层的进行操作,省去了不少的事情,这个是实质的原因!

JAVA早期的NIO的底层实现就是select,JDK1.5_update10开始支持epoll,但底层支持默认使用的是select,可以-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider开启采用的epoll作为底层支撑,JDK1.7及后续版本,在linux的环境下,基本都是epoll了,当然类似于epoll的机制,Solaris中有eventq,FreeBSD中的kqueue也相当的猛,这些都是IO多路复用技术,它们的本质并不是AIO,所谓的AIO至少到目前位置,没有什么好的系统调用实现,虽然有AIO的接口,但基于硬件平台的不同,效果差强人意。而IO多路复用技术,是通过一个按照时钟周期轮询的装置,基于事件去你注册的事件集合,有事件的话直接返回,没有事件的话如果没有超时时间的话,就阻塞,从这一点来看,貌似是异步的过程,但是这个过程和纯AIO还不一样,纯的AIO接口根本不需要什么Selector,epoll实例这些装置,还有上述的各种fd集合的扫描,绑定,遍历,和用户态到内核态的赋值和迁移,直接就是事件驱动,一个注册事件对应一个内核级别的绑定,上述的fd集合这些费劲的东西根本都不需要。不过随着时代的发展,基于硬件的AIO接口现在很多项目已经也在用,效率也是惊人的高的。

三、Epoll的ET&LT

一、水平触发

Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!

水平触发的主要特点是,如果用户在监听epoll事件,当内核有事件的时候,会拷贝给用户态事件,但是如果用户只处理了一次,那么剩下没有处理的会在下一次epoll_wait再次返回该事件。这样如果用户永远不处理这个事件,就导致每次都会有该事件从内核到用户的拷贝,耗费性能,但是水平触发相对安全,最起码事件不会丢掉,除非用户处理完毕。

二、边缘触发

Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!

边缘触发,相对跟水平触发相反,当内核有事件到达, 只会通知用户一次,至于用户处理还是不处理,以后将不会再通知。这样减少了拷贝过程,增加了性能,但是相对来说,如果用户马虎忘记处理,将会产生事件丢的情况。

select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。

AIO

AIO希望的是,你select,poll,epoll都需要用一个函数去监控一大堆fd,那么我AIO不需要了,你把fd告诉内核,你应用程序无需等待,内核会通过信号等软中断告诉应用程序,数据来了,你直接读了,所以,用了AIO可以废弃select,poll,epoll。但linux的AIO的实现方式是内核和应用共享一片内存区域,应用通过检测这个内存区域(避免调用nonblocking的read、write函数来测试是否来数据,因为即便调用nonblocking的read和write由于进程要切换用户态和内核态,仍旧效率不高)来得知fd是否有数据,可是检测内存区域毕竟不是实时的,你需要在线程里构造一个监控内存的循环,设置sleep,总的效率不如epoll这样的实时通知。所以,AIO是渣,适合低并发的IO操作。所以java7引入的NIO.2引入的AIO对高并发的网络IO设计程序来说,也是渣,只有Netty的epoll+edge-triggerednotification最牛,能在linux让应用和OS取得最高效率的沟通。

目前比较知名的异步IO有:

Glibc AIO:
Kernel Native AIO:

Glibc AIO的原理比较简单,采用多线程模拟,但存在一些bug和设计上的不合理。详见:。

Kernel AIO 是真正的能做到内核的异步通知,目前nginx有添加AIO,但它同样有一些缺陷:
libev的作者后来也写了一个libeio,设计上吸取了前两个的经验,使用了一些更tricky的方法,但关于成熟度的问题看介绍页 的第一句话:

Event-based fully asynchronous I/O library for C (used by IO::AIO). Currently in BETA!

http://www.10tiao.com/html/308/201601/401697863/1.html
https://aceld.gitbooks.io/libevent/content/
http://www.cnblogs.com/yuuyuu/p/5103744.html
https://www.zhihu.com/question/26943558

转载于:https://my.oschina.net/mywiki/blog/1538480

你可能感兴趣的文章
WinCE API
查看>>
POJ 3280 Cheapest Palindrome(DP 回文变形)
查看>>
oracle修改内存使用和性能调节,SGA
查看>>
SQL语言基础
查看>>
对事件处理的错误使用
查看>>
最大熵模型(二)朗格朗日函数
查看>>
深入了解setInterval方法
查看>>
html img Src base64 图片显示
查看>>
[Spring学习笔记 7 ] Spring中的数据库支持 RowMapper,JdbcDaoSupport 和 事务处理Transaction...
查看>>
FFMPEG中关于ts流的时长估计的实现(转)
查看>>
Java第三次作业
查看>>
【HDOJ 3652】B-number
查看>>
android代码混淆笔记
查看>>
Codeforces Round #423 (Div. 2, rated, based on VK Cup Finals) C. String Reconstruction 并查集
查看>>
BMP文件的读取与显示
查看>>
Flash文字效果
查看>>
各种排序算法总结篇(高速/堆/希尔/归并)
查看>>
使用c#訪问Access数据库时,提示找不到可安装的 ISAM
查看>>
Highcharts X轴纵向显示
查看>>
windows 注册表讲解
查看>>