多路I/O复用是一个异步阻塞方式,Linux中提供了select、poll和epoll三种阻塞监听的方式,只有一个进程,避免了CPU在多进程和多线程之间的切换。
反应堆就是阻塞监听的事件的集合。
select的反应堆(监听集合)是一个文件描述符集合fd_set,分别有读事件、写事件、错误事件,这些个fd_set是一个输入输出型的参数,即输入时告诉select要监听哪些文件描述符的事件,做输出时则返回反应堆中事件触发的fd们。
select的缺点是:
-
fd_set最大为1024个fd,即select模型最多只能监控1024个客户端=》但这个1024是可以改变的,跟内存占用和文件系统上有关。
-
select是轮询方式,即使只有一个1000的fd也会从0开始轮询,效率性能会很低。所以在处理活跃的套接字时,时间复杂度为O(N)。
使用select( )来阻塞监听。
poll的反应堆(监听集合)是一个结构体数组,每个元素是一个struct pollfd类型的结构体,成员分别表示:要监听哪一个套接字文件描述符、监听哪种事件、结果是触发了哪种事件。相比于select,poll模型把反应堆和返回值区分开来,通过两个变量来传递。poll模型中在处理活跃的套接字时,需要从头遍历整个反应堆,无论是否活跃,所以时间复杂度是O(N)。
使用poll( )来阻塞监听。
epoll模型是select和poll的改进版本,适用于存在大量并发但只有少量活跃的情况下。
epoll模型的反应堆(监听集合)是一棵平衡二叉树,这棵树存在于内核空间。想要监听某一个套接字,就把它包装成一个节点挂在这颗二叉树上。epoll会监听这棵树上所有的文件描述符,是否读触发、写触发、错误异常。=》树上节点:key-value对,键值就是socket fd,Value就是具体监听的内容。
凡是触发状态的套接字都会被放在一个就绪队列中,这样当处理事件时,就不需要遍历整个反应堆,而只需要遍历这些真正活跃的套接字fd,这样时间复杂度为O(logN)。=》只监听一次事件,就把这个节点从树上摘下来了。当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列中。
epoll除了提供select/poll的那种IO事件的电平触发,还提供了边沿触发。
使用epoll_wait( )来阻塞监听。
需要强调的是,epoll只适用于大量并发但只有少量活跃的情况,如果大量并发全部活跃它的性能则和poll模型差不多,甚至还要差,因为本身维护这颗二叉树也是需要很大的时间性能消耗的,插入删除节点,这棵树的价值只有在大量并发少量活跃的情况下才能体现出来。
三种模型都有一个共同点,就是timeout超时时间。因为都是阻塞监听,所以可以设置timeout,有3种情况:1.永远阻塞等待;2.等待固定时间,如果没有套接字活跃,则返回0;如果有,则返回对应可读的文件描述符个数;3.检查fd之后立即返回,非阻塞轮询方式。
另外,都不需要开多进程或者多线程,就可以处理多个客户端的请求了,并且也不需要忙于CPU的调度。
时间复杂度分析:select、poll在处理事件时需要循环的是监听的socket fd的个数,时间复杂度均为O(N)。而epoll循环的是活跃的socket fd的个数,时间复杂度为O(logN)。