PART1:Unix 常见的五种IO模型:网络编程中的五个 I/O 模型:同步阻塞 I/O(BIO)、同步非阻塞 I/O、 I/O 多路复用(NIO)、信号驱动、异步 I/O(AIO),实际开发工作,最为常用的是 BIO 和 NIO)
- Unix 常见的IO模型:对于一次IO访问(以read举例),数据会先被**拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间**。
- 当一个read操作发生时,会经历两个阶段:
- 等待数据准备就绪 (Waiting for the data to be ready)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
- 正式因为这两个阶段,linux系统产生了下面五种网络模式的方案:
-
Unix 常见的五种IO模型之一:同步阻塞式IO模型BIO(blocking IO model):在linux中,默认情况下所有的IO操作都是blocking
- 在JDK1.4推出JavaNIO之前,基于Java的所有Socket通信都采用了同步阻塞模式(BIO),这种一请求一应答的通信模型简化了上层的应用开发,但是BIO在性能和可靠性方面却存在着巨大的瓶颈。因此,在很长一段时间里,大型的应用服务器都采用C或者C++语言开发,因为它们可以直接使用操作系统提供的异步I/O或者AIO能力。当并发访问量增大、响应时间延迟增大之后,采用JavaBIO开发的服务端软件只有通过硬件的不断扩容来满足高并发和低时延,它极大地增加了企业的成本,并且随着集群规模的不断膨胀,系统的可维护性也面临巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,这会导致恶性循环。正是由于Java传统BIO的拙劣表现,才使得Java支持非阻塞I/O的呼声日渐高涨,最终,JDK1.4版本提供了新的NIO类库,Java 终于也可以支持非阻塞I/O 了。
- 一个典型的读操作流程大概是这样:
- 当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来),而数据被拷贝到操作系统内核的缓冲区中是需要一个过程的,这个过程需要等待。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户空间的缓冲区以后,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
- blocking IO的特点就是在IO执行的下两个阶段的时候都被block了。BIO 模型有两处阻塞的地方
- 等待数据准备就绪 (Waiting for the data to be ready) 阻塞 (服务端阻塞等待客户端发起连接。也就是通过 serverSocket.accept()方法服务端等待用户发连接请求过来。)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 阻塞(连接成功后,工作线程阻塞读取客户端 Socket 发送数据。也就是服务端通过 in.readLine() 从网络中读客户端发送过来的数据,这个地方也会阻塞。如果客户端已经和服务端建立了一个连接,但客户端迟迟不发送数据,那么服务端的 readLine() 操作会一直阻塞,造成资源浪费。)
- BIO模型的特点:或者说BIO模型的缺点:
- (Socket 连接数量受限,不适用于高并发场景)缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1 : 1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。显而易见,如果我们要构建高性能、低时延、支持大并发的应用系统,使用同步阻塞I/O模型是无法满足性能线性增长和可靠性的。
- 有两处阻塞,分别是等待用户发起连接,和等待用户发送数据。这不是个好事情,你想呀,你去买东西,别人给你取盒烟让你等了一个小时,然后你扫码时网不太好,又让你等了半小时,你不得不付现金,他给你找零钱又让你等了一小时…。所以肯定得解决这个两个地方阻塞的问题。用NIO网络模型(NIO网络模型操作上是用一个线程处理多个连接,使得每一个工作线程都可以处理多个客户端的 Socket 请求,这样工作线程的利用率就能得到提升,所需的工作线程数量也随之减少。此时 NIO 的线程模型就变为 1 个工作线程对应多个客户端 Socket 的请求,这就是所谓的 I/O多路复用。)来解决。
- 先理一理哈,在计算机网络这篇中提到了**应用程序建立连接后通过OS实现的TCP协议的socket接口给服务器发了数据**(假设咱们服务器上用的服务器软件是Tomcat),服务器会利用自己体内的endPoint的实现去socket(OS实现的TCP协议的socket接口)中拿到数据,然后再解析成为一个又一个请求,再交给tomcat去处理,处理完响应给客户端。
- I/O 模型典型的Java实现:
public class BIOSever { //在服务端创建一个 ServerSocket 对象 ServerSocket ss = new ServerSocket(); // 绑定端口 9090,然后启动运行服务端,然后阻塞等待客户端发起连接请求,直到有客户端的连接发送过来之后。当有客户端的连接请求后,服务端会启动一个新线程 ServerTaskThread,用新创建的线程去处理当前用户的读写操作。 ss.bind(new InetSocketAddress("localhost", 9090)); System.out.println("server started listening " + PORT); try { Socket s = null; while (true) { // 阻塞等待客户端发送连接请求,直到有客户端的连接发送过来之后,accept() 方法返回Socket s = ss.accept(); new Thread(new ServerTaskThread(s)).start(); } } catch (Exception e) { ... } finally { if (ss != null) { ss.close(); ss = null; } } public class ServerTaskThread implements Runnable { ... while (true) { // 阻塞等待客户端发请求过来 String readLine = in.readLine(); if (readLine == null) { break; } ... } ... }
-
Unix 常见的五种IO模型之二:同步非阻塞式IO模型(noblocking IO model)(一般很少直接使用这种模型,而是在其他 I/O 模型中使用非阻塞 I/O 这一特性。这种方式对单个 I/O 请求意义不大,但给 I/O 多路复用铺平了道路):linux下,可以通过设置socket使其变为non-blocking。
- socket设置为 NONBLOCK(非阻塞)就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误码(EWOULDBLOCK) ,这样请求就不会阻塞
- 当用户进程调用了recvfrom这个系统调用,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个EWOULDBLOCK error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个EWOULDBLOCK error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户空间缓冲区,然后返回。可以看到,I/O 操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续轮询,直到数据准备好为止。整个 I/O 请求的过程中,虽然用户线程每次发起 I/O 请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的 CPU 的资源
- non blocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有:
- 等待数据准备就绪 (Waiting for the data to be ready) 「非阻塞」
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 「阻塞」
- socket设置为 NONBLOCK(非阻塞)就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误码(EWOULDBLOCK) ,这样请求就不会阻塞
-
Unix 常见的五种IO模型之三:IO复用式IO模型(IO multiplexing model):也叫,I/O 多路复用( IO multiplexing,NIO):
- NIO 网络模型,操作上是用一个线程处理多个连接,使得每一个工作线程都可以处理多个客户端的 Socket 请求,这样工作线程的利用率就能得到提升,所需的工作线程数量也随之减少。此时 NIO 的线程模型就变为 1 个工作线程对应多个客户端 Socket 的请求,这就是所谓的 I/O多路复用。
- 既然服务端的工作线程可以服务于多个客户端的连接请求,那么具体由哪个工作线程服务于哪个客户端请求呢?
- 这时就需要一个调度者去监控所有的客户端连接,比如当图中的客户端 A 的输入已经准备好后,就由这个调度者 ,也就是Selector 选择器去通知服务端的工作线程,告诉它们由工作线程 1 去服务于客户端 A 的请求。这种思路就是 NIO 编程模型的基本原理,调度者就是 Selector 选择器。
- IO多路复用是指**通过一种机制监视多个文件描述符fd,一旦某个文件描述符fd就绪(一般是读就绪或者写就绪),或者说内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程,进行相应的读写操作**。
- (IO多路复用模型指的是:使用**单个进程同时处理多个网络连接IO,他的原理就是select、poll、epoll 不断轮询所负责的所有 socket,当某个socket有数据到达了,就通知用户进程。该模型的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接**。)
- 多路指的是多个 Socket 连接
- 复用指的是复用一个线程
- (IO多路复用模型指的是:使用**单个进程同时处理多个网络连接IO,他的原理就是select、poll、epoll 不断轮询所负责的所有 socket,当某个socket有数据到达了,就通知用户进程。该模型的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接**。)
- 在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。
- NIO 比 BIO 提高了服务端工作线程的利用率,并增加了一个调度者,来实现 Socket 连接与 Socket 数据读写之间的分离。
- 与传统的多线程/多进程模型相比,I/O 多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。
- Linux 系统中的 select、poll、epoll等系统调用都是 I/O 多路复用的机制。(IO multiplexing就是我们常说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。)
- select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这些个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程
- select、poll 和 epoll 之间的区别:(select,poll,epoll 都是 IO 多路复用的机制, select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。)
- select:
- 时间复杂度 O(n)。
- select 仅仅知道有 I/O 事件发生,但**并不知道是哪几个流,所以只能无差别轮询所有流,找出能读出数据或者写入数据的流,并对其进行操作。所以 select 具有 O(n) 的无差别轮询复杂度**,同时处理的流越多,无差别轮询时间就越长。
- poll:
- 时间复杂度 O(n)
- poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后**查询每个 fd 对应的设备状态, 但是它没有最大连接数的限制**,原因是它是基于链表来存储的。
- epoll:
- 时间复杂度 O(1)
- epoll 可以理解为 event poll,不同于忙轮询和无差别轮询,epoll 会把哪个流发生了怎样的 I/O 事件通知我们。所以说 epoll 实际上是事件驱动(每个事件关联上 fd)的。
- select:
- JDK 1.4提供了对非阻塞I/O (NIO)的支持,JDK1.5_ update10版本使用epoll替代了传统的select/poll,极大地提升了NIO通信的性能。与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现,这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞I/O以降低编程复杂度,但是对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。
- NIO采用多路复用技术,一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll(0代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。
- NIO采用多路复用技术,一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll(0代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。
- IO多路复用适用如下场合:
- 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用
- 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现
- 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用
- 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用
- 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用
- 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销
- 当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。这个图和blocking IO的图其实并没有太大的不同,事实上因为IO多路复用多了添加监视 socket,以及调用 select 函数的额外操作,效率更差。还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,但是,使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 I/O 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 I/O 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
- 因此对于IO多路复用模型来说: -等待数据准备就绪 (Waiting for the data to be ready) 「阻塞」
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 「阻塞」
- NIO 网络模型,操作上是用一个线程处理多个连接,使得每一个工作线程都可以处理多个客户端的 Socket 请求,这样工作线程的利用率就能得到提升,所需的工作线程数量也随之减少。此时 NIO 的线程模型就变为 1 个工作线程对应多个客户端 Socket 的请求,这就是所谓的 I/O多路复用。
-
Unix 常见的五种IO模型之四:异步 I/O(asynchronous IO):linux下的asynchronous IO的流程
- 用户进程发起aio_read调用之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它发现一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了
- 异步 I/O 模型使用了 Proactor 设计模式实现了这一机制。因此对异步IO模型来说:
- 等待数据准备就绪 (Waiting for the data to be ready) 「非阻塞」
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 「非阻塞」
- 异步 I/O 模型使用了 Proactor 设计模式实现了这一机制。因此对异步IO模型来说:
- 用户进程发起aio_read调用之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它发现一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了
-
Unix 常见的五种IO模型之五:信号驱动式IO模型(signal-driven IO model)
- 首先我们允许 socket 进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用 I/O 操作函数处理数据。
- 首先我们允许 socket 进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用 I/O 操作函数处理数据。
-
- 当一个read操作发生时,会经历两个阶段:



