1.前景回顾
无论是阻塞IO还是非阻塞IO,用户应用在一阶段都需要调用recvfrom来获取数据,差别在于无数据时的处理方案:
-
如果调用recvfrom时,恰好内核没有数据,那么阻塞IO会使用户进程阻塞,非阻塞IO使CPU进行空转,都不能充分发挥CPU的作用。
-
如果调用recvfrom时,恰好内核有数据,则将内核数据读取到用户区处理即可。
而且在多个读取请求单线程处理的情况下,只能依次处理一个个请求,一旦正在处理的请求的数据没有准备好,那么全体阻塞,性能很差。
可以利用多线程来改进,但也要考虑上下文切换的时间成本。能不能利用一个线程同时监听这多个读请求,一旦哪个请求所需要的数据在内核中准备就绪了,我们就去读取数据。
那么用户进程如何知道内核中的数据是否准备好了呢?
2.FD(文件描述符)
文件描述符:简称FD,是一个从0开始递增的无符号整数,用来关联Linux中的一个文件,也包括Socket。IO多路复用是利用一个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
3.IO多路复用概述
监听FD的方式又有多种实现,监听都是在内核实现的,常见的有:select、poll和epoll模式。
它们之间的差异在于:
-
select和poll只会通知用户进程有几个FD就绪,但不确定具体是哪个FD,还是需要用户进程逐个遍历FD来确认。
-
epoll则会在通知用户进程FD就绪的同时,把已经就绪的FD写入用户空间。
4.IO多路复用——select
select是Linux中最早的I/O多路复用实现方案,采用数组实现:
4.1select方案的缺点:
-
用户进程无法得知是哪个fd就绪,需要遍历整个fd_set。
-
fd_set监听的fd数量不超过1024。
-
需要来回将整个fd_set在用户和内核之间互相拷贝。
5.IO多路复用——poll
poll模式对select模式做了简单的改进,但性能提升不明显,也是只会通知用户进程有几个fd准备就绪了。区别就是poll模式在内核中采用链表存储,理论上无上限,但是监听的FD越多,每次遍历耗时也会越长,性能反倒会下降。
6.IO多路复用——epoll
epoll模式中内核采用的是call back事件回调,利用红黑树保存要监听的FD,而且每个FD只需要初始时添加一次到红黑树,等到红黑树中的FD就绪了,会自动触发事件,把对应的FD加入到一个就绪列表(list_head)中,当用户进程要检查内核就绪列表时(调用epoll_wait),如果列表不为空则返回已就绪的FD的数量,并把链表中的值拷贝到用户空间的events数组中,用户空间根据数组中的值就能直接定位可以读取的FD,然后去读取数据即可。
7.IO多路复用——事件通知机制
对于用户进程来说,我们调用epoll_wait函数查看就绪列表就能得到fd的通知。其事件通知的模式有两种:LT和ET。一个例子:
LT:当就绪队列中有可读的FD时,调一次通知一次,这样会重复通知多次,直至数据全部读取完成。(默认)
ET:当就绪队列中有可读的FD时,只会通知一次,然后直接删除,不管用户是否一次能够读完。
根据上面的分析,我们发现那肯定是LT好,但是LT会存在惊群问题,比如我们有多个进程同时在关心这一个socket的读取数据,其实实际读取操作一两个进程就能完成,但是你每次一调用epoll_wait函数,都会惊动所有进程去读,所以就没有必要。
8.IO多路复用——Web服务流程