IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
一旦某个文件句柄就绪,就能通知应用程序进行相应的读写操作;
没有文件句柄就绪时会阻塞应用程序,交出cpu;
多路时指网络连接,复用指同一个线程
IO多路复用的三种实现方式
- select
- poll
- epoll
服务器端采用单线程通过select / epoll等系统调用获取fd列表,
遍历有事件的fd进行accept / recv / send,使其能支持更多的并发连接请求
IO多路复用用来解决什么问题?
当多个客户端与服务器通信时,若服务器阻塞在其中一个客户的read(sockfd1,…),
当另一个客户数据到达sockfd2时,服务器无法及时处理,此时需要用到IO多路复用。
即同时监听n个客户,当其中有一个发来消息时就从select的阻塞中返回,然后调用read读取收到消息的sockfd,然后又循环回select阻塞。
这样就解决了阻塞在一个消息而无法处理其它的。即用来解决对多个I/O监听时,一个I/O阻塞影响其他I/O的问题。
select / poll / epoll之间的区别
select | poll | epoll | |
数据结构 | bitmap | 数组 | 红黑树 |
最大连接数 | 1024 | 无上限 | 无上限 |
fd拷贝 |
每次调用 select拷贝 |
每次调用 poll拷贝 |
fd首次调用epoll_ctl拷贝, 每次调用epoll_wait不拷贝 |
工作效率 | 轮询:O(n) | 轮询:O(n) | 回调:O(1) |
select缺点
单个进程所打开的FD是有限制的,通过FD_SETSIZE设置,默认1024
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)
select不是线程安全的
poll缺点(poll与select相比,只是没有fd的限制,其它基本一样)
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)
select和poll都会随着监控的文件描述符增加而出现性能下降,不适合高并发场景
epoll缺点
epoll只能工作在linux下,但是线程安全
epoll应用:Redis,Nginx
epoll LT (水平触发)与 ET(边缘触发)模式的区别
LT是默认的模式,ET是“高速”模式
LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作
ET模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。
所以在ET模式下,read一个fd的时候一定要把它的buffer读完,或者遇到EAGAIN错误
select的实现
将已连接的socket都放到一个文件描述符集合中,然后调用select函数讲文件描述符集合拷贝到内核中,
让内核检查是否有网络事件产生(遍历文件描述符集合),当检查到有事件产生后,讲此socket标记为可读或可写,
然后把整个文件描述符集合拷贝回用户态里,然后用户态通过遍历找到可读或可写的socket,然后再对其处理
select使用固定长度的BitsMap,表示文件描述符集合,在Linux中,内核中的FD_SETSIZE限制所支持的文件描述符个数,
默认最大值为1024,只能监听0~1023的文件描述符
poll的实现
poll不在使用BitsMap表示文件描述符,而是使用动态数组,以链表的形式组织,突破了文件描述符个数的限制,
其他实现方式与select基本一样,这种方式会随着并发的数量,性能损耗呈指数级增长
epoll的实现
epoll通过两个方面解决了select和poll的问题
epoll在内核中使用红黑树来跟踪进程所有待检测的文件描述符,把需要监控的socket通过epoll_ctl()函数加入内核中的红黑树里,
红黑树是个高效的数据结构,增删查一般时间复杂度是O(logn),通过对这棵树的操作,就不需要想select/poll每次操作都传入整个socket集合,
只需传入一个待检测的socket,减少内核和用户空间大量数据的拷贝和内存分配
epoll使用事件驱动机制,内核里维护了一个链表来记录事件,当socket有事发生时,就会回调函数内核将其加入到这个就绪事件列表中,
当用户调用epoll_wait()函数时,只会返回有事件发生的文件描述符个数,不需要像select/poll那样轮询扫描整个socket集合,提高了检测的效率
即使监听的socket数量很多,也不会造成效率大幅度降低。同时监听的socket数目也非常多,以系统定义进程打开的最大文件描述符个数为上限