对I/O传输的理解与思考(二)——I/O模型浅谈

前言

    对于两年前刚入大学初接触Java的我来说,当初学习网络编程的时候,看网上谈及I/O模型是一脸懵逼的,这几天,在写Netty的程序的时候再次引起了对I/O模型的兴趣,思来想去,决定搞清I/O模型。学习I/O模型不仅对网络编程有很大的帮助,而且可以从整体上提升你对架构的认识高度。

1. 什么是I/O?

    谈及I/O,不少人会脸色大变,这是编程中的最难啃动的一个点,因为它不仅涉及到如何保证传输的高效与可靠性,还必须的保证它在多线程环境中的安全性。在维基百科里,I/O的定义如下:

I/O(英语:Input/Output),即输入/输出,通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出。

    再细致一点就是,本端与对端遵循某种协议建立起一种连接,在这个连接上传输二进制数据,I/O的本质就是如此,用来传输数据。I/O分为两种,一种是本机传输,即自己与自己建立起连接,也就是通过一个socket与自身建立起一种连接(对,你没看错, socket可以连接自身来进行本地传输),用来传输文本、图片等这些数据。而另外一种则是网络I/O,本篇主要讲网络I/O的几大模型。

    而在Java中,I/O传输分为两种,流式传输(Stream流)与块式传输(Buffer)。在流式传输里面,数据像水流一样在进程间传输,每次一个字节一个字节的传,而块式可以自定义块的大小,一小块一小块的传。实质上,块式传输相当于是在应用层面上添加了一个缓冲区,等到写满缓冲的时候再写入OS内核缓冲区,这样就提升了传输的效率。

2. I/O传输的前言

2.1 I/O传输的阶段

    笔者在对I/O传输的理解与思考(一)——socket详述一文里提到I/O传输一般分为两个阶段,以输入为例:

  1. 等待网络中的数据到达OS内核的缓冲区;
  2. 将数据转移到用户进程的缓冲区内。

    对于第一步,实质上是等待socket中的接受缓冲区满或者数据已经完全到达了(即接受到文本结束符"/r/n"或者"/r"),而第二步实质上是从socket缓冲区转移到用户进程缓冲区。这两步都是阻塞且由OS内核完成的,而明确这两步,是后面区分I/O模型的必要条件。

2.2 同步和异步

    之所以提到这一点,是因为后续提到的I/O模型中有异步的模型。同步的概念很好理解,程序按照顺序一步步执行下来,你总能在程序的某个地方得到上面程序的运行状态。而异步的概念,在操作系统中是如此定义的:

异步程序是已不可预测的速度进行着,走走停停,直到完成后调用通知函数告知别人已经完成。

    从定义中来看,操作系统对异步的定义是用了“不可预测的速度”这一句话,如果以可预测和不可预测来区分同步和异步,那么就很好区分这两者。同步是可预测的,虽然你不能知道程序什么时候运行到某个地方,但是知道程序一定会按照顺序执行,后续程序一定能得到前者同步程序的的状态。而异步是不可预测的,不能预测异步程序是什么时候可以结束执行,所以往往一调用异步程序会立马返回,让得接下来的程序接着进行,后续程序不一定能(取决异步程序的运行速度)得到前者异步程序的状态,结束时会回调通知函数告诉其它程序已经完成,,因为你不知道异步程序什么时候执行完成的,也就不可预测了。
在这里插入图片描述

3. I/O模型的分类

    在《Unix网络编程》卷一中,将I/O模型分为了五种:

  • Blocking I/O 阻塞式I/O
  • Nonblocking I/O 非阻塞式I/O
  • I/O multiplexing I/O多路复用
  • signal driven I/O 信号驱动I/O
  • asynchronous I/O 异步I/O

    而在Java中,我们经常接触到的说法是分为三种:BIO、NIO以及AIO,它们与上面五种分类本质上是一样的,只不过有些细微的差别。网上有人认为阻塞I/O的划分可以视第一阶段是否阻塞而定,而同步异步I/O的划分是取决于第二阶段而定,这种理解可以促进对I/O模型的理解。

3.1 Blocking I/O 阻塞式I/O

    同步阻塞I/O,这类I/O模型应用进程被阻塞,等待网络中数据到达,直到数据从内核缓冲区复制到应用进程缓冲区中才返回,也就是一个socket需要使用一个线程来处理连接、读写。这类I/O模型是我们接触到最多的I/O模型,我们在刚学Java时写的I/O程序就是BIO。这类I/O模型优点是在连接数比较小的情况下比较有效,而且实现起来简单,但是在服务端需要支持高并发的连接时,就需要大量的线程来处理不同的socket连接,就显得效率与资源利用率比较的低下,因为线程的开销是很大的。这类I/O模型适合处理连接少且长的连接。

    在TCP Socket层面上来讲,当客户端socket向服务端server socket发起请求后,服务端产生一个新的socket与客户端socket进行连接,连接后就可进行数据的读写,但是连接、读取、写入都是阻塞的,那么为了不阻塞整个服务端程序的运行,那么每个连接都是需要开启一个线程来处理。

在这里插入图片描述

3.2 Nonblocking I/O 非阻塞式I/O

    同步非阻塞I/O,这类I/O模型,应用进程执行系统调用(读写以及连接)之后,内核会返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。这里轮询的是I/O传输的第一个阶段,如果轮询到第一阶段已经准备好了,那么就会返回一个正确码从而执行第二阶段,但是第二阶段依旧是阻塞的,是同步的。

    这类I/O模型的一个很明确的缺点就是轮询需要独自的一个线程,从而使得CPU的利用率低,毕竟单独轮询就需要一个线程,当连接数多了的时候,也会出现BIO出现的问题。

    从socket层面来讲,非阻塞式I/O是将socket设置为不阻塞的状态,当网络中的数据还未到达时,执行系统调用时会返回一个错误码,这时会开启线程持续进行系统调用,进行轮询操作,其它的步骤与阻塞式I/O相似。
在这里插入图片描述

3.3 I/O multiplexing I/O多路复用

    同步非阻塞I/O,I/O多路复用实现了一个线程处理多个socket请求,它又被称作事件驱动(Event Driver)型I/O。如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。

    然而实际上I/O多路复用只是实现了一个线程处理多个socket连接以及监听每个socket有无网络数据的到达,也就是解决了第一阶段,对于第二阶段依旧是阻塞的,需要开启线程对其进行处理,而Redis的底层通信与Java的NIO都是基于该模型实现的,这种模型是在服务端用的最为广阔的一个模型。

    I/O多路复用模型是基于Reactor模式实现的,它由一个Reactor反应器(或称之为事件分发器)、多个事件处理器(EventHandle)组成,核心是OS内核实现的I/O多路复用程序(Reactor反应器)。它将socket的各个请求封装成一个个事件,比如连接事件(ConnectEvent)、读取事件(ReadEvent)、写入事件(WriteEvent)等,假如其监听下的某个socket接受缓冲区已满,那么就会产生一个读取事件,然后为这个事件分发一个读取事件处理器(ReadEventHandle),并开启一个线程处理,处理完就可以回收该线程或者回收到线程池,它是事件驱动型的,所以它才又被称作为事件驱动型I/O,该I/O模型的主要实现有:select、poll、epoll。

    从整体的架构来看,I/O多路复用主要解决的问题是处理大量并发连接中,只有少量活跃的连接的情况,比如微信这些聊天程序。 如果都活跃那I/O多路复用就与阻塞式I/O没区别了,也就没提高效率,所以I/O多路复用适合于连接数很多但是连接较短的情况。而从socket的层面上讲,I/O多路复用模型是在OS内核里加了一个I/O多路复用程序,用来处理多个socket连接的情况。

在这里插入图片描述

3.4 signal driven I/O 信号驱动I/O

    该类I/O模型是同步非阻塞I/O模型,这类模型是非阻塞式I/O模型的优化版,它不再以通过轮询的方式来确定第一阶段是否完成,而是OS内核实现了一种信号机制,它在第一阶段完成后会发出一种信号来通知用户进程来将OS内核缓冲区的数据转移进用户进程的缓冲区,所以第二阶段是同步阻塞的。

    这个模型的优势在于我们无须等待数据报文到来。主程序可以继续执行,只需要等待被信号通知,一旦通知数据就已经可以处理。这种模式比非阻塞式I/O模型的CPU利用率更高。
在这里插入图片描述

3.5 asynchronous I/O 异步I/O

    异步非阻塞模型,该类I/O模型是唯一一个异步的模型,Java中的AIO就是该种模型,该种模型下,系统进行I/O调用的时候,会立马返回,让进程继续执行,而第一阶段与第二阶段都由OS内核完成,完成后OS内核会发送通知告诉用户进程已经完成,I/O操作是完全异步且非阻塞的。

    与信号驱动型I/O相比,异步I/O更为彻底,信号驱动型I/O只是在第一阶段异步,而第二阶段还是得用户主动发起系统调用来复制数据。异步I/O则是完全把第一阶段和第二阶段完全异步,在I/O传输完成后才通知用户进程。
在这里插入图片描述

4. 总结

  • 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段,应用进程会阻塞。
  • 异步 I/O:不会阻塞。

    阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O,它们的主要区别在第一个阶段。非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。

    网上有人用一个酒吧服务员与客户的关系来描述BIO、NIO与AIO的关系,我觉得很好,在这里分享:

  • BIO:一个服务员全程服务一个客户。
  • NIO:在服务员上有一个客户经理,客户经理会查看哪个客户需要,有需要的客户就派去服务员去服务。
  • AIO:当客户有需要的时候叫一个服务员来服务。

如图:

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值