一、Select
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。 用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,select在socket编程中还是比较重要的。一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常!!
select模型的特点:
(1) FD连接数量 :单个进程可监视的 fd数量被限制(支持最大连接数1024(x86) or 2048(x64));
(2) I/O效率:返回的可读集合是个fdset类型,需要对所有的监听读句柄一一进行FD_ISSET的测试来判断是否可读; 对描述符进行扫描时是线性扫描,比较耗时,时间复杂度线性增加,每次调用进行 线性遍历,时间复杂度为O(N)。比如如果只监听0和1000两个句柄,select需要遍历1001个句柄来检查事件。
select需要对所有的监听读句柄扫描判断是否是自己关心的一一进行FD_ISSET的测试来判断是否可读,是否发生了事件;
(3) 空间效率 :FD每次都全部拷贝。需要维护一个用来存放大量fd的数据结构, 这样会使得用户空间和内核空间在传递该结构时复制开销大;
二、Poll
poll模型的特点:
(1) FD连接数量:Poll 没有最大连接数的限制,
(2) I/O效率:poll运行过程中,也是将用户传入的数 组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历,每次调用进行线性遍历, 时间复杂度为O(N);原因是它是基于链表来存储的,但是同样有一个缺点: 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
(3)空间效率:FD每次poll都拷贝。
1、和select相比,poll的优点是:
(1) 不再局限于FD_SETSIZE个监听描述符,只要能打开的描述符,都可以监听;
(2) 监听描述符集合不再是值结果参数,而是event表示监听事件,revents表示触发的事件;
(3) poll的效率比select稍高(poll只遍历输入的监听数组中的描述符,如果数组中的fd<0,则poll忽略fd,当监听的描述符离散时效率稍高于select。比如监听0和1000两个句柄,则poll只需要遍历两个描述符,而select需要遍历1001个描述符;当监听描述符连续时,poll和select效率相当,底层实现也是一致的)。
解决了select中的问题(1),一定程度上也解决了(2),比如如果只监听0和1000两个句柄,select需要遍历2个描述符来检查事件,而select需要1001次。
2、poll和select共同的问题是性能较差:
(1)遍历所有的文件描述符,当监听描述符个数增加时, 监听效率降低,(2)并且select和poll每次都要在用户态和 内核态拷贝监听的描述符参数。
三、Epoll
epoll是为处理大批量句柄而作了改进的poll。是提高内核I/O性能的新方法。当同时需要保持很多的长连接,而且连接的开关很频繁时,就能够发挥epoll最大的优势了。在一些 benchmark中, 如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,空闲的连接量十分大,偶尔蹦出来几个活跃的客户端,epoll的效率就远在select/poll之上了。它是在2.5.44内核中被引进的:
(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。 epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用。
1、int epoll_create(int size);
注:当创建好epoll句柄后, 它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数, 它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事,struct epoll_event结构
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。
4、相比与select和poll模型,epoll模型的优点:
(1) FD连接数量:Epoll它没有最大连接数的限制,只受进程打开描述符总数的限制;
(2)I/O效率:epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是 ,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,返回参数中就是触发事件的列表,不用再遍历输入事件表查询各个事件是否被触发。内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。时间发复杂度 O(1)
(3)空间效率:调用epoll_ctl时拷贝进内核并由内核保存,之后每次epoll_wait不拷贝, 使用共享内存的方式,不在用户和内核之间反复传递监听的描述符信息;epoll使用 mmap减少复制开销。
(4)监听性能不随着监听描述符数的增加而增加 ,是O(1)的,不再是轮询描述符来探测事件,而是由 描述符主动上报事件;
epoll显著提高性能的前提是:监听大量描述符,并且每次触发事件的描述符文件非常少。
5、 select、poll和epoll性能PK
(1)、epoll模型的最大特点就是没有setfpdsize大小的限制;
而select有大小的限制1024个;可以在修改内核的大小,但是资料显示这个会导致效率下降;还可以选择多进程 ,但是多进程之间的数据的同步和开销在大量的连接请求上的效率是比较低下的;可以在ect/ frofile/maxfile中查看最大的数量,这个值一般和内存有很大的关系,比如1GB的内存大概有10万个FD文件描述符,句柄;
(2)、IO 效率不随FD数目增加而线性下降
二是基于事件的通知机制而不是线性扫描,内核采用回调函数callback来调用相应的处理函数。传统的select和poll模型中进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,并且会随着fd数量的增加而使其效率线性下降,因为poll和select是线性的扫描各个文件描述符来实现的;而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
(3)、使用mmap加速内核 与用户空间的消息传递。
采用mmap技术省去了结构体FD从用户态向内核态空间复制的时间内存开销。epoll模型使用mmap加速内核与用户之间的消息传递;避免了传统的select、和poll的一些不必要的内存拷贝,类似fork的(Copy-On-Write)COW技术一样,epoll和用户是公用一块内存空间实现的;
注:使用mmap加速内核 与用户空间的消息传递。mmap是将一个文件和对象映射到内存下;
四、epoll的两种工作支持水平触发和边沿触发。
LT(Level Triggered) 水平触发是epoll缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
ET (edge-triggered) 是高速工作方式,只支持no-block socket,它效率要比LT更高(该模式的工作原理决定了它只能支持非阻塞模式)。ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。因此,LT模式下开发基于epoll的应用要简单些,不太容易出错。而在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。在LT-水平触发和缺省模式下,只要是该缓冲区中有数据,则总是能从epoll_wait中获取该事件。显的就比较安全。
简而言之:
水平触发(level-triggered)——只要满足条件,就触发一个事件(只要有数据没有被获取,内核就不断通知你);
边缘触发(edge-triggered)——每当状态变化时,触发一个事件。例如:
1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另一端被写入了2KB的数据
3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
4. 然后我们读取了1KB的数据
5. 调用epoll_wait(2)......
若是LT工作方式,内核会继续通知你RFD句柄就绪,里面还有1KB的数据没有读处理;但是若是ET工作方式,内核将只会通知一遍,剩下的IKB数据在下一RFD句柄没有新的活动(数据的发送或接受)的情况下,就不会被应用程序处理,造成请求得不到响应。所以对于ET触发的,fd必须是非阻塞式的,这样可以一次把数据读完,如果是阻塞式的,说不定则会读到阻塞;
五、epoll使用注意问题:
注意一:
int epoll_create(int size); //创建epoll文件描述符
参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的 一个建议。返回是epoll描述符。-1表示创建失败。
注意二:
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//等待epfd上的io事件,最多返回maxevents个事件
timeout = -1 的行为是未定义的;timeout = 0 是立即返回
注意三:
epoll监听ET事件时,fd必须是非阻塞套接口。比如监听可读事件,当ET上报可读后,需要一直读fd直到遇到EAGAIN错误为止,以免遗留数据在缓冲区中。如果fd是阻塞的, 则会读到阻塞了。
EAGAIN错误对于非阻塞套接口来说不是错误,只是说没有数据可读或者没有空间可写。
EWOULDBLOCK就是EAGAIN,值都是11。
s elset/poll/epoll的LT模式监听的fd可以是阻塞模式的(因为只要满足条件,数据没有读完,内核就会持续通知你)。
博文资料参考:
http://blog.csdn.net/xiajun07061225/article/details/9250579
http://blog.163.com/huchengsz@126/blog/static/73483745201181824629285
http://blog.csdn.net/turkeyzhou/article/details/8504554
http://blog.chinaunix.net/uid-26912934-id-3308700.html
在此感谢各位博主的分享。