[Netty] [Netty]IO本质,IO简介分类,IO模型发展(一)

框架与中间件 框架与中间件 8981 人阅读 | 0 人回复

Netty是高性能的网络编程框架,基于Netty使我们的业务逻辑代码与网络传输部分的代码,可以进行很好地分离,更容易的进行应用的开发。

Netty的编程模型基于NIO,或者说与NIO的思维方式同出一辙,而介绍NIO,就不得不从IO的模型、分类开始说起,这样更具备系统性,也跟容易理解。

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

本专栏系列大致路线为从IO介绍开始,然后梳理NIO,最后过度到Netty。 所以接下来对IO简单介绍

宏观的IO模型

image.png 从宏观的角度看,我们的应用程序,都是运行在操作系统之上的。

现代操作系统,负责管理硬件资源,所有的应用程序都是构建于操作系统之上的,为了保护硬件等资源,操作系统设置了一定的机制和保护机制或者说隔离机制。

内核与用户

资源的访问,需要依赖操作系统,也就是在用户应用程序执行的时候,需要切换到操作系统。

操作系统将内存(虚拟内存)划分为两部分,一部分是内核空间(Kernel-Space),一部分是用户空间(User-Space)。

这就是我们常说的用户程序、内核程序,用户态、内核态,用户空间、内核空间等都是类似的意思。

了解过操作系统的应该对这块都不陌生。

操作系统提供了用户接口以及应用程序接口。 用户接口比如控制台输入命令行进行人机交互; 应用程序接口比如调用系统函数实现某种功能;

对于IO,用户程序进行IO的读写,依赖于底层的IO读写,基本上会用到底层的两大系统调用: 系统读 sys_read & 系统写 sys_write 。

缓冲区

为了提高执行效率,均有设置缓存区,有用户缓冲区和内核缓冲区。

这样当系统获得足够的资源后才会进行切换响应,减少频繁地与设备之间的物理交换

计算机的外部物理设备与内存与CPU相比,有着非常大的差距,外部设备的直接读写,涉及操作系统的中断。

发生系统中断时,需要保存之前的进程数据和状态等信息,而结束中断之后,还需要恢复之前的进程数据和状态等信息。

这就是必须要设置缓冲区的原因。 image.png 当发生调用时,其实是缓冲区之间的读取和写入。

  • 应用程序使用sys_read系统调用时,仅仅把数据从内核缓冲区复制到上层应用的缓冲区(用户缓冲区);
  • 上层应用使用sys_write系统调用时,仅仅把数据从应用的用户缓冲区复制到内核缓冲区中;

异步 同步 阻塞 非阻塞

日常中经常听到这样几个概念,到底如何理解?

同步异步,我们指的是两个或者以上的进程之间,一种协作的状态,协作方式,是从全局更高的角度 “进程之间 合作的方式”,关注的是消息的通信机制; 同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。 也就是由调用者主动等待这个调用的结果。 不管是一直卡住不动的等待,还是后期主动地不断地轮训,都是属于同步。

异步就是调用发出之后,这个调用就返回了,是否有结果并不影响继续执行,当有了结果之后,通过事件、状态、回调等方式进行通知调用者 这就是异步。

同步需要主动关注结果,异步则是被动的接受通知

阻塞与非阻塞相对来说比较容易理解,就是调用方在请求时可能会出现的状态。 指的是单一进程视角观察到的状态,当发起调用时,自身被阻塞不能继续进行,那么称之为阻塞,反之则是非阻塞。

发出请求后是自己主动的获取结果还是等通知?决定了是同步还是异步。 发布请求后,自身是否会被卡住还是可以继续执行?这决定了是阻塞还是非阻塞。

POSIX IO分类

  • 阻塞式IO(Blocking IO)
  • 非阻塞式(IONon-Blocking IO)
  • IO多路复用(IO Multiplexing)
  • 信号驱动式IO
  • 异步IO(Asynchronous IO)

不管是什么类型的IO,基本模型都是请求-响应,这两个阶段。

阻塞式IO(Blocking IO)

除了特指NIO以及AIO,我们平时说的IO通常指的就是阻塞的。 image.png recvfrom视为系统调用

第一阶段应用程序通过recvfrom从系统中获取数据。 最初系统内核中肯定是没有数据的,所以需要准备数据,这需要一个过程。

准备好了之后,在第二个阶段,需要复制到用户空间。 用户空间其实就是应用程序所在的空间,应用程序就是用户程序,区别于系统程序和内核空间。

这就是典型的阻塞模式的IO,在recvfrom 返回数据之前,会一直阻塞,应用程序的进程将会一直卡住,无法做其他的事情。 调用了方法,方法不返回,你不能动。

阻塞IO的特点是:

在内核进行IO执行的两个阶段,发起IO请求的用户进程(或者线程)被阻塞了。

阻塞IO的优点是:

应用的程序开发非常简单;在阻塞等待数据期间,用户线程挂起,用户线程基本不会占用CPU资源。

阻塞IO的缺点是:

一般情况下,会为每个连接配备一个独立的线程,一个线程维护一个连接的IO操作。 在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。 本文作者:程序员潇然 疯狂的字节X https://crazybytex.com/

在高并发应用场景中,阻塞IO模型是性能很低的,基本上是不可用的。也就是说如果是高并发,阻塞IO模型是不行的。

非阻塞式(IONon-Blocking IO)

image.png 非阻塞IO,指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间去执行后续的指令。

即发起IO请求的用户进程(或者线程)处于非阻塞的状态,与此同时,内核会立即返回给用户一个IO的状态值。

第一个阶段,前面几次的recvfrom调用,并没有数据可以返回,所以返回了一个EWOULDBLOCK;

第二个阶段,第四次调用的时候,有数据准备好,被复制到用户空间,于是recvfrom调用成功返回,用户进程可以继续处理数据;

PS: 对于这种对一个非阻塞描述符,循环调用的形式,通常称之为轮训(polling),很显然这会浪费CPU资源,因为一直在空循环做无用功,而且期间如果什么都没做,只是单纯地循环,那么事实上与阻塞式无异了。

阻塞与非阻塞的区别很简单,就是没有拿到结果之前,你有没有机会做其他的事情。 阻塞是指用户进程(或者线程)一直在等待,而不能干别的事情; 非阻塞是指用户进程(或者线程)拿到内核返回的状态值就返回自己的空间,可以去干别的事情;

非阻塞IO的特点:

应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止。

非阻塞IO的优点:

每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

非阻塞IO的缺点:

不断地轮询内核,这将占用大量的CPU时间,效率低下。

总体来说,在高并发应用场景中,非阻塞IO也是性能很低的,也是基本不可用的,一般Web服务器都不使用这种IO模型。

在Java的实际开发中,也不会涉及这种IO模型。 但是此模型还是有价值的,其他IO模型中可以使用非阻塞IO模型作为基础,以实现其高性能

特别注意: 此处讨论的是IO模型,而不是java类库的NIO。

同步非阻塞 IO 也可以简称为 NIO,但是,它不是 Java 编程中的 NIO,虽然它们的英文缩写一样,但是不能混淆。 Java 的 NIO(New IO)类库组件,所归属的不是基础 IO 模型中的NIO( None Blocking IO)模型,而是另外的一种模型,叫做 IO 多路复用模型( IO Multiplexing)

此处也特别提醒一下,当阐述概念模型时,以及具体类库、方法的名字等,如果有同名现象,并不代表说的是同一件事情。

IO多路复用(IO Multiplexing)

image.png

第一个阶段,发起调用时,阻塞在select上,等待数据变为可读。 第二个阶段,当返回可读状态时,就可以通过recvfrom把读取到的数据包复制到用户缓冲区。

select也是阻塞,那有什么优势? 而且,多使用一次调用,本来是通过recvfrom 现在是还需要额外的select。

为了提高性能,操作系统引入了一类新的系统调用,专门用于查询IO文件描述符的(含socket连接)的就绪状态。

在Linux系统中,新的系统调用为select/epoll系统调用。 通过这种系统调用,解决了轮询的问题,提高了轮询的性能 因为一个进程可以监视多个文件描述符(包括socket连接),一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序。 随后,应用程序根据就绪的状态,进行相应的IO系统调用。 只要有可读取的数据,就可以得到进一步的执行!当连接数量增加之后,这一特性将变得非常有用。 (大量并发情况下效率提升,如果只有一个,并没有什么优势)

注意: 目前支持IO多路复用的系统调用,有select、 epoll等等。 select系统调用,几乎在所有的操作系统上都有支持,具有良好的跨平台特性。 epoll是在Linux 2.6内核中提出的,是select系统调用的Linux增强版本。

在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的socket连接的就绪状态,当某个或者某些socket网络连接有IO就绪状态。 就返回这些就绪的状态(或者说就绪事件)。

流程如下:
( 1)选择器注册。

在这种模式中,首先,将需要sys_read操作的目标文件描述符( socket连接),提前注册到Linux的select/epoll选择器中,在Java中所对应的选择器类是Selector类。然后,才可以开启整个IO多路复用模型的轮询流程。

( 2)就绪状态的轮询。

通过选择器的查询方法,查询所有的提前注册过的目标文件描述符( socket连接)的IO就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好或者就绪了,就是内核缓冲区有数据了,内核就将该socket加入到就绪的列表中,并且返回就绪事件。

(3)发起调用。

用户线程获得了就绪状态的列表后,根据其中的socket连接,发起sys_read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。

(4)返回结果继续执行。

复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。

IO多路复用模型的优点:

一个选择器查询线程,可以同时处理成千上万的网络连接。 所以,用户程序不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。 这是一个线程维护一个连接的阻塞IO模式相比,使用多路IO复用模型的最大优势

通过JDK的源码可以看出, Java语言的NIO(New IO)组件,在Linux系统上,是使用的是select系统调用实现的。 所以, Java语言的NIO(New IO)组件所使用的,就是IO多路复用模型。

和NIO模型相似,多路复用IO也需要轮询。

负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。 但是同时轮询了所有的连接,而不是每一个线程自己轮询。

IO多路复用模型的缺点:

本质上, select/epoll系统调用是阻塞式的,属于同步阻塞IO。 都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个事件的查询过程是阻塞的。 如果彻底地解除线程的阻塞,就必须使用异步IO模型。

信号驱动式IO

image.png

在信号驱动IO模型中,第一个阶段,用户线程发起调用时,通过向核心注册IO事件的回调函数,来避免IO时间查询的阻塞。 用户进程预先在内核中设置一个回调函数。

当某个事件发生时,内核使用信号(SIGIO)通知进程运行回调函数。 然后进入IO操作的第二个阶段——执行阶段: 用户线程会继续执行,在信号回调函数中调用IO读写操作来进行实际的IO请求操作

信号驱动模式,在初始阶段,等待通知,不会被阻塞。 但是从上图可以看到,调用recvfrom之后,仍旧是阻塞的,需要等待数据复制完成 所以信号驱动模式,也被称之为 低配的 异步模式,或者不完全异步模式

信号驱动IO模型,每当套接字发生IO事件时,系统内核都会向用户进程发送SIGIO事件 所以,一般用于UDP传输,在TCP套接字的开发过程中很少使用,原因是SIGIO信号产生得过于频繁,并且内核发送的SIGIO信号,并没有告诉用户进程发生了什么IO事件。

但是在UDP套接字上, 通过SIGIO信号进行下面两个事件的类型判断即可: 1。数据报到达套接字 2 套接字上发生错误 因此,在SIGIO出现的时候, 用户进程很容易进行判断和做出对应的处理: 如果不是发生错误,那么就是有数据报到达了。

信号驱动IO优点:

用户进程在等待数据时,不会被阻塞,能够提高用户进程的效率。

在信号驱动式I/O模型中,应用程序使用套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。

信号驱动IO缺点:

1 在大量IO事件发生时,可能会由于处理不过来,而导致信号队列溢出。 2 对于处理UDP套接字来讲,对于信号驱动I/O是有用的。可是,对于TCP而言,由于致使SIGIO信号通知的条件为数众多,进行IO信号进一步区分的成本太高,信号驱动的I/O方式近乎无用。

3 信号驱动IO可以看成是一种异步IO,可以简单理解为系统进行用户函数的回调。 只是,信号驱动IO的异步特性,又做的不彻底。 信号驱动IO仅仅在IO事件的通知阶段是异步的, 而在第二阶段, 也就是在将数据从内核缓冲区复制到用户缓冲区这个过程,用户进程是阻塞的、同步的

异步IO(Asynchronous IO)

image.png

AIO的基本流程:

用户线程通过系统调用,向内核注册某个IO操作。 内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。

在异步IO模型中,在整个内核的数据处理过程中,包括内核将数据从网络物理设备(网卡)读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。

用户空间与内核空间的调用方式产生了翻转。

第一阶段,通过调用,直接返回,不会发生阻塞,也不需要等待,可以继续进行下一步的操作。 第二阶段,数据从内核空间复制到用户空间之后,通知用户进程,用户进程无需等待数据的拷贝。 在第二阶段,内核空间成了主动调用者。

异步IO类似于Java中典型的回调模式,用户进程(或者线程)向内核空间注册了各种IO事件的回调函数,由内核去主动调用。

异步IO包含两种:
  • 不完全异步的信号驱动IO模型
  • 完全的异步IO模型
异步IO模型的特点:

在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。 用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。 正因为如此,异步IO有的时候也被称为信号驱动IO。

异步IO异步模型的缺点:

应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。

理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。 就目前而言, Windows系统下通过IOCP实现了真正的异步IO。 而在Linux系统下,异步IO模型在2.6版本才引入, JDK的对其的支持目前并不完善,因此异步IO在性能上没有明显的优势。

image.png

从上面的图中,可以发现,其实前面四种都会有阻塞,只有异步IO模型 才符合POSIX关于异步IO的定义。 注:以上截图部分来自于《UNIX网络编程》

小结: 以上为操作系统视角分析的IO的模型,而Java的NIO是基于IO多路复用的,所以深入理解NIO的模型,对于后面Netty的展开有重大意义。 因为NIO的面向对象的设计模型,就是基于IO多路复用的抽象。 common_log.png 转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-48-1-1.html

关注下面的标签,发现更多相似文章

文章被以下专栏收录:

    黄小斜学Java

    疯狂的字节X

  • 目前专注于分享Java领域干货,公众号同步更新。原创以及收集整理,把最好的留下。
    包括但不限于JVM、计算机科学、算法、数据库、分布式、Spring全家桶、微服务、高并发、Docker容器、ELK、大数据等相关知识,一起进步,一起成长。
热门推荐
海康摄像头接入 wvp-GB28181-pro平台测试验
[md]### 简介 开箱即用的28181协议视频平台 `https://github.c
[CXX1300] CMake '3.18.1' was not
[md][CXX1300] CMake '3.18.1' was not found in SDK, PATH, or
[若依]微服务springcloud版新建增添加一个
[md]若依框架是一个比较出名的后台管理系统,有多个不同版本。