1.1 什么是 I/O 多路复用
在阻塞 I/O 中,调用 read 方法等待套接字有数据返回,这个过程中用户线程是阻塞的;在非阻塞 I/O 中,调用 read 方法会立刻返回,但如果没有读取到数据,则需要用户线程通过轮询的方式读取直到有数据返回;两种方式会大量消耗线程及 CPU 资源,实质上是不知道 socket 何时有数据可读,I/O 多路复用的设计初衷就是解决这样的场景。
多路复用函数阻塞监听所有注册在它上面的 socket,在有任何一路(socket) I/O 有事件发生的情况下,通知应用程序去处理相应的 I/O 事件,这样最大的优势就是可以单线程处理所有的 socket I/O 请求,同时也解决了上面两种 I/O 方式出现的问题。
1.1.1 I/O 多路复用的三种机制
-
select: 通知内核挂起进程,当一个或多个 I/O 事件发生后,控制权返还给应用程序,由应用程序进行 I/O 事件的处理;缺点有最大文件描述符限制 1024
-
poll: 突破了文件描述符的个数限制,通过数组形式动态分配
-
epoll:通过监控注册的多个描述字,来进行 I/O 事件的分发处理,性能最高
I/O 事件常用的类型有:监听套接字准备好新连接建立,套接字有数据可读、套接字已准备好可写等
1.1.2 事件触发机制(edge-triggered VS level-triggered)
边缘触发(edge-triggered):只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了
条件触发(level-triggered):只要满足事件的条件,比如有数据可读,就一直不断的把这个事件传递给用户直到数据被读取
select/poll 属于条件触发这一类,边缘触发由 epoll 提供的另一种机制;在条件触发下,如果某个套接字缓冲区可以写,会无限次返回 write ready notification 事件,如果程序不需要发送数据,一定要解除套接字上的 ready notification 事件,否则会出现 CPU 100% 的问题。
一般来说,边缘触发只会产生一次活动事件性能和效率比条件触发的更高,但在编码和程序处理上要更为小心。
1.1.3 epoll 的性能分析
在并发高的情况下,epoll 的性能是最好的,这要从两个角度来说明。
事件集合:每次使用 poll 或 select 之前,都需要准备一个感兴趣的事件集合,系统内核拿到事件集合,进行分析并在内核空间构建相应的数据结构来完成对事件集合的注册。而 epoll 则不是这样,epoll 维护了一个全局的事件集合,通过 epoll 句柄,可以操纵这个事件集合,增加、删除或修改这个事件集合里的某个元素。要知道在绝大多数情况下,事件集合的变化没有那么的大,这样操纵系统内核就不需要每次重新扫描事件集合,构建内核空间数据结构。
就绪列表:每次在使用 poll 或者 select 之后,应用程序都需要扫描整个感兴趣的事件集合,从中找出真正活动的事件,这个列表如果增长到 10K 以上,每次扫描的时间损耗也是惊人的。事实上,很多情况下扫描完一圈,可能发现只有几个真正活动的事件。而 epoll 则不是这样,epoll 返回的直接就是活动的事件列表,应用程序减少了大量的扫描时间。
简单的来说就是 注册事件(更加方便)和事件检测(无需轮询)