前言
本篇博客主要讲解一下网络IO模型。我们常见的网络模型分为 阻塞IO模型,非阻塞IO模型,IO复用模型,信号驱动IO模型,异步IO模型。下面我详细的介绍一下这五个IO模型。
阻塞IO模型
所谓阻塞IO就是当应用A发起读取数据申请时,在内核数据没有准备好之前,应用B会一直处于等待数据状态,直到内核把数据准备好了交给应用A才结束。如下图所示
非阻塞IO模型
所谓非阻塞IO就是当应用A发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用A,不会让应用A在这里等待。
- 当内核中的数据报还没准备好,此时应用A系统调用立即返回一个
EWOULDBLOCK
错误,即不会将用户进程(线程)至于阻塞状态。我们拿Java的NIO来说,当我们配置ServerSocketChannel.configureBlocking(false);
或SocketChannel..configureBlocking(false);
时,我们调用ServerSocketChannel.accept()
的null
或SocketChannel.read(buffer)
不会阻塞的,若没有新连接接入或内核中没有数据报准备好,此时会理解返回null
或0的返回结果,说白了这个返回结果就是对应EWOULDBLOCK
错误;- 当内核中的数据报已经准备好时,此时应用A系统调用,用户进程(线程)还是会阻塞,直到内核中的数据报已经拷贝到了用户空间,此时用户进程(线程)才会被唤醒来处理接收的数据报
非阻塞IO在用户数据报还没有准备好的时候,应用A系统调用不会阻塞,接着会继续进行下一轮的系统调用,看数据报是否准备完成,周而复始的进行轮询,直到有数据返回成功。这个时候是非常耗费CPU的,因此这个模型不是很常用
IO复用模型
可以由一个线程监控多个网络请求(我们后面将称为fd文件描述符,linux系统把所有网络请求以一个fd来标识),这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这就是IO复用模型的思路。
IO复用模型的思路就是系统提供了一种函数可以同时监控多个fd的操作,这个函数就是我们常说到的select、poll、epoll函数,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,select函数监控的fd中只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起应用A请求去读取数据。
后面会详细介绍一下IO复用模型的详细实现原理
信号驱动IO模型
复用IO模型解决了一个线程可以监控多个fd的问题,但是select是采用轮询的方式来监控多个fd的,通过不断的轮询fd的可读状态来知道是否有可读的数据,而无脑的轮询就显得有点暴力,因为大部分情况下的轮询都是无效的,所以有人就想,能不能不要我总是去问你是否数据准备就绪,能不能我发出请求后等你数据准备好了就通知我,所以就衍生了信号驱动IO模型。
于是信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,此时再向内核发起应用A读取数据的请求,因为信号驱动IO的模型下应用线程在发出信号监控后即可返回,不会阻塞,所以这样的方式下,一个应用线程也可以同时监控多个fd。如下图所示
异步IO模型
应用提交一些IO操作到内核,然后不需要去关注这些IO,等到适当的时机,或者内核发信号给应用,或者应用主动询问内核,来获取到IO操作的执行是否完成。这就是异步IO模型的原理。如下图所示
用户进程(线程)在等待数据报和数据报从内核拷贝到用户空间这两阶段都是非阻塞的,即用户进程(线程)发生一次系统调用后,立即返回,然后该用户进程(线程)继续往下执行。当内核把接收到数据报并把数据报拷贝到了用户空间后,此时再通知用户进程(线程)来处理用户空间的数据报。也就是说,这一些列IO操作都交给了内核去处理了,用户进程无须同步阻塞,因此是异步非阻塞的
总结
前4种IO模型都是同步阻塞IO模型,因为其第二阶段数据报从内核拷贝到用户空间都是同步阻塞的,只是第一阶段等待数据报的处理不同;最后一种IO模型(异步IO模型)才是真正的异步非阻塞IO模型,内核将一切事情都干完。