Unix下的IO可以区分为5种I/O模型:
- 阻塞式I/O
- 非阻塞式I/O
- I/O复用(select和poll)
- 信号驱动式I/O(SIGIO)
- 异步I/O
Unix下一个输入操作可分为两个步骤:
- 等待数据准备好
- 从内核向进程复制数据
对于一个网络I/O的输入操作也是一样:
- 等待数据从网络到达,复制到内核中的某个缓冲区
- 把数据从内核缓冲区复制到进程缓冲区
阻塞式I/O
Linux默认情况下,所有套接字都是阻塞的。一个从网络读数据的流程如下图:
我们在进程中,让套接字调用读操作(recvfrom函数),这个系统调用让程序在应用进程空间中运行切换到内核空间中运行,系统内核等待网络数据包分组到达,复制到内核中的缓冲区,准备好数据后将数据从内核复制到进程的缓冲区,这时向进程返回成功信号或过程中出错返回错误信号。在这段期间中对于整个进程是阻塞的,直到内核返回信号阻塞才停止,这就是阻塞式I/O模型。
阻塞式I/O特点就是在IO的两个步骤中一直是block的。
非阻塞式I/O
可以将套接字设置为非阻塞模式,同样是上面的例子,新的流程如下图:
从图看出,在非阻塞模式下,IO操作的两个步骤中,第一步进程并不阻塞,调用recvfrom函数后内核等待数据过程,返回给进程EWOULDBLOCK错误,进程反复(轮询)调用recvfrom函数,直到内核缓冲区中数据准备好了,这时将数据从内核缓冲区复制到进程缓冲区,成功后返回给进程成功信号。
这种模式与上面的相比,第一个步骤进程不阻塞,第二个步骤才阻塞。不过这种方式频繁在用户态和内核态中切换,会耗费大量CPU时间,所以这种IO模式并不常遇到。
I/O复用
I/O复用模型调用select或poll函数(目前主流都使用epoll,是select/poll的增强版本),进程阻塞在两个步骤调用的某一个之上,而不是两个都阻塞。下面是IO的流程图:
如上图,进程阻塞于select函数,当内核数据准备好后,返回可读条件,进程在调用recvfrom把数据从内核缓冲区复制到进程缓冲区。
可以看出I/O复用模型,在IO操作的两个步骤中,进程分别都阻塞了,看似和阻塞式I/O相比,还多了一次系统调用。如果只有一次IO操作的话,select反而没有优势了,不过select的优势在于可以等待多个描述符就绪。内核监视select负责的所有socket,当有一个socket中的数据准备好了,select就返回,用户进程就能调用recvfrom将数据从内核缓冲区复制到用户进程缓冲区进行下一步操作了。
在多并发的场景下,select的阻塞和recvfrom的阻塞可以在不同的线程中分别进行,这样能大大提高IO的操作性能。这也是I/O复用在实际场景下被经常使用的原因。
信号驱动式I/O
进程调用sigaction安装一个信号处理函数,系统调用立即返回,进程不阻塞继续工作。当数据准备好后,内核为进程产生一个SIGOI信号,然后我们就可以再信号处理程序中调用recvfrom从内核缓冲区读取数据了。
异步I/O
异步I/O机制原理和信号驱动一样,调用异步IO的函数,告知内核启动某IO操作,内核完成整个IO操作(等待数据和复制到进程缓冲区)后信号通知进程,进程就可以直接从进程缓冲区读取数据。异步IO的流程图如下:
和信号驱动IO的区别在于信号驱动IO是内核通知进程可以从内核缓冲区复制数据,而异步IO是内核通知进程整个IO操作已完成,可以直接从进程缓冲区拿数据了。
同步I/O和异步I/O
从上面总结的5种模型可以看出,前4种模型区别都在于第一阶段,它们的第二阶段是一样的,进程阻塞于recvfrom调用。第5种模型进程都不阻塞,所以也可以将IO归类为两种:
同步IO:请求进程阻塞,直到IO操作完成。
异步IO:不导致请求进程阻塞。
如下图所示,只有异步IO模型才是真正的进程不阻塞,其他模型都有阻塞的阶段。