网络IO模型:如何解决高并发下IO瓶颈?(一)

目录

阻塞式IO(Blocking IO)

非阻塞式IO(Non-Blocking IO)

IO多路复用(IO multiplexing)

信号驱动式IO(SIGIO)

异步IO(Asynchronous IO)

 


在我们日常工作当中,经常会接触到程序读写磁盘、网络通信,这就是宏观上的网络IO。

IO分为磁盘IO和网络IO,比如我们使用Java提供的流读取磁盘时,会涉及到一些流操作,回顾一下,Java流可总结为下图:

网络IO模型:如何解决高并发下IO瓶颈?(一)

也就是2大类,字节流和字符流。

我们知道,字节是信息存储的最小单元,这里你可能会有疑问,既然Java提供了字节流,为什么还要使用字符流呢?

很多情况下,字节流已经可以满足大多数场景下的数据传输,但是字节数据还需要经过一次转换,也就是转成字符,才能被我们看懂,这个转换过程会涉及到编码转码,如果由于字段集的不同很有可能出现乱码,字符流就是为了避免出现这种情况。

Java流模式是一种传统的网络IO模型:阻塞式IO(BIO)。

在《UNIX网络编程》一书中,把网络IO模型分为5种,分别是:阻塞式IO,非阻塞式IO,IO复用,信号驱动式IO,异步IO。

下面我们就从应用程序和操作系统(跟图中的"内核"是一个概念)交互的角度,来分别了解这5种网络IO模型。

阻塞式IO(Blocking IO)

这是最常用、最简单的IO模型,常用是因为它简单好理解。

网络IO模型:如何解决高并发下IO瓶颈?(一)

应用程序一个线程向操作系统发起一次读磁盘请求时(我们把recvfrom函数视为系统调用),此时该线程进入阻塞状态。

操作系统处理IO操作分为2个阶段——等待数据和拷贝数据,等待数据也是一个阻塞的状态。

非阻塞式IO(Non-Blocking IO)

即Non-Blocking IO,注意跟 Java NIO 区分开。

进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。

网络IO模型:如何解决高并发下IO瓶颈?(一)

从上图来看,前三次调用 recvfrom 时没有数据可返回,因此内核立即返回一个EWOULDBLOCK错误。第四次调用 recvfrom 时已有一个数据报准备好,它被复制到应用进程缓冲区,于是 recvfrom 成功返回。我们接着处理数据。

IO多路复用(IO multiplexing)

什么是 IO 多路复用呢?多路就是指多个通道,也就是多个网络连接的 IO,而复用就是指多个通道复用在一个复用器上。Linux 提供了 I/O 复用函数 select/poll/epoll,进程将一个或多个读操作通过系统调用函数,阻塞在函数操作上,而不是阻塞在真正的IO系统调用上。

网络IO模型:如何解决高并发下IO瓶颈?(一)

从上图可以看出,进程阻塞于 select 调用,等待数据报变为可读。当 select 返回数据报可读这一条件时,进程调用 recvfrom 把所读数据报复制到应用进程缓冲区。

比较IO复用和BIO模型图,IO复用并不显得有什么优势,事实上由于使用 select 需要两个而不是单个系统调用,IO复用还稍有劣势。使用 select 的优势在于应用进程可以等待多个描述符就绪。

信号驱动式IO(SIGIO)

用户进程发起一个 I/O 请求操作,会通过系统调用 sigaction 函数,给对应的套接字注册一个信号回调,此时不阻塞用户进程,进程会继续工作。当内核数据就绪时,内核就为该进程生成一个 SIGIO 信号,通过信号回调通知进程进行相关 I/O 操作。

网络IO模型:如何解决高并发下IO瓶颈?(一)

异步IO(Asynchronous IO)

信号驱动式 I/O 虽然在等待数据就绪时,没有阻塞进程,但在被通知后进行的 I/O 操作还是阻塞的,进程会等待数据从内核空间复制到用户空间中。而异步 I/O 则是实现了真正的非阻塞 I/O。

网络IO模型:如何解决高并发下IO瓶颈?(一)

参考资料:

《UNIX网络编程》

刘超极客专栏《Java性能调优实战》11讲