select,poll,epoll

先说个故事html

鬼子进村程序员

处理大量的链接的读写,select 是够低效的。由于 kernel 每次都要对 select 传入的一组 socket 号作轮询,这叫鬼子进村策略。一遍遍的询问“鬼子进村了吗?”,“鬼子进村了吗?”... 大量的 cpu 时间都耗了进去。(更过度的是在 windows 上,还有个万恶的 64 限制。)使用 kqueue 这些,变成了派一些我的去站岗,鬼子来了就能够拿到通知,效率天然高了许多。web


 

缓存I/O

缓存I/O又称为标准I/O,大多数文件系统的默认I/O操做都是缓存I/O。在Linux的缓存I/O机制中,操做系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。编程

 

五种IO模型 

阻塞式I/O模型:默认状况下,全部套接字都是阻塞的。recvfrom等待数据准备好,从内核向进程复制数据。windows

非阻塞式I/O: 进程把一个套接字设置成非阻塞是在通知内核,当所请求的I/O操做非得把本进程投入睡眠才能完成时,不要把进程投入睡眠,而是返回一个错误,recvfrom老是当即返回。数组

I/O多路复用:虽然I/O多路复用的函数也是阻塞的,可是其与以上两种仍是有不一样的,I/O多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。其本质是经过一种机制(系统内核缓冲I/O数据),让单个进程能够监视多个文件描述符,一旦某个描述符就绪(通常是读就绪或写就绪),可以通知程序进行相应的读写操做。select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式。缓存

信号驱动式I/O:用的不多,就不作讲解了。服务器

异步I/O:这类函数的工做机制是告知内核启动某个操做,并让内核在整个操做(包括将数据从内核拷贝到用户空间)完成后通知咱们。网络

recvfrom函数(经socket接收数据)。数据结构

 

再看POSIX对这两个术语的定义:

  • 同步I/O操做:致使请求进程阻塞,直到I/O操做完成;

  • 异步I/O操做:不致使请求进程阻塞。

 

select运行机制

select()的机制中提供一种fd_set的数据结构,其实是一个long类型的数组,每个数组元素都能与一打开的文件句柄(不论是Socket句柄,仍是其余文件或命名管道或设备句柄)创建联系,创建联系的工做由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操做,效率更差。可是,使用select之后最大的优点是用户能够在一个线程内同时处理多个socket的IO请求。用户能够注册多个socket,而后不断地调用select读取被激活的socket,便可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须经过多线程的方式才能达到这个目的。

poll运行机制
poll的机制与select相似,与select在本质上没有多大差异,管理多个描述符也是进行轮询,根据描述符的状态进行处理,可是poll没有最大文件描述符数量的限制。
 
epoll运行机制
epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来讲,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll是Linux内核为处理大批量文件描述符而做了改进的poll,是Linux下多路复用IO接口select/poll的加强版本,它能显著提升程序在大量并发链接中只有少许活跃的状况下的系统CPU利用率。缘由就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就好了。

epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减小epoll_wait/epoll_pwait的调用,提升应用程序效率。

  • 水平触发(LT):默认工做模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序能够不当即处理该事件;下次调用epoll_wait时,会再次通知此事件
  • 边缘触发(ET): 当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须当即处理该事件。若是不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你作了某些操做致使该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次)。

LT和ET本来应该是用于脉冲信号的,可能用它来解释更加形象。Level和Edge指的就是触发点,Level为只要处于水平,那么就一直触发,而Edge则为上升沿和降低沿的时候触发。好比:0->1 就是Edge,1->1 就是Level。

ET模式很大程度上减小了epoll事件的触发次数,所以效率比LT模式下高。

 

总结

一张图总结一下select,poll,epoll的区别:

  select poll epoll
操做方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO效率 每次调用都进行线性遍历,时间复杂度为O(n) 每次调用都进行线性遍历,时间复杂度为O(n) 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)
最大链接数 1024(x86)或2048(x64) 无上限 无上限
fd拷贝 每次调用select,都须要把fd集合从用户态拷贝到内核态 每次调用poll,都须要把fd集合从用户态拷贝到内核态 调用epoll_ctl时拷贝进内核并保存,以后每次epoll_wait不拷贝
 
epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数状况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。可是,在并发链接不高的状况下,多线程+阻塞I/O方式可能性能更好。

既然select,poll,epoll都是I/O多路复用的具体的实现,之因此如今同时存在,其实他们也是不一样历史时期的产物

  • select出现是1984年在BSD里面实现的
  • 14年以后也就是1997年才实现了poll,其实拖那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个连接简直就是神同样的存在了,select很长段时间已经知足需求
  • 2002, 大神 Davide Libenzi 实现了epoll


 参考:

个人网络开发之旅——socket编程

https://blog.csdn.net/dapengbusi/article/details/50690690

IO多路复用的三种机制Select,Poll,Epoll

IOCP , kqueue , epoll ... 有多重要?

相关文章
相关标签/搜索