原文链接:https://www.leahy.club/archives/select-poll-epoll

select、poll和epoll都是Linux系统中的I/O多路复用的模型,是网络编程的基础知识。首先Linux中有多种I/O模型,比如NIO、BIO、AIO等,比如NIO和BIO可以使用I/O多路复用来提高效率。


select:

Linux中一切皆文件,网络连接使用文件描述符。对于每一个socket连接都是用一个文件描述符(FD)来表示。在Linux中实现用户进程之间网络文件传输必须经过两步:①等待连接建立,并将服务端数据拷贝到本地内核缓存②将内核缓存数据拷贝到用户进程缓存。为了实现IO多路复用,select采用了一个FD_SET来实现多个socket事件的监听和动作。

select首先构建一个FD_SET,然后将FD_SET从用户态拷贝到内核态,由内核态进行事件的监听,监听到感兴趣的事件(读或写)之后,会将FD_SET从内核中拷贝会用户,然后用户遍历读取数据。

固定的bitmap是1024的,可以设置FD_SETSIZE。所以不论定义多少个事件,都是需要维护一个1024位的bitmap。

主要缺点如图中所示的①-④


poll:

最关键的改进是使用了pollfd的一个描述事件的结构体。主要改进了select中的缺点①和②。

①的改进:使用struct取代bitmap,这样可以无限制的扩充pollfds数组


epoll:

主要改进了select中的缺点①-④。

首先其底层也是基于一个事件结构体,所以解决了缺点①②。

select和poll中需要将事件数组从用户拷贝到内核中,并且拿到内核返回的FD_SET之后需要进行轮询来确认具体是哪个事件的状态改变了。而在epoll中具体改进如下:

epoll_create 创建一个白板 存放fd_events。
epoll_ctl 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上。
epoll_wait 通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符并完成相应的动作。

监控大量的FD才能体现epoll的优势,监控少量的FD使用poll或者select也可。


I/O多路复用本质上是减少了用户态和内核态之间的切换。假设Java NIO中没有使用多路复用,而是使用链表等数据结构将各个socket存储起来进行轮询,那么每一次轮询其实都涉及到一次系统的切换,假设100个连接只有1个连接有事件发生,那么剩余的99次切换都是无用功。比如下面这种写法:

看图中的NIO代码,没有使用select,而是使用一个LinkedList存储socket并进行轮询,会涉及到多次系统切换。而使用selector之后是将所有的socket事件的FD整体拷贝到内核中,一旦有一个FD状态改变,那么就整体返回,selector对其进行轮询,找出状态发生变化的事件。poll和epoll的整体思路与之类似,只不过进行了一些改进。


https://segmentfault.com/a/1190000003063859 Linux I/O模型及select、poll、epoll详解

https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md 关于网络编程的各种I/O模型的整理

https://www.bilibili.com/video/BV1i4411p7kk?from=search&seid=4478440997657260007 马士兵讲解的Java I/O模型